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 = { window.SignalContext = {
activeWindowService: { activeWindowService: {
isActive: () => true; isActive: () => true,
registerForActive: noop, registerForActive: noop,
unregisterForActive: noop, unregisterForActive: noop,
registerForChange: 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": { "StoryViewer__pause": {
"message": "Pause", "message": "Pause",
"description": "Aria label for pausing a story" "description": "Aria label for pausing a story"

View file

@ -1108,6 +1108,7 @@ export async function startApp(): Promise<void> {
actionCreators.storyDistributionLists, actionCreators.storyDistributionLists,
store.dispatch store.dispatch
), ),
toast: bindActionCreators(actionCreators.toast, store.dispatch),
updates: bindActionCreators(actionCreators.updates, store.dispatch), updates: bindActionCreators(actionCreators.updates, store.dispatch),
user: bindActionCreators(actionCreators.user, 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 { Globals } from '@react-spring/web';
import classNames from 'classnames'; 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 { AppViewType } from '../state/ducks/app';
import { Inbox } from './Inbox'; import { Inbox } from './Inbox';
import { SmartInstallScreen } from '../state/smart/InstallScreen'; import { SmartInstallScreen } from '../state/smart/InstallScreen';
import { StandaloneRegistration } from './StandaloneRegistration'; import { StandaloneRegistration } from './StandaloneRegistration';
import { ThemeType } from '../types/Util'; 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 { usePageVisibility } from '../hooks/usePageVisibility';
import { useReducedMotion } from '../hooks/useReducedMotion'; 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 = { type PropsType = {
appView: AppViewType; appView: AppViewType;
@ -45,6 +47,8 @@ type PropsType = {
executeMenuRole: ExecuteMenuRoleType; executeMenuRole: ExecuteMenuRoleType;
executeMenuAction: (action: MenuActionType) => void; executeMenuAction: (action: MenuActionType) => void;
titleBarDoubleClick: () => void; titleBarDoubleClick: () => void;
toastType?: ToastType;
hideToast: () => unknown;
} & ComponentProps<typeof Inbox>; } & ComponentProps<typeof Inbox>;
export const App = ({ export const App = ({
@ -56,6 +60,7 @@ export const App = ({
getPreferredBadge, getPreferredBadge,
hasInitialLoadCompleted, hasInitialLoadCompleted,
hideMenuBar, hideMenuBar,
hideToast,
i18n, i18n,
isCustomizingPreferredReactions, isCustomizingPreferredReactions,
isFullScreen, isFullScreen,
@ -81,6 +86,7 @@ export const App = ({
showWhatsNewModal, showWhatsNewModal,
theme, theme,
titleBarDoubleClick, titleBarDoubleClick,
toastType,
verifyConversationsStoppingSend, verifyConversationsStoppingSend,
}: PropsType): JSX.Element => { }: PropsType): JSX.Element => {
let contents; let contents;
@ -171,6 +177,7 @@ export const App = ({
'dark-theme': theme === ThemeType.dark, 'dark-theme': theme === ThemeType.dark,
})} })}
> >
<ToastManager hideToast={hideToast} i18n={i18n} toastType={toastType} />
{renderGlobalModalContainer()} {renderGlobalModalContainer()}
{renderCallManager()} {renderCallManager()}
{isShowingStoriesView && renderStories()} {isShowingStoriesView && renderStories()}

View file

@ -11,6 +11,7 @@ import type { EmojiPickDataType } from './emoji/EmojiPicker';
import type { PreferredBadgeSelectorType } from '../state/selectors/badges'; import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
import type { RenderEmojiPickerProps } from './conversation/ReactionPicker'; import type { RenderEmojiPickerProps } from './conversation/ReactionPicker';
import type { ReplyStateType, StoryViewType } from '../types/Stories'; import type { ReplyStateType, StoryViewType } from '../types/Stories';
import type { ShowToastActionCreatorType } from '../state/ducks/toast';
import type { ViewStoryActionCreatorType } from '../state/ducks/stories'; import type { ViewStoryActionCreatorType } from '../state/ducks/stories';
import * as log from '../logging/log'; import * as log from '../logging/log';
import { AnimatedEmojiGalore } from './AnimatedEmojiGalore'; import { AnimatedEmojiGalore } from './AnimatedEmojiGalore';
@ -24,6 +25,7 @@ import { StoryImage } from './StoryImage';
import { StoryViewDirectionType, StoryViewModeType } from '../types/Stories'; import { StoryViewDirectionType, StoryViewModeType } from '../types/Stories';
import { StoryViewsNRepliesModal } from './StoryViewsNRepliesModal'; import { StoryViewsNRepliesModal } from './StoryViewsNRepliesModal';
import { Theme } from '../util/theme'; import { Theme } from '../util/theme';
import { ToastType } from '../state/ducks/toast';
import { getAvatarColor } from '../types/Colors'; import { getAvatarColor } from '../types/Colors';
import { getStoryBackground } from '../util/getStoryBackground'; import { getStoryBackground } from '../util/getStoryBackground';
import { getStoryDuration } from '../util/getStoryDuration'; import { getStoryDuration } from '../util/getStoryDuration';
@ -66,6 +68,7 @@ export type PropsType = {
recentEmojis?: Array<string>; recentEmojis?: Array<string>;
renderEmojiPicker: (props: RenderEmojiPickerProps) => JSX.Element; renderEmojiPicker: (props: RenderEmojiPickerProps) => JSX.Element;
replyState?: ReplyStateType; replyState?: ReplyStateType;
showToast: ShowToastActionCreatorType;
skinTone?: number; skinTone?: number;
story: StoryViewType; story: StoryViewType;
storyViewMode?: StoryViewModeType; storyViewMode?: StoryViewModeType;
@ -105,6 +108,7 @@ export const StoryViewer = ({
recentEmojis, recentEmojis,
renderEmojiPicker, renderEmojiPicker,
replyState, replyState,
showToast,
skinTone, skinTone,
story, story,
storyViewMode, storyViewMode,
@ -650,12 +654,14 @@ export const StoryViewer = ({
onReactToStory(emoji, story); onReactToStory(emoji, story);
setHasReplyModal(false); setHasReplyModal(false);
setReactionEmoji(emoji); setReactionEmoji(emoji);
showToast(ToastType.StoryReact);
}} }}
onReply={(message, mentions, replyTimestamp) => { onReply={(message, mentions, replyTimestamp) => {
if (!isGroupStory) { if (!isGroupStory) {
setHasReplyModal(false); setHasReplyModal(false);
} }
onReplyToStory(message, mentions, replyTimestamp, story); onReplyToStory(message, mentions, replyTimestamp, story);
showToast(ToastType.StoryReply);
}} }}
onSetSkinTone={onSetSkinTone} onSetSkinTone={onSetSkinTone}
onTextTooLong={onTextTooLong} 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 stickers } from './ducks/stickers';
import { actions as stories } from './ducks/stories'; import { actions as stories } from './ducks/stories';
import { actions as storyDistributionLists } from './ducks/storyDistributionLists'; import { actions as storyDistributionLists } from './ducks/storyDistributionLists';
import { actions as toast } from './ducks/toast';
import { actions as updates } from './ducks/updates'; import { actions as updates } from './ducks/updates';
import { actions as user } from './ducks/user'; import { actions as user } from './ducks/user';
import type { ReduxActions } from './types'; import type { ReduxActions } from './types';
@ -46,6 +47,7 @@ export const actionCreators: ReduxActions = {
stickers, stickers,
stories, stories,
storyDistributionLists, storyDistributionLists,
toast,
updates, updates,
user, user,
}; };
@ -71,6 +73,7 @@ export const mapDispatchToProps = {
...stickers, ...stickers,
...stories, ...stories,
...storyDistributionLists, ...storyDistributionLists,
...toast,
...updates, ...updates,
...user, ...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 search } from './ducks/search';
import { getEmptyState as getStoriesEmptyState } from './ducks/stories'; import { getEmptyState as getStoriesEmptyState } from './ducks/stories';
import { getEmptyState as getStoryDistributionListsEmptyState } from './ducks/storyDistributionLists'; import { getEmptyState as getStoryDistributionListsEmptyState } from './ducks/storyDistributionLists';
import { getEmptyState as getToastEmptyState } from './ducks/toast';
import { getEmptyState as updates } from './ducks/updates'; import { getEmptyState as updates } from './ducks/updates';
import { getEmptyState as user } from './ducks/user'; import { getEmptyState as user } from './ducks/user';
@ -115,6 +116,7 @@ export function getInitialState({
...getStoryDistributionListsEmptyState(), ...getStoryDistributionListsEmptyState(),
distributionLists: storyDistributionLists || [], distributionLists: storyDistributionLists || [],
}, },
toast: getToastEmptyState(),
updates: updates(), updates: updates(),
user: { user: {
...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 stickers } from './ducks/stickers';
import { reducer as stories } from './ducks/stories'; import { reducer as stories } from './ducks/stories';
import { reducer as storyDistributionLists } from './ducks/storyDistributionLists'; import { reducer as storyDistributionLists } from './ducks/storyDistributionLists';
import { reducer as toast } from './ducks/toast';
import { reducer as updates } from './ducks/updates'; import { reducer as updates } from './ducks/updates';
import { reducer as user } from './ducks/user'; import { reducer as user } from './ducks/user';
@ -49,6 +50,7 @@ export const reducer = combineReducers({
stickers, stickers,
stories, stories,
storyDistributionLists, storyDistributionLists,
toast,
updates, updates,
user, user,
}); });

View file

@ -89,6 +89,7 @@ const mapStateToProps = (state: StateType) => {
titleBarDoubleClick: (): void => { titleBarDoubleClick: (): void => {
window.titleBarDoubleClick(); 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 { StateType } from '../reducer';
import type { SelectedStoryDataType } from '../ducks/stories'; import type { SelectedStoryDataType } from '../ducks/stories';
import { StoryViewer } from '../../components/StoryViewer'; import { StoryViewer } from '../../components/StoryViewer';
import { ToastMessageBodyTooLong } from '../../components/ToastMessageBodyTooLong'; import { ToastType, useToastActions } from '../ducks/toast';
import { getConversationSelector } from '../selectors/conversations'; import { getConversationSelector } from '../selectors/conversations';
import { import {
getEmojiSkinTone, getEmojiSkinTone,
@ -26,7 +26,6 @@ import {
getStoryView, getStoryView,
} from '../selectors/stories'; } from '../selectors/stories';
import { renderEmojiPicker } from './renderEmojiPicker'; import { renderEmojiPicker } from './renderEmojiPicker';
import { showToast } from '../../util/showToast';
import { strictAssert } from '../../util/assert'; import { strictAssert } from '../../util/assert';
import { useActions as useEmojisActions } from '../ducks/emojis'; import { useActions as useEmojisActions } from '../ducks/emojis';
import { useActions as useItemsActions } from '../ducks/items'; import { useActions as useItemsActions } from '../ducks/items';
@ -39,6 +38,7 @@ export function SmartStoryViewer(): JSX.Element | null {
const { onSetSkinTone, toggleHasAllStoriesMuted } = useItemsActions(); const { onSetSkinTone, toggleHasAllStoriesMuted } = useItemsActions();
const { onUseEmoji } = useEmojisActions(); const { onUseEmoji } = useEmojisActions();
const { showConversation, toggleHideStories } = useConversationsActions(); const { showConversation, toggleHideStories } = useConversationsActions();
const { showToast } = useToastActions();
const i18n = useSelector<StateType, LocalizerType>(getIntl); const i18n = useSelector<StateType, LocalizerType>(getIntl);
const getPreferredBadge = useSelector(getPreferredBadgeSelector); const getPreferredBadge = useSelector(getPreferredBadgeSelector);
@ -100,12 +100,13 @@ export function SmartStoryViewer(): JSX.Element | null {
); );
}} }}
onSetSkinTone={onSetSkinTone} onSetSkinTone={onSetSkinTone}
onTextTooLong={() => showToast(ToastMessageBodyTooLong)} onTextTooLong={() => showToast(ToastType.MessageBodyTooLong)}
onUseEmoji={onUseEmoji} onUseEmoji={onUseEmoji}
preferredReactionEmoji={preferredReactionEmoji} preferredReactionEmoji={preferredReactionEmoji}
recentEmojis={recentEmojis} recentEmojis={recentEmojis}
renderEmojiPicker={renderEmojiPicker} renderEmojiPicker={renderEmojiPicker}
replyState={replyState} replyState={replyState}
showToast={showToast}
skinTone={skinTone} skinTone={skinTone}
story={storyView} story={storyView}
storyViewMode={storyViewMode} 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 stickers } from './ducks/stickers';
import type { actions as stories } from './ducks/stories'; import type { actions as stories } from './ducks/stories';
import type { actions as storyDistributionLists } from './ducks/storyDistributionLists'; 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 updates } from './ducks/updates';
import type { actions as user } from './ducks/user'; import type { actions as user } from './ducks/user';
@ -45,6 +46,7 @@ export type ReduxActions = {
stickers: typeof stickers; stickers: typeof stickers;
stories: typeof stories; stories: typeof stories;
storyDistributionLists: typeof storyDistributionLists; storyDistributionLists: typeof storyDistributionLists;
toast: typeof toast;
updates: typeof updates; updates: typeof updates;
user: typeof user; user: typeof user;
}; };