Functional context menu in story viewer
This commit is contained in:
parent
d6b58d23d6
commit
6e7092c294
7 changed files with 98 additions and 20 deletions
|
@ -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>
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -23,6 +23,8 @@ function getDefaultProps(): PropsType {
|
|||
return {
|
||||
i18n,
|
||||
onClick: action('onClick'),
|
||||
onGoToConversation: action('onGoToConversation'),
|
||||
onHideStory: action('onHideStory'),
|
||||
queueStoryDownload: action('queueStoryDownload'),
|
||||
story: {
|
||||
messageId: '123',
|
||||
|
|
|
@ -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'),
|
||||
},
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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) => {
|
||||
|
|
Loading…
Reference in a new issue