Show toast when reacting/replying to a Story
This commit is contained in:
parent
fc98d54326
commit
9ce4b8977d
14 changed files with 228 additions and 9 deletions
|
@ -23,7 +23,7 @@
|
|||
};
|
||||
window.SignalContext = {
|
||||
activeWindowService: {
|
||||
isActive: () => true;
|
||||
isActive: () => true,
|
||||
registerForActive: noop,
|
||||
unregisterForActive: noop,
|
||||
registerForChange: noop,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
|
|
|
@ -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()}
|
||||
|
|
|
@ -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}
|
||||
|
|
52
ts/components/ToastManager.stories.tsx
Normal file
52
ts/components/ToastManager.stories.tsx
Normal 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,
|
||||
};
|
49
ts/components/ToastManager.tsx
Normal file
49
ts/components/ToastManager.tsx
Normal 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;
|
||||
};
|
|
@ -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
85
ts/state/ducks/toast.ts
Normal 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;
|
||||
}
|
|
@ -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(),
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -89,6 +89,7 @@ const mapStateToProps = (state: StateType) => {
|
|||
titleBarDoubleClick: (): void => {
|
||||
window.titleBarDoubleClick();
|
||||
},
|
||||
toastType: state.toast.toastType,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
Loading…
Add table
Reference in a new issue