New feature flag with ability to migrate GV1 groups

This commit is contained in:
Scott Nonnenberg 2020-12-01 08:42:35 -08:00 committed by GitHub
parent 089a6fb5a2
commit 2b8ae412e0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 608 additions and 189 deletions

View file

@ -69,6 +69,8 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
overrideProps.messageRequestsEnabled || false
),
title: '',
// GroupV1 Disabled Actions
onStartGroupMigration: action('onStartGroupMigration'),
});
story.add('Default', () => {

View file

@ -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">

View file

@ -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

View file

@ -43,7 +43,6 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
),
i18n,
invitedMembers: overrideProps.invitedMembers || [contact2],
learnMore: action('learnMore'),
migrate: action('migrate'),
onClose: action('onClose'),
});

View file

@ -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"

View file

@ -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()} />;
});

View 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>
);
};

View file

@ -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()')
}

View file

@ -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),

View file

@ -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}