Functional context menu in story viewer

This commit is contained in:
Josh Perez 2022-04-29 13:43:24 -04:00 committed by GitHub
parent d6b58d23d6
commit 6e7092c294
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 98 additions and 20 deletions

View File

@ -95,12 +95,12 @@ export const Stories = ({
<StoriesPane
hiddenStories={hiddenStories}
i18n={i18n}
onBack={toggleStoriesView}
onStoryClicked={setConversationIdToView}
openConversationInternal={openConversationInternal}
queueStoryDownload={queueStoryDownload}
stories={stories}
toggleHideStories={toggleHideStories}
toggleStoriesView={toggleStoriesView}
/>
</div>
</FocusTrap>

View File

@ -52,23 +52,23 @@ function getNewestStory(story: ConversationStoryType): StoryViewType {
export type PropsType = {
hiddenStories: Array<ConversationStoryType>;
i18n: LocalizerType;
onBack: () => unknown;
onStoryClicked: (conversationId: string) => unknown;
openConversationInternal: (_: { conversationId: string }) => unknown;
queueStoryDownload: (storyId: string) => unknown;
stories: Array<ConversationStoryType>;
toggleHideStories: (conversationId: string) => unknown;
toggleStoriesView: () => unknown;
};
export const StoriesPane = ({
hiddenStories,
i18n,
onBack,
onStoryClicked,
openConversationInternal,
queueStoryDownload,
stories,
toggleHideStories,
toggleStoriesView,
}: PropsType): JSX.Element => {
const [searchTerm, setSearchTerm] = useState('');
const [isShowingHiddenStories, setIsShowingHiddenStories] = useState(false);
@ -89,7 +89,7 @@ export const StoriesPane = ({
<button
aria-label={i18n('back')}
className="Stories__pane__header--back"
onClick={onBack}
onClick={toggleStoriesView}
tabIndex={0}
type="button"
/>
@ -119,11 +119,10 @@ export const StoriesPane = ({
onClick={() => {
onStoryClicked(story.conversationId);
}}
onHideStory={() => {
toggleHideStories(getNewestStory(story).sender.id);
}}
onHideStory={toggleHideStories}
onGoToConversation={conversationId => {
openConversationInternal({ conversationId });
toggleStoriesView();
}}
queueStoryDownload={queueStoryDownload}
story={getNewestStory(story)}
@ -149,11 +148,10 @@ export const StoriesPane = ({
onClick={() => {
onStoryClicked(story.conversationId);
}}
onHideStory={() => {
toggleHideStories(getNewestStory(story).sender.id);
}}
onHideStory={toggleHideStories}
onGoToConversation={conversationId => {
openConversationInternal({ conversationId });
toggleStoriesView();
}}
queueStoryDownload={queueStoryDownload}
story={getNewestStory(story)}

View File

@ -23,6 +23,8 @@ function getDefaultProps(): PropsType {
return {
i18n,
onClick: action('onClick'),
onGoToConversation: action('onGoToConversation'),
onHideStory: action('onHideStory'),
queueStoryDownload: action('queueStoryDownload'),
story: {
messageId: '123',

View File

@ -62,8 +62,8 @@ export type PropsType = Pick<
> & {
i18n: LocalizerType;
onClick: () => unknown;
onGoToConversation?: (conversationId: string) => unknown;
onHideStory?: (conversationId: string) => unknown;
onGoToConversation: (conversationId: string) => unknown;
onHideStory: (conversationId: string) => unknown;
queueStoryDownload: (storyId: string) => unknown;
story: StoryViewType;
};
@ -217,7 +217,7 @@ export const StoryListItem = ({
: i18n('StoryListItem__hide'),
onClick: () => {
if (isHidden) {
onHideStory?.(sender.id);
onHideStory(sender.id);
} else {
setHasConfirmHideStory(true);
}
@ -227,7 +227,7 @@ export const StoryListItem = ({
icon: 'StoryListItem__icon--chat',
label: i18n('StoryListItem__go-to-chat'),
onClick: () => {
onGoToConversation?.(sender.id);
onGoToConversation(sender.id);
},
},
]}
@ -242,7 +242,7 @@ export const StoryListItem = ({
<ConfirmationDialog
actions={[
{
action: () => onHideStory?.(sender.id),
action: () => onHideStory(sender.id),
style: 'affirmative',
text: i18n('StoryListItem__hide-modal--confirm'),
},

View File

@ -27,6 +27,8 @@ function getDefaultProps(): PropsType {
loadStoryReplies: action('loadStoryReplies'),
markStoryRead: action('markStoryRead'),
onClose: action('onClose'),
onGoToConversation: action('onGoToConversation'),
onHideStory: action('onHideStory'),
onNextUserStories: action('onNextUserStories'),
onPrevUserStories: action('onPrevUserStories'),
onReactToStory: action('onReactToStory'),

View File

@ -12,6 +12,8 @@ import type { RenderEmojiPickerProps } from './conversation/ReactionPicker';
import type { ReplyStateType } from '../types/Stories';
import type { StoryViewType } from './StoryListItem';
import { Avatar, AvatarSize } from './Avatar';
import { ConfirmationDialog } from './ConfirmationDialog';
import { ContextMenuPopper } from './ContextMenu';
import { Intl } from './Intl';
import { MessageTimestamp } from './conversation/MessageTimestamp';
import { StoryImage } from './StoryImage';
@ -41,6 +43,8 @@ export type PropsType = {
loadStoryReplies: (conversationId: string, messageId: string) => unknown;
markStoryRead: (mId: string) => unknown;
onClose: () => unknown;
onGoToConversation: (conversationId: string) => unknown;
onHideStory: (conversationId: string) => unknown;
onNextUserStories: () => unknown;
onPrevUserStories: () => unknown;
onSetSkinTone: (tone: number) => unknown;
@ -76,6 +80,8 @@ export const StoryViewer = ({
loadStoryReplies,
markStoryRead,
onClose,
onGoToConversation,
onHideStory,
onNextUserStories,
onPrevUserStories,
onReactToStory,
@ -96,15 +102,21 @@ export const StoryViewer = ({
const [currentStoryIndex, setCurrentStoryIndex] =
useState(selectedStoryIndex);
const [storyDuration, setStoryDuration] = useState<number | undefined>();
const [isShowingContextMenu, setIsShowingContextMenu] = useState(false);
const [hasConfirmHideStory, setHasConfirmHideStory] = useState(false);
const [referenceElement, setReferenceElement] =
useState<HTMLButtonElement | null>(null);
const visibleStory = stories[currentStoryIndex];
const { attachment, canReply, messageId, timestamp } = visibleStory;
const { attachment, canReply, isHidden, messageId, timestamp } = visibleStory;
const {
acceptedMessageRequest,
avatarPath,
color,
isMe,
id,
firstName,
name,
profileName,
sharedGroupNames,
@ -226,13 +238,19 @@ export const StoryViewer = ({
};
}, [currentStoryIndex, spring, storyDuration]);
const shouldPauseViewing =
hasConfirmHideStory ||
hasExpandedCaption ||
hasReplyModal ||
isShowingContextMenu;
useEffect(() => {
if (hasReplyModal || hasExpandedCaption) {
if (shouldPauseViewing) {
spring.pause();
} else {
spring.resume();
}
}, [hasExpandedCaption, hasReplyModal, spring]);
}, [shouldPauseViewing, spring]);
useEffect(() => {
markStoryRead(messageId);
@ -248,7 +266,7 @@ export const StoryViewer = ({
.map(story => story.messageId);
}, [stories]);
useEffect(() => {
storiesToDownload.forEach(id => queueStoryDownload(id));
storiesToDownload.forEach(storyId => queueStoryDownload(storyId));
}, [queueStoryDownload, storiesToDownload]);
const navigateStories = useCallback(
@ -452,6 +470,8 @@ export const StoryViewer = ({
<button
aria-label={i18n('MyStories__more')}
className="StoryViewer__more"
onClick={() => setIsShowingContextMenu(true)}
ref={setReferenceElement}
tabIndex={0}
type="button"
/>
@ -463,6 +483,37 @@ export const StoryViewer = ({
type="button"
/>
</div>
<ContextMenuPopper
isMenuShowing={isShowingContextMenu}
menuOptions={[
{
icon: 'StoryListItem__icon--hide',
label: isHidden
? i18n('StoryListItem__unhide')
: i18n('StoryListItem__hide'),
onClick: () => {
if (isHidden) {
onHideStory(id);
} else {
setHasConfirmHideStory(true);
}
},
},
{
icon: 'StoryListItem__icon--chat',
label: i18n('StoryListItem__go-to-chat'),
onClick: () => {
onGoToConversation(id);
},
},
]}
onClose={() => setIsShowingContextMenu(false)}
popperOptions={{
placement: 'bottom',
strategy: 'absolute',
}}
referenceElement={referenceElement}
/>
{hasReplyModal && canReply && (
<StoryViewsNRepliesModal
authorTitle={title}
@ -492,6 +543,23 @@ export const StoryViewer = ({
views={[]}
/>
)}
{hasConfirmHideStory && (
<ConfirmationDialog
actions={[
{
action: () => onHideStory(id),
style: 'affirmative',
text: i18n('StoryListItem__hide-modal--confirm'),
},
]}
i18n={i18n}
onClose={() => {
setHasConfirmHideStory(false);
}}
>
{i18n('StoryListItem__hide-modal--body', [String(firstName)])}
</ConfirmationDialog>
)}
</div>
</FocusTrap>
);

View File

@ -13,13 +13,14 @@ import {
getEmojiSkinTone,
getPreferredReactionEmoji,
} from '../selectors/items';
import { getStoriesSelector, getStoryReplies } from '../selectors/stories';
import { getIntl } from '../selectors/user';
import { getPreferredBadgeSelector } from '../selectors/badges';
import { getStoriesSelector, getStoryReplies } from '../selectors/stories';
import { renderEmojiPicker } from './renderEmojiPicker';
import { showToast } from '../../util/showToast';
import { useActions as useEmojisActions } from '../ducks/emojis';
import { useActions as useItemsActions } from '../ducks/items';
import { useConversationsActions } from '../ducks/conversations';
import { useRecentEmojis } from '../selectors/emojis';
import { useStoriesActions } from '../ducks/stories';
@ -39,6 +40,8 @@ export function SmartStoryViewer({
const storiesActions = useStoriesActions();
const { onSetSkinTone } = useItemsActions();
const { onUseEmoji } = useEmojisActions();
const { openConversationInternal, toggleHideStories } =
useConversationsActions();
const i18n = useSelector<StateType, LocalizerType>(getIntl);
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
@ -66,6 +69,11 @@ export function SmartStoryViewer({
group={group}
i18n={i18n}
onClose={onClose}
onHideStory={toggleHideStories}
onGoToConversation={senderId => {
openConversationInternal({ conversationId: senderId });
storiesActions.toggleStoriesView();
}}
onNextUserStories={onNextUserStories}
onPrevUserStories={onPrevUserStories}
onReactToStory={async (emoji, story) => {