Show toast when reacting/replying to a Story

This commit is contained in:
Josh Perez 2022-07-12 12:41:41 -04:00 committed by GitHub
parent fc98d54326
commit 9ce4b8977d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 228 additions and 9 deletions

View File

@ -23,7 +23,7 @@
};
window.SignalContext = {
activeWindowService: {
isActive: () => true;
isActive: () => true,
registerForActive: noop,
unregisterForActive: noop,
registerForChange: noop,

View File

@ -7161,6 +7161,14 @@
}
}
},
"Stories__toast--sending-reply": {
"message": "Sending reply...",
"description": "Toast message"
},
"Stories__toast--sending-reaction": {
"message": "Sending reaction...",
"description": "Toast message"
},
"StoryViewer__pause": {
"message": "Pause",
"description": "Aria label for pausing a story"

View File

@ -1108,6 +1108,7 @@ export async function startApp(): Promise<void> {
actionCreators.storyDistributionLists,
store.dispatch
),
toast: bindActionCreators(actionCreators.toast, store.dispatch),
updates: bindActionCreators(actionCreators.updates, store.dispatch),
user: bindActionCreators(actionCreators.user, store.dispatch),
};

View File

@ -6,18 +6,20 @@ import React, { useEffect } from 'react';
import { Globals } from '@react-spring/web';
import classNames from 'classnames';
import type { ExecuteMenuRoleType } from './TitleBarContainer';
import type { LocaleMessagesType } from '../types/I18N';
import type { MenuOptionsType, MenuActionType } from '../types/menu';
import type { SelectedStoryDataType } from '../state/ducks/stories';
import type { ToastType } from '../state/ducks/toast';
import { AppViewType } from '../state/ducks/app';
import { Inbox } from './Inbox';
import { SmartInstallScreen } from '../state/smart/InstallScreen';
import { StandaloneRegistration } from './StandaloneRegistration';
import { ThemeType } from '../types/Util';
import type { LocaleMessagesType } from '../types/I18N';
import { TitleBarContainer } from './TitleBarContainer';
import { ToastManager } from './ToastManager';
import { usePageVisibility } from '../hooks/usePageVisibility';
import { useReducedMotion } from '../hooks/useReducedMotion';
import type { MenuOptionsType, MenuActionType } from '../types/menu';
import { TitleBarContainer } from './TitleBarContainer';
import type { ExecuteMenuRoleType } from './TitleBarContainer';
import type { SelectedStoryDataType } from '../state/ducks/stories';
type PropsType = {
appView: AppViewType;
@ -45,6 +47,8 @@ type PropsType = {
executeMenuRole: ExecuteMenuRoleType;
executeMenuAction: (action: MenuActionType) => void;
titleBarDoubleClick: () => void;
toastType?: ToastType;
hideToast: () => unknown;
} & ComponentProps<typeof Inbox>;
export const App = ({
@ -56,6 +60,7 @@ export const App = ({
getPreferredBadge,
hasInitialLoadCompleted,
hideMenuBar,
hideToast,
i18n,
isCustomizingPreferredReactions,
isFullScreen,
@ -81,6 +86,7 @@ export const App = ({
showWhatsNewModal,
theme,
titleBarDoubleClick,
toastType,
verifyConversationsStoppingSend,
}: PropsType): JSX.Element => {
let contents;
@ -171,6 +177,7 @@ export const App = ({
'dark-theme': theme === ThemeType.dark,
})}
>
<ToastManager hideToast={hideToast} i18n={i18n} toastType={toastType} />
{renderGlobalModalContainer()}
{renderCallManager()}
{isShowingStoriesView && renderStories()}

View File

@ -11,6 +11,7 @@ import type { EmojiPickDataType } from './emoji/EmojiPicker';
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
import type { RenderEmojiPickerProps } from './conversation/ReactionPicker';
import type { ReplyStateType, StoryViewType } from '../types/Stories';
import type { ShowToastActionCreatorType } from '../state/ducks/toast';
import type { ViewStoryActionCreatorType } from '../state/ducks/stories';
import * as log from '../logging/log';
import { AnimatedEmojiGalore } from './AnimatedEmojiGalore';
@ -24,6 +25,7 @@ import { StoryImage } from './StoryImage';
import { StoryViewDirectionType, StoryViewModeType } from '../types/Stories';
import { StoryViewsNRepliesModal } from './StoryViewsNRepliesModal';
import { Theme } from '../util/theme';
import { ToastType } from '../state/ducks/toast';
import { getAvatarColor } from '../types/Colors';
import { getStoryBackground } from '../util/getStoryBackground';
import { getStoryDuration } from '../util/getStoryDuration';
@ -66,6 +68,7 @@ export type PropsType = {
recentEmojis?: Array<string>;
renderEmojiPicker: (props: RenderEmojiPickerProps) => JSX.Element;
replyState?: ReplyStateType;
showToast: ShowToastActionCreatorType;
skinTone?: number;
story: StoryViewType;
storyViewMode?: StoryViewModeType;
@ -105,6 +108,7 @@ export const StoryViewer = ({
recentEmojis,
renderEmojiPicker,
replyState,
showToast,
skinTone,
story,
storyViewMode,
@ -650,12 +654,14 @@ export const StoryViewer = ({
onReactToStory(emoji, story);
setHasReplyModal(false);
setReactionEmoji(emoji);
showToast(ToastType.StoryReact);
}}
onReply={(message, mentions, replyTimestamp) => {
if (!isGroupStory) {
setHasReplyModal(false);
}
onReplyToStory(message, mentions, replyTimestamp, story);
showToast(ToastType.StoryReply);
}}
onSetSkinTone={onSetSkinTone}
onTextTooLong={onTextTooLong}

View File

@ -0,0 +1,52 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { Meta, Story } from '@storybook/react';
import React from 'react';
import type { PropsType } from './ToastManager';
import enMessages from '../../_locales/en/messages.json';
import { ToastManager } from './ToastManager';
import { ToastType } from '../state/ducks/toast';
import { setupI18n } from '../util/setupI18n';
const i18n = setupI18n('en', enMessages);
export default {
title: 'Components/ToastManager',
component: ToastManager,
argTypes: {
hideToast: { action: true },
i18n: {
defaultValue: i18n,
},
toastType: {
defaultValue: undefined,
},
},
} as Meta;
const Template: Story<PropsType> = args => <ToastManager {...args} />;
export const UndefinedToast = Template.bind({});
UndefinedToast.args = {};
export const InvalidToast = Template.bind({});
InvalidToast.args = {
toastType: 'this is a toast that does not exist' as ToastType,
};
export const StoryReact = Template.bind({});
StoryReact.args = {
toastType: ToastType.StoryReact,
};
export const StoryReply = Template.bind({});
StoryReply.args = {
toastType: ToastType.StoryReply,
};
export const MessageBodyTooLong = Template.bind({});
MessageBodyTooLong.args = {
toastType: ToastType.MessageBodyTooLong,
};

View File

@ -0,0 +1,49 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import type { LocalizerType } from '../types/Util';
import { SECOND } from '../util/durations';
import { Toast } from './Toast';
import { ToastMessageBodyTooLong } from './ToastMessageBodyTooLong';
import { ToastType } from '../state/ducks/toast';
import { strictAssert } from '../util/assert';
export type PropsType = {
hideToast: () => unknown;
i18n: LocalizerType;
toastType?: ToastType;
};
export const ToastManager = ({
hideToast,
i18n,
toastType,
}: PropsType): JSX.Element | null => {
if (toastType === ToastType.MessageBodyTooLong) {
return <ToastMessageBodyTooLong i18n={i18n} onClose={hideToast} />;
}
if (toastType === ToastType.StoryReact) {
return (
<Toast onClose={hideToast} timeout={3 * SECOND}>
{i18n('Stories__toast--sending-reaction')}
</Toast>
);
}
if (toastType === ToastType.StoryReply) {
return (
<Toast onClose={hideToast} timeout={3 * SECOND}>
{i18n('Stories__toast--sending-reply')}
</Toast>
);
}
strictAssert(
toastType === undefined,
`Unhandled toast of type: ${toastType}`
);
return null;
};

View File

@ -21,6 +21,7 @@ import { actions as search } from './ducks/search';
import { actions as stickers } from './ducks/stickers';
import { actions as stories } from './ducks/stories';
import { actions as storyDistributionLists } from './ducks/storyDistributionLists';
import { actions as toast } from './ducks/toast';
import { actions as updates } from './ducks/updates';
import { actions as user } from './ducks/user';
import type { ReduxActions } from './types';
@ -46,6 +47,7 @@ export const actionCreators: ReduxActions = {
stickers,
stories,
storyDistributionLists,
toast,
updates,
user,
};
@ -71,6 +73,7 @@ export const mapDispatchToProps = {
...stickers,
...stories,
...storyDistributionLists,
...toast,
...updates,
...user,
};

85
ts/state/ducks/toast.ts Normal file
View File

@ -0,0 +1,85 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { useBoundActions } from '../../hooks/useBoundActions';
export enum ToastType {
MessageBodyTooLong = 'MessageBodyTooLong',
StoryReact = 'StoryReact',
StoryReply = 'StoryReply',
}
// State
export type ToastStateType = {
toastType?: ToastType;
};
// Actions
const HIDE_TOAST = 'toast/HIDE_TOAST';
const SHOW_TOAST = 'toast/SHOW_TOAST';
type HideToastActionType = {
type: typeof HIDE_TOAST;
};
type ShowToastActionType = {
type: typeof SHOW_TOAST;
payload: ToastType;
};
export type ToastActionType = HideToastActionType | ShowToastActionType;
// Action Creators
function hideToast(): HideToastActionType {
return {
type: HIDE_TOAST,
};
}
export type ShowToastActionCreatorType = (
toastType: ToastType
) => ShowToastActionType;
const showToast: ShowToastActionCreatorType = toastType => {
return {
type: SHOW_TOAST,
payload: toastType,
};
};
export const actions = {
hideToast,
showToast,
};
export const useToastActions = (): typeof actions => useBoundActions(actions);
// Reducer
export function getEmptyState(): ToastStateType {
return {};
}
export function reducer(
state: Readonly<ToastStateType> = getEmptyState(),
action: Readonly<ToastActionType>
): ToastStateType {
if (action.type === HIDE_TOAST) {
return {
...state,
toastType: undefined,
};
}
if (action.type === SHOW_TOAST) {
return {
...state,
toastType: action.payload,
};
}
return state;
}

View File

@ -18,6 +18,7 @@ import { getEmptyState as safetyNumber } from './ducks/safetyNumber';
import { getEmptyState as search } from './ducks/search';
import { getEmptyState as getStoriesEmptyState } from './ducks/stories';
import { getEmptyState as getStoryDistributionListsEmptyState } from './ducks/storyDistributionLists';
import { getEmptyState as getToastEmptyState } from './ducks/toast';
import { getEmptyState as updates } from './ducks/updates';
import { getEmptyState as user } from './ducks/user';
@ -115,6 +116,7 @@ export function getInitialState({
...getStoryDistributionListsEmptyState(),
distributionLists: storyDistributionLists || [],
},
toast: getToastEmptyState(),
updates: updates(),
user: {
...user(),

View File

@ -24,6 +24,7 @@ import { reducer as search } from './ducks/search';
import { reducer as stickers } from './ducks/stickers';
import { reducer as stories } from './ducks/stories';
import { reducer as storyDistributionLists } from './ducks/storyDistributionLists';
import { reducer as toast } from './ducks/toast';
import { reducer as updates } from './ducks/updates';
import { reducer as user } from './ducks/user';
@ -49,6 +50,7 @@ export const reducer = combineReducers({
stickers,
stories,
storyDistributionLists,
toast,
updates,
user,
});

View File

@ -89,6 +89,7 @@ const mapStateToProps = (state: StateType) => {
titleBarDoubleClick: (): void => {
window.titleBarDoubleClick();
},
toastType: state.toast.toastType,
};
};

View File

@ -10,7 +10,7 @@ import type { StoryViewModeType } from '../../types/Stories';
import type { StateType } from '../reducer';
import type { SelectedStoryDataType } from '../ducks/stories';
import { StoryViewer } from '../../components/StoryViewer';
import { ToastMessageBodyTooLong } from '../../components/ToastMessageBodyTooLong';
import { ToastType, useToastActions } from '../ducks/toast';
import { getConversationSelector } from '../selectors/conversations';
import {
getEmojiSkinTone,
@ -26,7 +26,6 @@ import {
getStoryView,
} from '../selectors/stories';
import { renderEmojiPicker } from './renderEmojiPicker';
import { showToast } from '../../util/showToast';
import { strictAssert } from '../../util/assert';
import { useActions as useEmojisActions } from '../ducks/emojis';
import { useActions as useItemsActions } from '../ducks/items';
@ -39,6 +38,7 @@ export function SmartStoryViewer(): JSX.Element | null {
const { onSetSkinTone, toggleHasAllStoriesMuted } = useItemsActions();
const { onUseEmoji } = useEmojisActions();
const { showConversation, toggleHideStories } = useConversationsActions();
const { showToast } = useToastActions();
const i18n = useSelector<StateType, LocalizerType>(getIntl);
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
@ -100,12 +100,13 @@ export function SmartStoryViewer(): JSX.Element | null {
);
}}
onSetSkinTone={onSetSkinTone}
onTextTooLong={() => showToast(ToastMessageBodyTooLong)}
onTextTooLong={() => showToast(ToastType.MessageBodyTooLong)}
onUseEmoji={onUseEmoji}
preferredReactionEmoji={preferredReactionEmoji}
recentEmojis={recentEmojis}
renderEmojiPicker={renderEmojiPicker}
replyState={replyState}
showToast={showToast}
skinTone={skinTone}
story={storyView}
storyViewMode={storyViewMode}

View File

@ -21,6 +21,7 @@ import type { actions as search } from './ducks/search';
import type { actions as stickers } from './ducks/stickers';
import type { actions as stories } from './ducks/stories';
import type { actions as storyDistributionLists } from './ducks/storyDistributionLists';
import type { actions as toast } from './ducks/toast';
import type { actions as updates } from './ducks/updates';
import type { actions as user } from './ducks/user';
@ -45,6 +46,7 @@ export type ReduxActions = {
stickers: typeof stickers;
stories: typeof stories;
storyDistributionLists: typeof storyDistributionLists;
toast: typeof toast;
updates: typeof updates;
user: typeof user;
};