New feature flag with ability to migrate GV1 groups
This commit is contained in:
parent
089a6fb5a2
commit
2b8ae412e0
26 changed files with 608 additions and 189 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
@ -30,7 +30,7 @@ jobs:
|
|||
- run: yarn generate
|
||||
- run: yarn lint
|
||||
- run: yarn lint-deps
|
||||
- run: git diff --quiet --exit-code
|
||||
- run: git diff --exit-code
|
||||
|
||||
macos:
|
||||
needs: lint
|
||||
|
|
|
@ -3968,6 +3968,16 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"GroupV1--Migration--disabled": {
|
||||
"message": "Upgrade this group to activate new features like @mentions and admins. Members who have not shared their name or photo in this group will be invited to join. $learnMore$",
|
||||
"description": "Shown instead of composition area when user is forced to migrate a legacy group (GV1).",
|
||||
"placeholders": {
|
||||
"learnMore": {
|
||||
"content": "$1",
|
||||
"example": "Learn more."
|
||||
}
|
||||
}
|
||||
},
|
||||
"GroupV1--Migration--was-upgraded": {
|
||||
"message": "This group was upgraded to a New Group.",
|
||||
"description": "Shown in timeline when a legacy group (GV1) is upgraded to a new group (GV2)"
|
||||
|
|
|
@ -73,6 +73,9 @@ const {
|
|||
createConversationHeader,
|
||||
} = require('../../ts/state/roots/createConversationHeader');
|
||||
const { createCallManager } = require('../../ts/state/roots/createCallManager');
|
||||
const {
|
||||
createGroupV1MigrationModal,
|
||||
} = require('../../ts/state/roots/createGroupV1MigrationModal');
|
||||
const { createLeftPane } = require('../../ts/state/roots/createLeftPane');
|
||||
const {
|
||||
createSafetyNumberViewer,
|
||||
|
@ -326,6 +329,7 @@ exports.setup = (options = {}) => {
|
|||
createCompositionArea,
|
||||
createContactModal,
|
||||
createConversationHeader,
|
||||
createGroupV1MigrationModal,
|
||||
createLeftPane,
|
||||
createSafetyNumberViewer,
|
||||
createShortcutGuideModal,
|
||||
|
|
|
@ -6983,6 +6983,10 @@ button.module-image__border-overlay:focus {
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.module-timeline--disabled {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.module-timeline__message-container {
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
|
@ -9834,6 +9838,59 @@ button.module-image__border-overlay:focus {
|
|||
@include button-secondary-blue-text;
|
||||
}
|
||||
|
||||
// Module: GroupV1 Disabled Actions
|
||||
|
||||
.module-group-v1-disabled-actions {
|
||||
padding: 8px 16px 12px 16px;
|
||||
max-width: 650px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
@include light-theme {
|
||||
background: $color-white;
|
||||
}
|
||||
|
||||
@include dark-theme {
|
||||
background: $color-gray-95;
|
||||
}
|
||||
}
|
||||
|
||||
.module-group-v1-disabled-actions__message {
|
||||
@include font-body-2;
|
||||
text-align: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
@include light-theme {
|
||||
color: $color-gray-60;
|
||||
}
|
||||
|
||||
@include dark-theme {
|
||||
color: $color-gray-25;
|
||||
}
|
||||
}
|
||||
|
||||
.module-group-v1-disabled-actions__message__learn-more {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.module-group-v1-disabled-actions__buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
.module-group-v1-disabled-actions__buttons__button {
|
||||
@include button-reset;
|
||||
@include font-body-1-bold;
|
||||
|
||||
border-radius: 4px;
|
||||
|
||||
padding: 8px;
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
|
||||
@include button-primary;
|
||||
}
|
||||
|
||||
// Module: Modal Host
|
||||
|
||||
.module-modal-host__overlay {
|
||||
|
|
|
@ -7,11 +7,14 @@ import { WebAPIType } from './textsecure/WebAPI';
|
|||
type ConfigKeyType =
|
||||
| 'desktop.cds'
|
||||
| 'desktop.clientExpiration'
|
||||
| 'desktop.disableGV1'
|
||||
| 'desktop.gv2'
|
||||
| 'desktop.mandatoryProfileSharing'
|
||||
| 'desktop.messageRequests'
|
||||
| 'desktop.storage'
|
||||
| 'desktop.storageWrite';
|
||||
| 'desktop.storageWrite'
|
||||
| 'global.groupsv2.maxGroupSize'
|
||||
| 'global.groupsv2.groupSizeHardLimit';
|
||||
type ConfigValueType = {
|
||||
name: ConfigKeyType;
|
||||
enabled: boolean;
|
||||
|
@ -112,3 +115,7 @@ export const maybeRefreshRemoteConfig = throttle(
|
|||
export function isEnabled(name: ConfigKeyType): boolean {
|
||||
return get(config, [name, 'enabled'], false);
|
||||
}
|
||||
|
||||
export function getValue(name: ConfigKeyType): string | undefined {
|
||||
return get(config, [name, 'value'], undefined);
|
||||
}
|
||||
|
|
|
@ -69,6 +69,8 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
overrideProps.messageRequestsEnabled || false
|
||||
),
|
||||
title: '',
|
||||
// GroupV1 Disabled Actions
|
||||
onStartGroupMigration: action('onStartGroupMigration'),
|
||||
});
|
||||
|
||||
story.add('Default', () => {
|
||||
|
|
|
@ -18,6 +18,10 @@ import {
|
|||
MessageRequestActions,
|
||||
Props as MessageRequestActionsProps,
|
||||
} from './conversation/MessageRequestActions';
|
||||
import {
|
||||
GroupV1DisabledActions,
|
||||
PropsType as GroupV1DisabledActionsPropsType,
|
||||
} from './conversation/GroupV1DisabledActions';
|
||||
import { MandatoryProfileSharingActions } from './conversation/MandatoryProfileSharingActions';
|
||||
import { countStickers } from './stickers/lib';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
|
@ -27,6 +31,7 @@ export type OwnProps = {
|
|||
readonly i18n: LocalizerType;
|
||||
readonly areWePending?: boolean;
|
||||
readonly groupVersion?: 1 | 2;
|
||||
readonly isGroupV1AndDisabled?: boolean;
|
||||
readonly isMissingMandatoryProfileSharing?: boolean;
|
||||
readonly messageRequestsEnabled?: boolean;
|
||||
readonly acceptedMessageRequest?: boolean;
|
||||
|
@ -77,6 +82,7 @@ export type Props = Pick<
|
|||
| 'clearShowPickerHint'
|
||||
> &
|
||||
MessageRequestActionsProps &
|
||||
Pick<GroupV1DisabledActionsPropsType, 'onStartGroupMigration'> &
|
||||
OwnProps;
|
||||
|
||||
const emptyElement = (el: HTMLElement) => {
|
||||
|
@ -135,6 +141,9 @@ export const CompositionArea = ({
|
|||
phoneNumber,
|
||||
profileName,
|
||||
title,
|
||||
// GroupV1 Disabled Actions
|
||||
isGroupV1AndDisabled,
|
||||
onStartGroupMigration,
|
||||
}: Props): JSX.Element => {
|
||||
const [disabled, setDisabled] = React.useState(false);
|
||||
const [showMic, setShowMic] = React.useState(!draftText);
|
||||
|
@ -381,6 +390,16 @@ export const CompositionArea = ({
|
|||
);
|
||||
}
|
||||
|
||||
// If this is a V1 group, now disabled entirely, we show UI to help them upgrade
|
||||
if (isGroupV1AndDisabled) {
|
||||
return (
|
||||
<GroupV1DisabledActions
|
||||
i18n={i18n}
|
||||
onStartGroupMigration={onStartGroupMigration}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="module-composition-area">
|
||||
<div className="module-composition-area__toggle-large">
|
||||
|
|
|
@ -570,6 +570,13 @@ export const CompositionInput: React.ComponentType<Props> = props => {
|
|||
[]
|
||||
);
|
||||
|
||||
// The onClick handler below is only to make it easier for mouse users to focus the
|
||||
// message box. In 'large' mode, the actual Quill text box can be one line while the
|
||||
// visual text box is much larger. Clicking that should allow you to start typing,
|
||||
// hence the click handler.
|
||||
// eslint-disable-next-line max-len
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */
|
||||
|
||||
return (
|
||||
<Manager>
|
||||
<Reference>
|
||||
|
@ -577,6 +584,7 @@ export const CompositionInput: React.ComponentType<Props> = props => {
|
|||
<div className="module-composition-input__input" ref={ref}>
|
||||
<div
|
||||
ref={scrollerRef}
|
||||
onClick={focus}
|
||||
className={classNames(
|
||||
'module-composition-input__input__scroller',
|
||||
large
|
||||
|
|
|
@ -43,7 +43,6 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
|||
),
|
||||
i18n,
|
||||
invitedMembers: overrideProps.invitedMembers || [contact2],
|
||||
learnMore: action('learnMore'),
|
||||
migrate: action('migrate'),
|
||||
onClose: action('onClose'),
|
||||
});
|
||||
|
|
|
@ -19,7 +19,6 @@ export type DataPropsType = {
|
|||
readonly droppedMembers: Array<ConversationType>;
|
||||
readonly hasMigrated: boolean;
|
||||
readonly invitedMembers: Array<ConversationType>;
|
||||
readonly learnMore: CallbackType;
|
||||
readonly migrate: CallbackType;
|
||||
readonly onClose: CallbackType;
|
||||
};
|
||||
|
@ -42,7 +41,6 @@ export const GroupV1MigrationDialog = React.memo((props: PropsType) => {
|
|||
hasMigrated,
|
||||
i18n,
|
||||
invitedMembers,
|
||||
learnMore,
|
||||
migrate,
|
||||
onClose,
|
||||
} = props;
|
||||
|
@ -85,7 +83,7 @@ export const GroupV1MigrationDialog = React.memo((props: PropsType) => {
|
|||
)}
|
||||
{renderMembers(droppedMembers, droppedMembersKey, i18n)}
|
||||
</div>
|
||||
{renderButtons(hasMigrated, onClose, learnMore, migrate, i18n)}
|
||||
{renderButtons(hasMigrated, onClose, migrate, i18n)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
@ -93,7 +91,6 @@ export const GroupV1MigrationDialog = React.memo((props: PropsType) => {
|
|||
function renderButtons(
|
||||
hasMigrated: boolean,
|
||||
onClose: CallbackType,
|
||||
learnMore: CallbackType,
|
||||
migrate: CallbackType,
|
||||
i18n: LocalizerType
|
||||
) {
|
||||
|
@ -125,9 +122,9 @@ function renderButtons(
|
|||
'module-group-v2-migration-dialog__button--secondary'
|
||||
)}
|
||||
type="button"
|
||||
onClick={learnMore}
|
||||
onClick={onClose}
|
||||
>
|
||||
{i18n('GroupV1--Migration--learn-more')}
|
||||
{i18n('cancel')}
|
||||
</button>
|
||||
<button
|
||||
className="module-group-v2-migration-dialog__button"
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import {
|
||||
GroupV1DisabledActions,
|
||||
PropsType as GroupV1DisabledActionsPropsType,
|
||||
} from './GroupV1DisabledActions';
|
||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const createProps = (): GroupV1DisabledActionsPropsType => ({
|
||||
i18n,
|
||||
onStartGroupMigration: action('onStartGroupMigration'),
|
||||
});
|
||||
|
||||
const stories = storiesOf(
|
||||
'Components/Conversation/GroupV1DisabledActions',
|
||||
module
|
||||
);
|
||||
|
||||
stories.add('Default', () => {
|
||||
return <GroupV1DisabledActions {...createProps()} />;
|
||||
});
|
49
ts/components/conversation/GroupV1DisabledActions.tsx
Normal file
49
ts/components/conversation/GroupV1DisabledActions.tsx
Normal file
|
@ -0,0 +1,49 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import { Intl } from '../Intl';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
export type PropsType = {
|
||||
i18n: LocalizerType;
|
||||
onStartGroupMigration: () => unknown;
|
||||
};
|
||||
|
||||
export const GroupV1DisabledActions = ({
|
||||
i18n,
|
||||
onStartGroupMigration,
|
||||
}: PropsType): JSX.Element => {
|
||||
return (
|
||||
<div className="module-group-v1-disabled-actions">
|
||||
<p className="module-group-v1-disabled-actions__message">
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id="GroupV1--Migration--disabled"
|
||||
components={{
|
||||
learnMore: (
|
||||
<a
|
||||
href="https://support.signal.org/hc/articles/360007319331"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="module-group-v1-disabled-actions__message__learn-more"
|
||||
>
|
||||
{i18n('MessageRequests--learn-more')}
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<div className="module-group-v1-disabled-actions__buttons">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onStartGroupMigration}
|
||||
tabIndex={0}
|
||||
className="module-group-v1-disabled-actions__buttons__button"
|
||||
>
|
||||
{i18n('MessageRequests--continue')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -55,9 +55,6 @@ export function GroupV1Migration(props: PropsType): React.ReactElement {
|
|||
hasMigrated
|
||||
i18n={i18n}
|
||||
invitedMembers={invitedMembers}
|
||||
learnMore={() =>
|
||||
window.log.warn('GroupV1Migration: Modal called learnMore()')
|
||||
}
|
||||
migrate={() =>
|
||||
window.log.warn('GroupV1Migration: Modal called migrate()')
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import { action } from '@storybook/addon-actions';
|
|||
|
||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
import { Props, Timeline } from './Timeline';
|
||||
import { PropsType, Timeline } from './Timeline';
|
||||
import { TimelineItem, TimelineItemType } from './TimelineItem';
|
||||
import { LastSeenIndicator } from './LastSeenIndicator';
|
||||
import { TimelineLoadingRow } from './TimelineLoadingRow';
|
||||
|
@ -278,7 +278,7 @@ const renderTypingBubble = () => (
|
|||
/>
|
||||
);
|
||||
|
||||
const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
i18n,
|
||||
|
||||
haveNewest: boolean('haveNewest', overrideProps.haveNewest !== false),
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { debounce, get, isNumber } from 'lodash';
|
||||
import classNames from 'classnames';
|
||||
import React, { CSSProperties } from 'react';
|
||||
import {
|
||||
AutoSizer,
|
||||
|
@ -44,6 +45,8 @@ type PropsHousekeepingType = {
|
|||
id: string;
|
||||
unreadCount?: number;
|
||||
typingContact?: unknown;
|
||||
isGroupV1AndDisabled?: boolean;
|
||||
|
||||
selectedMessageId?: string;
|
||||
|
||||
i18n: LocalizerType;
|
||||
|
@ -82,7 +85,9 @@ type PropsActionsType = {
|
|||
} & MessageActionsType &
|
||||
SafetyNumberActionsType;
|
||||
|
||||
export type Props = PropsDataType & PropsHousekeepingType & PropsActionsType;
|
||||
export type PropsType = PropsDataType &
|
||||
PropsHousekeepingType &
|
||||
PropsActionsType;
|
||||
|
||||
// from https://github.com/bvaughn/react-virtualized/blob/fb3484ed5dcc41bffae8eab029126c0fb8f7abc0/source/List/types.js#L5
|
||||
type RowRendererParamsType = {
|
||||
|
@ -120,7 +125,7 @@ type VisibleRowsType = {
|
|||
};
|
||||
};
|
||||
|
||||
type State = {
|
||||
type StateType = {
|
||||
atBottom: boolean;
|
||||
atTop: boolean;
|
||||
oneTimeScrollRow?: number;
|
||||
|
@ -133,7 +138,7 @@ type State = {
|
|||
areUnreadBelowCurrentPosition: boolean;
|
||||
};
|
||||
|
||||
export class Timeline extends React.PureComponent<Props, State> {
|
||||
export class Timeline extends React.PureComponent<PropsType, StateType> {
|
||||
public cellSizeCache = new CellMeasurerCache({
|
||||
defaultHeight: 64,
|
||||
fixedWidth: true,
|
||||
|
@ -153,7 +158,7 @@ export class Timeline extends React.PureComponent<Props, State> {
|
|||
|
||||
public loadCountdownTimeout: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(props: Props) {
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
|
||||
const { scrollToIndex } = this.props;
|
||||
|
@ -170,7 +175,10 @@ export class Timeline extends React.PureComponent<Props, State> {
|
|||
};
|
||||
}
|
||||
|
||||
public static getDerivedStateFromProps(props: Props, state: State): State {
|
||||
public static getDerivedStateFromProps(
|
||||
props: PropsType,
|
||||
state: StateType
|
||||
): StateType {
|
||||
if (
|
||||
isNumber(props.scrollToIndex) &&
|
||||
(props.scrollToIndex !== state.prevPropScrollToIndex ||
|
||||
|
@ -646,7 +654,10 @@ export class Timeline extends React.PureComponent<Props, State> {
|
|||
return itemsCount + extraRows;
|
||||
}
|
||||
|
||||
public fromRowToItemIndex(row: number, props?: Props): number | undefined {
|
||||
public fromRowToItemIndex(
|
||||
row: number,
|
||||
props?: PropsType
|
||||
): number | undefined {
|
||||
const { items } = props || this.props;
|
||||
|
||||
// We will always render either the hero row or the loading row
|
||||
|
@ -666,7 +677,7 @@ export class Timeline extends React.PureComponent<Props, State> {
|
|||
return index;
|
||||
}
|
||||
|
||||
public getLastSeenIndicatorRow(props?: Props): number | undefined {
|
||||
public getLastSeenIndicatorRow(props?: PropsType): number | undefined {
|
||||
const { oldestUnreadIndex } = props || this.props;
|
||||
if (!isNumber(oldestUnreadIndex)) {
|
||||
return;
|
||||
|
@ -785,7 +796,7 @@ export class Timeline extends React.PureComponent<Props, State> {
|
|||
window.unregisterForActive(this.updateWithVisibleRows);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Props): void {
|
||||
public componentDidUpdate(prevProps: PropsType): void {
|
||||
const {
|
||||
id,
|
||||
clearChangedMessages,
|
||||
|
@ -1052,7 +1063,7 @@ export class Timeline extends React.PureComponent<Props, State> {
|
|||
};
|
||||
|
||||
public render(): JSX.Element | null {
|
||||
const { i18n, id, items } = this.props;
|
||||
const { i18n, id, items, isGroupV1AndDisabled } = this.props;
|
||||
const {
|
||||
shouldShowScrollDownButton,
|
||||
areUnreadBelowCurrentPosition,
|
||||
|
@ -1067,7 +1078,10 @@ export class Timeline extends React.PureComponent<Props, State> {
|
|||
|
||||
return (
|
||||
<div
|
||||
className="module-timeline"
|
||||
className={classNames(
|
||||
'module-timeline',
|
||||
isGroupV1AndDisabled ? 'module-timeline--disabled' : null
|
||||
)}
|
||||
role="presentation"
|
||||
tabIndex={-1}
|
||||
onBlur={this.handleBlur}
|
||||
|
|
326
ts/groups.ts
326
ts/groups.ts
|
@ -7,6 +7,7 @@ import {
|
|||
difference,
|
||||
flatten,
|
||||
fromPairs,
|
||||
isFinite,
|
||||
isNumber,
|
||||
values,
|
||||
} from 'lodash';
|
||||
|
@ -722,6 +723,168 @@ export async function isGroupEligibleToMigrate(
|
|||
return true;
|
||||
}
|
||||
|
||||
export async function getGroupMigrationMembers(
|
||||
conversation: ConversationModel
|
||||
): Promise<{
|
||||
areWeInvited: boolean;
|
||||
areWeMember: boolean;
|
||||
droppedGV2MemberIds: Array<string>;
|
||||
membersV2: Array<GroupV2MemberType>;
|
||||
pendingMembersV2: Array<GroupV2PendingMemberType>;
|
||||
previousGroupV1Members: Array<string>;
|
||||
}> {
|
||||
const logId = conversation.idForLogging();
|
||||
const MEMBER_ROLE_ENUM = window.textsecure.protobuf.Member.Role;
|
||||
|
||||
const ourConversationId = window.ConversationController.getOurConversationId();
|
||||
if (!ourConversationId) {
|
||||
throw new Error(
|
||||
`getGroupMigrationMembers/${logId}: Couldn't fetch our own conversationId!`
|
||||
);
|
||||
}
|
||||
|
||||
let areWeMember = false;
|
||||
let areWeInvited = false;
|
||||
|
||||
const previousGroupV1Members = conversation.get('members') || [];
|
||||
const now = Date.now();
|
||||
const memberLookup: Record<string, boolean> = {};
|
||||
const membersV2: Array<GroupV2MemberType> = compact(
|
||||
await Promise.all(
|
||||
previousGroupV1Members.map(async e164 => {
|
||||
const contact = window.ConversationController.get(e164);
|
||||
|
||||
if (!contact) {
|
||||
throw new Error(
|
||||
`getGroupMigrationMembers/${logId}: membersV2 - missing local contact for ${e164}, skipping.`
|
||||
);
|
||||
}
|
||||
if (!contact.get('uuid')) {
|
||||
window.log.warn(
|
||||
`getGroupMigrationMembers/${logId}: membersV2 - missing uuid for ${e164}, skipping.`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!contact.get('profileKey')) {
|
||||
window.log.warn(
|
||||
`getGroupMigrationMembers/${logId}: membersV2 - missing profileKey for member ${e164}, skipping.`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
let capabilities = contact.get('capabilities');
|
||||
|
||||
// Refresh our local data to be sure
|
||||
if (
|
||||
!capabilities ||
|
||||
!capabilities.gv2 ||
|
||||
!capabilities['gv1-migration'] ||
|
||||
!contact.get('profileKeyCredential')
|
||||
) {
|
||||
await contact.getProfiles();
|
||||
}
|
||||
|
||||
capabilities = contact.get('capabilities');
|
||||
if (!capabilities || !capabilities.gv2) {
|
||||
window.log.warn(
|
||||
`getGroupMigrationMembers/${logId}: membersV2 - member ${e164} is missing gv2 capability, skipping.`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
if (!capabilities || !capabilities['gv1-migration']) {
|
||||
window.log.warn(
|
||||
`getGroupMigrationMembers/${logId}: membersV2 - member ${e164} is missing gv1-migration capability, skipping.`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
if (!contact.get('profileKeyCredential')) {
|
||||
window.log.warn(
|
||||
`getGroupMigrationMembers/${logId}: membersV2 - no profileKeyCredential for ${e164}, skipping.`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
const conversationId = contact.id;
|
||||
|
||||
if (conversationId === ourConversationId) {
|
||||
areWeMember = true;
|
||||
}
|
||||
|
||||
memberLookup[conversationId] = true;
|
||||
|
||||
return {
|
||||
conversationId,
|
||||
role: MEMBER_ROLE_ENUM.ADMINISTRATOR,
|
||||
joinedAtVersion: 0,
|
||||
};
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const droppedGV2MemberIds: Array<string> = [];
|
||||
const pendingMembersV2: Array<GroupV2PendingMemberType> = compact(
|
||||
(previousGroupV1Members || []).map(e164 => {
|
||||
const contact = window.ConversationController.get(e164);
|
||||
|
||||
if (!contact) {
|
||||
throw new Error(
|
||||
`getGroupMigrationMembers/${logId}: pendingMembersV2 - missing local contact for ${e164}, skipping.`
|
||||
);
|
||||
}
|
||||
|
||||
const conversationId = contact.id;
|
||||
// If we've already added this contact above, we'll skip here
|
||||
if (memberLookup[conversationId]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!contact.get('uuid')) {
|
||||
window.log.warn(
|
||||
`getGroupMigrationMembers/${logId}: pendingMembersV2 - missing uuid for ${e164}, skipping.`
|
||||
);
|
||||
droppedGV2MemberIds.push(conversationId);
|
||||
return null;
|
||||
}
|
||||
|
||||
const capabilities = contact.get('capabilities');
|
||||
if (!capabilities || !capabilities.gv2) {
|
||||
window.log.warn(
|
||||
`getGroupMigrationMembers/${logId}: pendingMembersV2 - member ${e164} is missing gv2 capability, skipping.`
|
||||
);
|
||||
droppedGV2MemberIds.push(conversationId);
|
||||
return null;
|
||||
}
|
||||
if (!capabilities || !capabilities['gv1-migration']) {
|
||||
window.log.warn(
|
||||
`getGroupMigrationMembers/${logId}: pendingMembersV2 - member ${e164} is missing gv1-migration capability, skipping.`
|
||||
);
|
||||
droppedGV2MemberIds.push(conversationId);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (conversationId === ourConversationId) {
|
||||
areWeInvited = true;
|
||||
}
|
||||
|
||||
return {
|
||||
conversationId,
|
||||
timestamp: now,
|
||||
addedByUserId: ourConversationId,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
areWeInvited,
|
||||
areWeMember,
|
||||
droppedGV2MemberIds,
|
||||
membersV2,
|
||||
pendingMembersV2,
|
||||
previousGroupV1Members,
|
||||
};
|
||||
}
|
||||
|
||||
// This is called when the user chooses to migrate a GroupV1. It will update the server,
|
||||
// then let all members know about the new group.
|
||||
export async function initiateMigrationToGroupV2(
|
||||
|
@ -732,7 +895,6 @@ export async function initiateMigrationToGroupV2(
|
|||
|
||||
try {
|
||||
await conversation.queueJob(async () => {
|
||||
const MEMBER_ROLE_ENUM = window.textsecure.protobuf.Member.Role;
|
||||
const ACCESS_ENUM =
|
||||
window.textsecure.protobuf.AccessControl.AccessRequired;
|
||||
|
||||
|
@ -766,138 +928,14 @@ export async function initiateMigrationToGroupV2(
|
|||
);
|
||||
}
|
||||
|
||||
let areWeMember = false;
|
||||
let areWeInvited = false;
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
const previousGroupV1Members = conversation.get('members') || [];
|
||||
const memberLookup: Record<string, boolean> = {};
|
||||
const membersV2: Array<GroupV2MemberType> = compact(
|
||||
await Promise.all(
|
||||
previousGroupV1Members.map(async e164 => {
|
||||
const contact = window.ConversationController.get(e164);
|
||||
|
||||
if (!contact) {
|
||||
throw new Error(
|
||||
`initiateMigrationToGroupV2/${logId}: membersV2 - missing local contact for ${e164}, skipping.`
|
||||
);
|
||||
}
|
||||
if (!contact.get('uuid')) {
|
||||
window.log.warn(
|
||||
`initiateMigrationToGroupV2/${logId}: membersV2 - missing uuid for ${e164}, skipping.`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!contact.get('profileKey')) {
|
||||
window.log.warn(
|
||||
`initiateMigrationToGroupV2/${logId}: membersV2 - missing profileKey for member ${e164}, skipping.`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
let capabilities = contact.get('capabilities');
|
||||
|
||||
// Refresh our local data to be sure
|
||||
if (
|
||||
!capabilities ||
|
||||
!capabilities.gv2 ||
|
||||
!capabilities['gv1-migration'] ||
|
||||
!contact.get('profileKeyCredential')
|
||||
) {
|
||||
await contact.getProfiles();
|
||||
}
|
||||
|
||||
capabilities = contact.get('capabilities');
|
||||
if (!capabilities || !capabilities.gv2) {
|
||||
window.log.warn(
|
||||
`initiateMigrationToGroupV2/${logId}: membersV2 - member ${e164} is missing gv2 capability, skipping.`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
if (!capabilities || !capabilities['gv1-migration']) {
|
||||
window.log.warn(
|
||||
`initiateMigrationToGroupV2/${logId}: membersV2 - member ${e164} is missing gv1-migration capability, skipping.`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
if (!contact.get('profileKeyCredential')) {
|
||||
window.log.warn(
|
||||
`initiateMigrationToGroupV2/${logId}: membersV2 - no profileKeyCredential for ${e164}, skipping.`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
const conversationId = contact.id;
|
||||
|
||||
if (conversationId === ourConversationId) {
|
||||
areWeMember = true;
|
||||
}
|
||||
|
||||
memberLookup[conversationId] = true;
|
||||
|
||||
return {
|
||||
conversationId,
|
||||
role: MEMBER_ROLE_ENUM.ADMINISTRATOR,
|
||||
joinedAtVersion: 0,
|
||||
};
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const droppedGV2MemberIds: Array<string> = [];
|
||||
const pendingMembersV2: Array<GroupV2PendingMemberType> = compact(
|
||||
(previousGroupV1Members || []).map(e164 => {
|
||||
const contact = window.ConversationController.get(e164);
|
||||
|
||||
if (!contact) {
|
||||
throw new Error(
|
||||
`initiateMigrationToGroupV2/${logId}: pendingMembersV2 - missing local contact for ${e164}, skipping.`
|
||||
);
|
||||
}
|
||||
|
||||
const conversationId = contact.id;
|
||||
// If we've already added this contact above, we'll skip here
|
||||
if (memberLookup[conversationId]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!contact.get('uuid')) {
|
||||
window.log.warn(
|
||||
`initiateMigrationToGroupV2/${logId}: pendingMembersV2 - missing uuid for ${e164}, skipping.`
|
||||
);
|
||||
droppedGV2MemberIds.push(conversationId);
|
||||
return null;
|
||||
}
|
||||
|
||||
const capabilities = contact.get('capabilities');
|
||||
if (!capabilities || !capabilities.gv2) {
|
||||
window.log.warn(
|
||||
`initiateMigrationToGroupV2/${logId}: pendingMembersV2 - member ${e164} is missing gv2 capability, skipping.`
|
||||
);
|
||||
droppedGV2MemberIds.push(conversationId);
|
||||
return null;
|
||||
}
|
||||
if (!capabilities || !capabilities['gv1-migration']) {
|
||||
window.log.warn(
|
||||
`initiateMigrationToGroupV2/${logId}: pendingMembersV2 - member ${e164} is missing gv1-migration capability, skipping.`
|
||||
);
|
||||
droppedGV2MemberIds.push(conversationId);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (conversationId === ourConversationId) {
|
||||
areWeInvited = true;
|
||||
}
|
||||
|
||||
return {
|
||||
conversationId,
|
||||
timestamp: now,
|
||||
addedByUserId: ourConversationId,
|
||||
};
|
||||
})
|
||||
);
|
||||
const {
|
||||
areWeMember,
|
||||
areWeInvited,
|
||||
membersV2,
|
||||
pendingMembersV2,
|
||||
droppedGV2MemberIds,
|
||||
previousGroupV1Members,
|
||||
} = await getGroupMigrationMembers(conversation);
|
||||
|
||||
if (!areWeMember) {
|
||||
throw new Error(
|
||||
|
@ -910,6 +948,26 @@ export async function initiateMigrationToGroupV2(
|
|||
);
|
||||
}
|
||||
|
||||
const rawSizeLimit = window.Signal.RemoteConfig.getValue(
|
||||
'global.groupsv2.groupSizeHardLimit'
|
||||
);
|
||||
if (!rawSizeLimit) {
|
||||
throw new Error(
|
||||
`initiateMigrationToGroupV2/${logId}: Failed to fetch group size limit`
|
||||
);
|
||||
}
|
||||
const sizeLimit = parseInt(rawSizeLimit, 10);
|
||||
if (!isFinite(sizeLimit)) {
|
||||
throw new Error(
|
||||
`initiateMigrationToGroupV2/${logId}: Failed to parse group size limit`
|
||||
);
|
||||
}
|
||||
if (membersV2.length + pendingMembersV2.length > sizeLimit) {
|
||||
throw new Error(
|
||||
`initiateMigrationToGroupV2/${logId}: Too many members! Member count: ${membersV2.length}, Pending member count: ${pendingMembersV2.length}`
|
||||
);
|
||||
}
|
||||
|
||||
// Note: A few group elements don't need to change here:
|
||||
// - avatar
|
||||
// - name
|
||||
|
@ -2004,7 +2062,7 @@ async function integrateGroupChange({
|
|||
};
|
||||
}
|
||||
|
||||
export async function getCurrentGroupState({
|
||||
async function getCurrentGroupState({
|
||||
authCredentialBase64,
|
||||
dropInitialJoinMessage,
|
||||
group,
|
||||
|
|
|
@ -632,6 +632,13 @@ export class ConversationModel extends window.Backbone.Model<
|
|||
window.Signal.Data.updateConversation(this.attributes);
|
||||
}
|
||||
|
||||
isGroupV1AndDisabled(): boolean {
|
||||
return (
|
||||
this.isGroupV1() &&
|
||||
window.Signal.RemoteConfig.isEnabled('desktop.disableGV1')
|
||||
);
|
||||
}
|
||||
|
||||
isBlocked(): boolean {
|
||||
const uuid = this.get('uuid');
|
||||
if (uuid) {
|
||||
|
@ -1181,6 +1188,7 @@ export class ConversationModel extends window.Backbone.Model<
|
|||
isArchived: this.get('isArchived')!,
|
||||
isBlocked: this.isBlocked(),
|
||||
isMe: this.isMe(),
|
||||
isGroupV1AndDisabled: this.isGroupV1AndDisabled(),
|
||||
isPinned: this.get('isPinned'),
|
||||
isMissingMandatoryProfileSharing: this.isMissingRequiredProfileSharing(),
|
||||
isVerified: this.isVerified(),
|
||||
|
@ -4063,6 +4071,10 @@ export class ConversationModel extends window.Backbone.Model<
|
|||
return true;
|
||||
}
|
||||
|
||||
if (this.isGroupV1AndDisabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.isGroupV2()) {
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -2079,33 +2079,42 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
const isOutgoing = this.get('type') === 'outgoing';
|
||||
const numDelivered = this.get('delivered');
|
||||
|
||||
// Case 1: If mandatory profile sharing is enabled, and we haven't shared yet, then
|
||||
if (!conversation) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If GroupV1 groups have been disabled, we can't reply.
|
||||
if (conversation.isGroupV1AndDisabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If mandatory profile sharing is enabled, and we haven't shared yet, then
|
||||
// we can't reply.
|
||||
if (conversation?.isMissingRequiredProfileSharing()) {
|
||||
if (conversation.isMissingRequiredProfileSharing()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Case 2: We cannot reply if we have accepted the message request
|
||||
if (!conversation?.getAccepted()) {
|
||||
// We cannot reply if we haven't accepted the message request
|
||||
if (!conversation.getAccepted()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Case 3: We cannot reply if this message is deleted for everyone
|
||||
// We cannot reply if this message is deleted for everyone
|
||||
if (this.get('deletedForEveryone')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Case 4: We can reply if this is outgoing and delievered to at least one recipient
|
||||
// We can reply if this is outgoing and delievered to at least one recipient
|
||||
if (isOutgoing && numDelivered > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Case 5: We can reply if there are no errors
|
||||
// We can reply if there are no errors
|
||||
if (!errors || (errors && errors.length === 0)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Case 6: default
|
||||
// Fail safe.
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -54,6 +54,7 @@ export type ConversationType = {
|
|||
isAccepted?: boolean;
|
||||
isArchived?: boolean;
|
||||
isBlocked?: boolean;
|
||||
isGroupV1AndDisabled?: boolean;
|
||||
isPinned?: boolean;
|
||||
isVerified?: boolean;
|
||||
activeAt?: number;
|
||||
|
|
28
ts/state/roots/createGroupV1MigrationModal.tsx
Normal file
28
ts/state/roots/createGroupV1MigrationModal.tsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { Store } from 'redux';
|
||||
|
||||
import { ModalHost } from '../../components/ModalHost';
|
||||
import {
|
||||
SmartGroupV1MigrationDialog,
|
||||
PropsType,
|
||||
} from '../smart/GroupV1MigrationDialog';
|
||||
|
||||
export const createGroupV1MigrationModal = (
|
||||
store: Store,
|
||||
props: PropsType
|
||||
): React.ReactElement => {
|
||||
const { onClose } = props;
|
||||
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<ModalHost onClose={onClose}>
|
||||
<SmartGroupV1MigrationDialog {...props} />
|
||||
</ModalHost>
|
||||
</Provider>
|
||||
);
|
||||
};
|
59
ts/state/smart/GroupV1MigrationDialog.tsx
Normal file
59
ts/state/smart/GroupV1MigrationDialog.tsx
Normal file
|
@ -0,0 +1,59 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { mapDispatchToProps } from '../actions';
|
||||
import {
|
||||
GroupV1MigrationDialog,
|
||||
PropsType as GroupV1MigrationDialogPropsType,
|
||||
} from '../../components/GroupV1MigrationDialog';
|
||||
import { ConversationType } from '../ducks/conversations';
|
||||
import { StateType } from '../reducer';
|
||||
import { getConversationSelector } from '../selectors/conversations';
|
||||
|
||||
import { getIntl } from '../selectors/user';
|
||||
|
||||
export type PropsType = {
|
||||
readonly droppedMemberIds: Array<string>;
|
||||
readonly invitedMemberIds: Array<string>;
|
||||
} & Omit<
|
||||
GroupV1MigrationDialogPropsType,
|
||||
'i18n' | 'droppedMembers' | 'invitedMembers'
|
||||
>;
|
||||
|
||||
const mapStateToProps = (
|
||||
state: StateType,
|
||||
props: PropsType
|
||||
): GroupV1MigrationDialogPropsType => {
|
||||
const getConversation = getConversationSelector(state);
|
||||
const { droppedMemberIds, invitedMemberIds } = props;
|
||||
|
||||
const droppedMembers = droppedMemberIds
|
||||
.map(getConversation)
|
||||
.filter(Boolean) as Array<ConversationType>;
|
||||
if (droppedMembers.length !== droppedMemberIds.length) {
|
||||
window.log.warn(
|
||||
'smart/GroupV1MigrationDialog: droppedMembers length changed'
|
||||
);
|
||||
}
|
||||
|
||||
const invitedMembers = invitedMemberIds
|
||||
.map(getConversation)
|
||||
.filter(Boolean) as Array<ConversationType>;
|
||||
if (invitedMembers.length !== invitedMemberIds.length) {
|
||||
window.log.warn(
|
||||
'smart/GroupV1MigrationDialog: invitedMembers length changed'
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...props,
|
||||
droppedMembers,
|
||||
invitedMembers,
|
||||
i18n: getIntl(state),
|
||||
};
|
||||
};
|
||||
|
||||
const smart = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
export const SmartGroupV1MigrationDialog = smart(GroupV1MigrationDialog);
|
|
@ -101,7 +101,11 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
|||
|
||||
return {
|
||||
id,
|
||||
...pick(conversation, ['unreadCount', 'typingContact']),
|
||||
...pick(conversation, [
|
||||
'unreadCount',
|
||||
'typingContact',
|
||||
'isGroupV1AndDisabled',
|
||||
]),
|
||||
...conversationMessages,
|
||||
selectedMessageId: selectedMessage ? selectedMessage.id : undefined,
|
||||
i18n: getIntl(state),
|
||||
|
|
|
@ -1077,8 +1077,9 @@ export function initialize({
|
|||
responseType: 'json',
|
||||
});
|
||||
|
||||
return res.config.filter(({ name }: { name: string }) =>
|
||||
name.startsWith('desktop.')
|
||||
return res.config.filter(
|
||||
({ name }: { name: string }) =>
|
||||
name.startsWith('desktop.') || name.startsWith('global.')
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -14463,7 +14463,7 @@
|
|||
"rule": "DOM-innerHTML",
|
||||
"path": "ts/components/CompositionArea.js",
|
||||
"line": " el.innerHTML = '';",
|
||||
"lineNumber": 27,
|
||||
"lineNumber": 28,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-05-20T20:10:43.540Z",
|
||||
"reasonDetail": "Our code, no user input, only clearing out the dom"
|
||||
|
@ -14472,7 +14472,7 @@
|
|||
"rule": "React-useRef",
|
||||
"path": "ts/components/CompositionArea.js",
|
||||
"line": " const inputApiRef = React.useRef();",
|
||||
"lineNumber": 43,
|
||||
"lineNumber": 46,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2020-10-26T19:12:24.410Z",
|
||||
"reasonDetail": "Doesn't refer to a DOM element."
|
||||
|
@ -14481,7 +14481,7 @@
|
|||
"rule": "React-useRef",
|
||||
"path": "ts/components/CompositionArea.js",
|
||||
"line": " const attSlotRef = React.useRef(null);",
|
||||
"lineNumber": 66,
|
||||
"lineNumber": 69,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-10-26T19:12:24.410Z",
|
||||
"reasonDetail": "Needed for the composition area."
|
||||
|
@ -14490,7 +14490,7 @@
|
|||
"rule": "React-useRef",
|
||||
"path": "ts/components/CompositionArea.js",
|
||||
"line": " const micCellRef = React.useRef(null);",
|
||||
"lineNumber": 100,
|
||||
"lineNumber": 103,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-10-26T19:12:24.410Z",
|
||||
"reasonDetail": "Needed for the composition area."
|
||||
|
@ -14499,7 +14499,7 @@
|
|||
"rule": "DOM-innerHTML",
|
||||
"path": "ts/components/CompositionArea.tsx",
|
||||
"line": " el.innerHTML = '';",
|
||||
"lineNumber": 85,
|
||||
"lineNumber": 91,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-06-03T19:23:21.195Z",
|
||||
"reasonDetail": "Our code, no user input, only clearing out the dom"
|
||||
|
@ -14859,7 +14859,7 @@
|
|||
"rule": "React-createRef",
|
||||
"path": "ts/components/conversation/Timeline.js",
|
||||
"line": " this.listRef = react_1.default.createRef();",
|
||||
"lineNumber": 29,
|
||||
"lineNumber": 30,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2019-07-31T00:19:18.696Z",
|
||||
"reasonDetail": "Timeline needs to interact with its child List directly"
|
||||
|
@ -15172,7 +15172,7 @@
|
|||
"rule": "jQuery-wrap(",
|
||||
"path": "ts/textsecure/WebAPI.ts",
|
||||
"line": " const byteBuffer = window.dcodeIO.ByteBuffer.wrap(",
|
||||
"lineNumber": 2171,
|
||||
"lineNumber": 2172,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2020-09-08T23:07:22.682Z"
|
||||
}
|
||||
|
|
|
@ -3,9 +3,11 @@
|
|||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
// Note: because this file is pulled in directly from background.html, we can't use any
|
||||
// imports here aside from types. That means everything will have to be references via
|
||||
// globals right on window.
|
||||
// This allows us to pull in types despite the fact that this is not a module. We can't
|
||||
// use normal import syntax, nor can we use 'import type' syntax, or this will be turned
|
||||
// into a module, and we'll get the dreaded 'exports is not defined' error.
|
||||
// see https://github.com/microsoft/TypeScript/issues/41562
|
||||
type GroupV2PendingMemberType = import('../model-types.d').GroupV2PendingMemberType;
|
||||
|
||||
interface GetLinkPreviewResult {
|
||||
title: string;
|
||||
|
@ -404,8 +406,6 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
},
|
||||
|
||||
events: {
|
||||
'click .composition-area-placeholder': 'onClickPlaceholder',
|
||||
'click .bottom-bar': 'focusMessageField',
|
||||
'click .capture-audio .microphone': 'captureAudio',
|
||||
'change input.file-input': 'onChoseAttachment',
|
||||
|
||||
|
@ -647,6 +647,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
),
|
||||
});
|
||||
},
|
||||
onStartGroupMigration: () => this.startMigrationToGV2(),
|
||||
};
|
||||
|
||||
this.compositionAreaView = new Whisper.ReactWrapperView({
|
||||
|
@ -661,13 +662,13 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
this.$('.composition-area-placeholder').append(this.compositionAreaView.el);
|
||||
},
|
||||
|
||||
async longRunningTaskWrapper({
|
||||
async longRunningTaskWrapper<T>({
|
||||
name,
|
||||
task,
|
||||
}: {
|
||||
name: string;
|
||||
task: () => Promise<void>;
|
||||
}): Promise<void> {
|
||||
task: () => Promise<T>;
|
||||
}): Promise<T> {
|
||||
const idLog = `${name}/${this.model.idForLogging()}`;
|
||||
const ONE_SECOND = 1000;
|
||||
const TWO_SECONDS = 2000;
|
||||
|
@ -690,7 +691,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
// show a spinner until it's done
|
||||
try {
|
||||
window.log.info(`longRunningTaskWrapper/${idLog}: Starting task`);
|
||||
await task();
|
||||
const result = await task();
|
||||
window.log.info(
|
||||
`longRunningTaskWrapper/${idLog}: Task completed successfully`
|
||||
);
|
||||
|
@ -710,6 +711,8 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
progressView.remove();
|
||||
progressView = undefined;
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
window.log.error(
|
||||
`longRunningTaskWrapper/${idLog}: Error!`,
|
||||
|
@ -736,6 +739,8 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
onClose: () => errorView.remove(),
|
||||
},
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -1170,10 +1175,58 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
}
|
||||
},
|
||||
|
||||
// We need this, or clicking the reactified buttons will submit the form and send any
|
||||
// mid-composition message content.
|
||||
onClickPlaceholder(e: any) {
|
||||
e.preventDefault();
|
||||
async startMigrationToGV2(): Promise<void> {
|
||||
const logId = this.model.idForLogging();
|
||||
|
||||
if (!this.model.isGroupV1()) {
|
||||
throw new Error(
|
||||
`startMigrationToGV2/${logId}: Cannot start, not a GroupV1 group`
|
||||
);
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
if (this.migrationDialog) {
|
||||
this.migrationDialog.remove();
|
||||
this.migrationDialog = undefined;
|
||||
}
|
||||
};
|
||||
onClose();
|
||||
|
||||
const migrate = () => {
|
||||
onClose();
|
||||
|
||||
this.longRunningTaskWrapper({
|
||||
name: 'initiateMigrationToGroupV2',
|
||||
task: () => window.Signal.Groups.initiateMigrationToGroupV2(this.model),
|
||||
});
|
||||
};
|
||||
|
||||
// Grab the dropped/invited user set
|
||||
const {
|
||||
droppedGV2MemberIds,
|
||||
pendingMembersV2,
|
||||
} = await this.longRunningTaskWrapper({
|
||||
name: 'getGroupMigrationMembers',
|
||||
task: () => window.Signal.Groups.getGroupMigrationMembers(this.model),
|
||||
});
|
||||
|
||||
const invitedMemberIds = pendingMembersV2.map(
|
||||
(item: GroupV2PendingMemberType) => item.conversationId
|
||||
);
|
||||
|
||||
this.migrationDialog = new Whisper.ReactWrapperView({
|
||||
className: 'group-v1-migration-wrapper',
|
||||
JSX: window.Signal.State.Roots.createGroupV1MigrationModal(
|
||||
window.reduxStore,
|
||||
{
|
||||
droppedMemberIds: droppedGV2MemberIds,
|
||||
hasMigrated: false,
|
||||
invitedMemberIds,
|
||||
migrate,
|
||||
onClose,
|
||||
}
|
||||
),
|
||||
});
|
||||
},
|
||||
|
||||
onChooseAttachment() {
|
||||
|
|
2
ts/window.d.ts
vendored
2
ts/window.d.ts
vendored
|
@ -36,6 +36,7 @@ import { createCallManager } from './state/roots/createCallManager';
|
|||
import { createCompositionArea } from './state/roots/createCompositionArea';
|
||||
import { createContactModal } from './state/roots/createContactModal';
|
||||
import { createConversationHeader } from './state/roots/createConversationHeader';
|
||||
import { createGroupV1MigrationModal } from './state/roots/createGroupV1MigrationModal';
|
||||
import { createLeftPane } from './state/roots/createLeftPane';
|
||||
import { createSafetyNumberViewer } from './state/roots/createSafetyNumberViewer';
|
||||
import { createShortcutGuideModal } from './state/roots/createShortcutGuideModal';
|
||||
|
@ -430,6 +431,7 @@ declare global {
|
|||
createCompositionArea: typeof createCompositionArea;
|
||||
createContactModal: typeof createContactModal;
|
||||
createConversationHeader: typeof createConversationHeader;
|
||||
createGroupV1MigrationModal: typeof createGroupV1MigrationModal;
|
||||
createLeftPane: typeof createLeftPane;
|
||||
createSafetyNumberViewer: typeof createSafetyNumberViewer;
|
||||
createShortcutGuideModal: typeof createShortcutGuideModal;
|
||||
|
|
Loading…
Reference in a new issue