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
|
<StoriesPane
|
||||||
hiddenStories={hiddenStories}
|
hiddenStories={hiddenStories}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
onBack={toggleStoriesView}
|
|
||||||
onStoryClicked={setConversationIdToView}
|
onStoryClicked={setConversationIdToView}
|
||||||
openConversationInternal={openConversationInternal}
|
openConversationInternal={openConversationInternal}
|
||||||
queueStoryDownload={queueStoryDownload}
|
queueStoryDownload={queueStoryDownload}
|
||||||
stories={stories}
|
stories={stories}
|
||||||
toggleHideStories={toggleHideStories}
|
toggleHideStories={toggleHideStories}
|
||||||
|
toggleStoriesView={toggleStoriesView}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</FocusTrap>
|
</FocusTrap>
|
||||||
|
|
|
@ -52,23 +52,23 @@ function getNewestStory(story: ConversationStoryType): StoryViewType {
|
||||||
export type PropsType = {
|
export type PropsType = {
|
||||||
hiddenStories: Array<ConversationStoryType>;
|
hiddenStories: Array<ConversationStoryType>;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
onBack: () => unknown;
|
|
||||||
onStoryClicked: (conversationId: string) => unknown;
|
onStoryClicked: (conversationId: string) => unknown;
|
||||||
openConversationInternal: (_: { conversationId: string }) => unknown;
|
openConversationInternal: (_: { conversationId: string }) => unknown;
|
||||||
queueStoryDownload: (storyId: string) => unknown;
|
queueStoryDownload: (storyId: string) => unknown;
|
||||||
stories: Array<ConversationStoryType>;
|
stories: Array<ConversationStoryType>;
|
||||||
toggleHideStories: (conversationId: string) => unknown;
|
toggleHideStories: (conversationId: string) => unknown;
|
||||||
|
toggleStoriesView: () => unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const StoriesPane = ({
|
export const StoriesPane = ({
|
||||||
hiddenStories,
|
hiddenStories,
|
||||||
i18n,
|
i18n,
|
||||||
onBack,
|
|
||||||
onStoryClicked,
|
onStoryClicked,
|
||||||
openConversationInternal,
|
openConversationInternal,
|
||||||
queueStoryDownload,
|
queueStoryDownload,
|
||||||
stories,
|
stories,
|
||||||
toggleHideStories,
|
toggleHideStories,
|
||||||
|
toggleStoriesView,
|
||||||
}: PropsType): JSX.Element => {
|
}: PropsType): JSX.Element => {
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [isShowingHiddenStories, setIsShowingHiddenStories] = useState(false);
|
const [isShowingHiddenStories, setIsShowingHiddenStories] = useState(false);
|
||||||
|
@ -89,7 +89,7 @@ export const StoriesPane = ({
|
||||||
<button
|
<button
|
||||||
aria-label={i18n('back')}
|
aria-label={i18n('back')}
|
||||||
className="Stories__pane__header--back"
|
className="Stories__pane__header--back"
|
||||||
onClick={onBack}
|
onClick={toggleStoriesView}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
type="button"
|
type="button"
|
||||||
/>
|
/>
|
||||||
|
@ -119,11 +119,10 @@ export const StoriesPane = ({
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onStoryClicked(story.conversationId);
|
onStoryClicked(story.conversationId);
|
||||||
}}
|
}}
|
||||||
onHideStory={() => {
|
onHideStory={toggleHideStories}
|
||||||
toggleHideStories(getNewestStory(story).sender.id);
|
|
||||||
}}
|
|
||||||
onGoToConversation={conversationId => {
|
onGoToConversation={conversationId => {
|
||||||
openConversationInternal({ conversationId });
|
openConversationInternal({ conversationId });
|
||||||
|
toggleStoriesView();
|
||||||
}}
|
}}
|
||||||
queueStoryDownload={queueStoryDownload}
|
queueStoryDownload={queueStoryDownload}
|
||||||
story={getNewestStory(story)}
|
story={getNewestStory(story)}
|
||||||
|
@ -149,11 +148,10 @@ export const StoriesPane = ({
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onStoryClicked(story.conversationId);
|
onStoryClicked(story.conversationId);
|
||||||
}}
|
}}
|
||||||
onHideStory={() => {
|
onHideStory={toggleHideStories}
|
||||||
toggleHideStories(getNewestStory(story).sender.id);
|
|
||||||
}}
|
|
||||||
onGoToConversation={conversationId => {
|
onGoToConversation={conversationId => {
|
||||||
openConversationInternal({ conversationId });
|
openConversationInternal({ conversationId });
|
||||||
|
toggleStoriesView();
|
||||||
}}
|
}}
|
||||||
queueStoryDownload={queueStoryDownload}
|
queueStoryDownload={queueStoryDownload}
|
||||||
story={getNewestStory(story)}
|
story={getNewestStory(story)}
|
||||||
|
|
|
@ -23,6 +23,8 @@ function getDefaultProps(): PropsType {
|
||||||
return {
|
return {
|
||||||
i18n,
|
i18n,
|
||||||
onClick: action('onClick'),
|
onClick: action('onClick'),
|
||||||
|
onGoToConversation: action('onGoToConversation'),
|
||||||
|
onHideStory: action('onHideStory'),
|
||||||
queueStoryDownload: action('queueStoryDownload'),
|
queueStoryDownload: action('queueStoryDownload'),
|
||||||
story: {
|
story: {
|
||||||
messageId: '123',
|
messageId: '123',
|
||||||
|
|
|
@ -62,8 +62,8 @@ export type PropsType = Pick<
|
||||||
> & {
|
> & {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
onClick: () => unknown;
|
onClick: () => unknown;
|
||||||
onGoToConversation?: (conversationId: string) => unknown;
|
onGoToConversation: (conversationId: string) => unknown;
|
||||||
onHideStory?: (conversationId: string) => unknown;
|
onHideStory: (conversationId: string) => unknown;
|
||||||
queueStoryDownload: (storyId: string) => unknown;
|
queueStoryDownload: (storyId: string) => unknown;
|
||||||
story: StoryViewType;
|
story: StoryViewType;
|
||||||
};
|
};
|
||||||
|
@ -217,7 +217,7 @@ export const StoryListItem = ({
|
||||||
: i18n('StoryListItem__hide'),
|
: i18n('StoryListItem__hide'),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
if (isHidden) {
|
if (isHidden) {
|
||||||
onHideStory?.(sender.id);
|
onHideStory(sender.id);
|
||||||
} else {
|
} else {
|
||||||
setHasConfirmHideStory(true);
|
setHasConfirmHideStory(true);
|
||||||
}
|
}
|
||||||
|
@ -227,7 +227,7 @@ export const StoryListItem = ({
|
||||||
icon: 'StoryListItem__icon--chat',
|
icon: 'StoryListItem__icon--chat',
|
||||||
label: i18n('StoryListItem__go-to-chat'),
|
label: i18n('StoryListItem__go-to-chat'),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
onGoToConversation?.(sender.id);
|
onGoToConversation(sender.id);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
@ -242,7 +242,7 @@ export const StoryListItem = ({
|
||||||
<ConfirmationDialog
|
<ConfirmationDialog
|
||||||
actions={[
|
actions={[
|
||||||
{
|
{
|
||||||
action: () => onHideStory?.(sender.id),
|
action: () => onHideStory(sender.id),
|
||||||
style: 'affirmative',
|
style: 'affirmative',
|
||||||
text: i18n('StoryListItem__hide-modal--confirm'),
|
text: i18n('StoryListItem__hide-modal--confirm'),
|
||||||
},
|
},
|
||||||
|
|
|
@ -27,6 +27,8 @@ function getDefaultProps(): PropsType {
|
||||||
loadStoryReplies: action('loadStoryReplies'),
|
loadStoryReplies: action('loadStoryReplies'),
|
||||||
markStoryRead: action('markStoryRead'),
|
markStoryRead: action('markStoryRead'),
|
||||||
onClose: action('onClose'),
|
onClose: action('onClose'),
|
||||||
|
onGoToConversation: action('onGoToConversation'),
|
||||||
|
onHideStory: action('onHideStory'),
|
||||||
onNextUserStories: action('onNextUserStories'),
|
onNextUserStories: action('onNextUserStories'),
|
||||||
onPrevUserStories: action('onPrevUserStories'),
|
onPrevUserStories: action('onPrevUserStories'),
|
||||||
onReactToStory: action('onReactToStory'),
|
onReactToStory: action('onReactToStory'),
|
||||||
|
|
|
@ -12,6 +12,8 @@ import type { RenderEmojiPickerProps } from './conversation/ReactionPicker';
|
||||||
import type { ReplyStateType } from '../types/Stories';
|
import type { ReplyStateType } from '../types/Stories';
|
||||||
import type { StoryViewType } from './StoryListItem';
|
import type { StoryViewType } from './StoryListItem';
|
||||||
import { Avatar, AvatarSize } from './Avatar';
|
import { Avatar, AvatarSize } from './Avatar';
|
||||||
|
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||||
|
import { ContextMenuPopper } from './ContextMenu';
|
||||||
import { Intl } from './Intl';
|
import { Intl } from './Intl';
|
||||||
import { MessageTimestamp } from './conversation/MessageTimestamp';
|
import { MessageTimestamp } from './conversation/MessageTimestamp';
|
||||||
import { StoryImage } from './StoryImage';
|
import { StoryImage } from './StoryImage';
|
||||||
|
@ -41,6 +43,8 @@ export type PropsType = {
|
||||||
loadStoryReplies: (conversationId: string, messageId: string) => unknown;
|
loadStoryReplies: (conversationId: string, messageId: string) => unknown;
|
||||||
markStoryRead: (mId: string) => unknown;
|
markStoryRead: (mId: string) => unknown;
|
||||||
onClose: () => unknown;
|
onClose: () => unknown;
|
||||||
|
onGoToConversation: (conversationId: string) => unknown;
|
||||||
|
onHideStory: (conversationId: string) => unknown;
|
||||||
onNextUserStories: () => unknown;
|
onNextUserStories: () => unknown;
|
||||||
onPrevUserStories: () => unknown;
|
onPrevUserStories: () => unknown;
|
||||||
onSetSkinTone: (tone: number) => unknown;
|
onSetSkinTone: (tone: number) => unknown;
|
||||||
|
@ -76,6 +80,8 @@ export const StoryViewer = ({
|
||||||
loadStoryReplies,
|
loadStoryReplies,
|
||||||
markStoryRead,
|
markStoryRead,
|
||||||
onClose,
|
onClose,
|
||||||
|
onGoToConversation,
|
||||||
|
onHideStory,
|
||||||
onNextUserStories,
|
onNextUserStories,
|
||||||
onPrevUserStories,
|
onPrevUserStories,
|
||||||
onReactToStory,
|
onReactToStory,
|
||||||
|
@ -96,15 +102,21 @@ export const StoryViewer = ({
|
||||||
const [currentStoryIndex, setCurrentStoryIndex] =
|
const [currentStoryIndex, setCurrentStoryIndex] =
|
||||||
useState(selectedStoryIndex);
|
useState(selectedStoryIndex);
|
||||||
const [storyDuration, setStoryDuration] = useState<number | undefined>();
|
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 visibleStory = stories[currentStoryIndex];
|
||||||
|
|
||||||
const { attachment, canReply, messageId, timestamp } = visibleStory;
|
const { attachment, canReply, isHidden, messageId, timestamp } = visibleStory;
|
||||||
const {
|
const {
|
||||||
acceptedMessageRequest,
|
acceptedMessageRequest,
|
||||||
avatarPath,
|
avatarPath,
|
||||||
color,
|
color,
|
||||||
isMe,
|
isMe,
|
||||||
|
id,
|
||||||
|
firstName,
|
||||||
name,
|
name,
|
||||||
profileName,
|
profileName,
|
||||||
sharedGroupNames,
|
sharedGroupNames,
|
||||||
|
@ -226,13 +238,19 @@ export const StoryViewer = ({
|
||||||
};
|
};
|
||||||
}, [currentStoryIndex, spring, storyDuration]);
|
}, [currentStoryIndex, spring, storyDuration]);
|
||||||
|
|
||||||
|
const shouldPauseViewing =
|
||||||
|
hasConfirmHideStory ||
|
||||||
|
hasExpandedCaption ||
|
||||||
|
hasReplyModal ||
|
||||||
|
isShowingContextMenu;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasReplyModal || hasExpandedCaption) {
|
if (shouldPauseViewing) {
|
||||||
spring.pause();
|
spring.pause();
|
||||||
} else {
|
} else {
|
||||||
spring.resume();
|
spring.resume();
|
||||||
}
|
}
|
||||||
}, [hasExpandedCaption, hasReplyModal, spring]);
|
}, [shouldPauseViewing, spring]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
markStoryRead(messageId);
|
markStoryRead(messageId);
|
||||||
|
@ -248,7 +266,7 @@ export const StoryViewer = ({
|
||||||
.map(story => story.messageId);
|
.map(story => story.messageId);
|
||||||
}, [stories]);
|
}, [stories]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
storiesToDownload.forEach(id => queueStoryDownload(id));
|
storiesToDownload.forEach(storyId => queueStoryDownload(storyId));
|
||||||
}, [queueStoryDownload, storiesToDownload]);
|
}, [queueStoryDownload, storiesToDownload]);
|
||||||
|
|
||||||
const navigateStories = useCallback(
|
const navigateStories = useCallback(
|
||||||
|
@ -452,6 +470,8 @@ export const StoryViewer = ({
|
||||||
<button
|
<button
|
||||||
aria-label={i18n('MyStories__more')}
|
aria-label={i18n('MyStories__more')}
|
||||||
className="StoryViewer__more"
|
className="StoryViewer__more"
|
||||||
|
onClick={() => setIsShowingContextMenu(true)}
|
||||||
|
ref={setReferenceElement}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
type="button"
|
type="button"
|
||||||
/>
|
/>
|
||||||
|
@ -463,6 +483,37 @@ export const StoryViewer = ({
|
||||||
type="button"
|
type="button"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 && (
|
{hasReplyModal && canReply && (
|
||||||
<StoryViewsNRepliesModal
|
<StoryViewsNRepliesModal
|
||||||
authorTitle={title}
|
authorTitle={title}
|
||||||
|
@ -492,6 +543,23 @@ export const StoryViewer = ({
|
||||||
views={[]}
|
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>
|
</div>
|
||||||
</FocusTrap>
|
</FocusTrap>
|
||||||
);
|
);
|
||||||
|
|
|
@ -13,13 +13,14 @@ import {
|
||||||
getEmojiSkinTone,
|
getEmojiSkinTone,
|
||||||
getPreferredReactionEmoji,
|
getPreferredReactionEmoji,
|
||||||
} from '../selectors/items';
|
} from '../selectors/items';
|
||||||
import { getStoriesSelector, getStoryReplies } from '../selectors/stories';
|
|
||||||
import { getIntl } from '../selectors/user';
|
import { getIntl } from '../selectors/user';
|
||||||
import { getPreferredBadgeSelector } from '../selectors/badges';
|
import { getPreferredBadgeSelector } from '../selectors/badges';
|
||||||
|
import { getStoriesSelector, getStoryReplies } from '../selectors/stories';
|
||||||
import { renderEmojiPicker } from './renderEmojiPicker';
|
import { renderEmojiPicker } from './renderEmojiPicker';
|
||||||
import { showToast } from '../../util/showToast';
|
import { showToast } from '../../util/showToast';
|
||||||
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';
|
||||||
|
import { useConversationsActions } from '../ducks/conversations';
|
||||||
import { useRecentEmojis } from '../selectors/emojis';
|
import { useRecentEmojis } from '../selectors/emojis';
|
||||||
import { useStoriesActions } from '../ducks/stories';
|
import { useStoriesActions } from '../ducks/stories';
|
||||||
|
|
||||||
|
@ -39,6 +40,8 @@ export function SmartStoryViewer({
|
||||||
const storiesActions = useStoriesActions();
|
const storiesActions = useStoriesActions();
|
||||||
const { onSetSkinTone } = useItemsActions();
|
const { onSetSkinTone } = useItemsActions();
|
||||||
const { onUseEmoji } = useEmojisActions();
|
const { onUseEmoji } = useEmojisActions();
|
||||||
|
const { openConversationInternal, toggleHideStories } =
|
||||||
|
useConversationsActions();
|
||||||
|
|
||||||
const i18n = useSelector<StateType, LocalizerType>(getIntl);
|
const i18n = useSelector<StateType, LocalizerType>(getIntl);
|
||||||
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
|
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
|
||||||
|
@ -66,6 +69,11 @@ export function SmartStoryViewer({
|
||||||
group={group}
|
group={group}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
|
onHideStory={toggleHideStories}
|
||||||
|
onGoToConversation={senderId => {
|
||||||
|
openConversationInternal({ conversationId: senderId });
|
||||||
|
storiesActions.toggleStoriesView();
|
||||||
|
}}
|
||||||
onNextUserStories={onNextUserStories}
|
onNextUserStories={onNextUserStories}
|
||||||
onPrevUserStories={onPrevUserStories}
|
onPrevUserStories={onPrevUserStories}
|
||||||
onReactToStory={async (emoji, story) => {
|
onReactToStory={async (emoji, story) => {
|
||||||
|
|
Loading…
Reference in a new issue