2022-03-04 21:14:52 +00:00
|
|
|
// Copyright 2022 Signal Messenger, LLC
|
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
2022-04-07 21:11:33 +00:00
|
|
|
import FocusTrap from 'focus-trap-react';
|
2022-07-19 20:38:32 +00:00
|
|
|
import React, {
|
|
|
|
useCallback,
|
|
|
|
useEffect,
|
|
|
|
useMemo,
|
|
|
|
useRef,
|
|
|
|
useState,
|
|
|
|
} from 'react';
|
2022-05-03 23:50:44 +00:00
|
|
|
import classNames from 'classnames';
|
2022-03-04 21:14:52 +00:00
|
|
|
import { useSpring, animated, to } from '@react-spring/web';
|
|
|
|
import type { BodyRangeType, LocalizerType } from '../types/Util';
|
2022-07-25 18:55:44 +00:00
|
|
|
import type { ContextMenuOptionType } from './ContextMenu';
|
2022-03-04 21:14:52 +00:00
|
|
|
import type { ConversationType } from '../state/ducks/conversations';
|
|
|
|
import type { EmojiPickDataType } from './emoji/EmojiPicker';
|
|
|
|
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
|
|
|
|
import type { RenderEmojiPickerProps } from './conversation/ReactionPicker';
|
2022-07-01 00:52:03 +00:00
|
|
|
import type { ReplyStateType, StoryViewType } from '../types/Stories';
|
2022-07-12 16:41:41 +00:00
|
|
|
import type { ShowToastActionCreatorType } from '../state/ducks/toast';
|
2022-07-06 19:06:20 +00:00
|
|
|
import type { ViewStoryActionCreatorType } from '../state/ducks/stories';
|
|
|
|
import * as log from '../logging/log';
|
2022-05-04 17:43:22 +00:00
|
|
|
import { AnimatedEmojiGalore } from './AnimatedEmojiGalore';
|
2022-03-04 21:14:52 +00:00
|
|
|
import { Avatar, AvatarSize } from './Avatar';
|
2022-04-29 17:43:24 +00:00
|
|
|
import { ConfirmationDialog } from './ConfirmationDialog';
|
2022-07-25 18:55:44 +00:00
|
|
|
import { ContextMenu } from './ContextMenu';
|
2022-03-04 21:14:52 +00:00
|
|
|
import { Intl } from './Intl';
|
|
|
|
import { MessageTimestamp } from './conversation/MessageTimestamp';
|
2022-07-01 00:52:03 +00:00
|
|
|
import { SendStatus } from '../messages/MessageSendState';
|
2022-07-25 18:55:44 +00:00
|
|
|
import { StoryDetailsModal } from './StoryDetailsModal';
|
|
|
|
import { StoryViewsNRepliesModal } from './StoryViewsNRepliesModal';
|
2022-03-29 01:10:08 +00:00
|
|
|
import { StoryImage } from './StoryImage';
|
2022-07-06 19:06:20 +00:00
|
|
|
import { StoryViewDirectionType, StoryViewModeType } from '../types/Stories';
|
2022-05-06 19:02:44 +00:00
|
|
|
import { Theme } from '../util/theme';
|
2022-07-12 16:41:41 +00:00
|
|
|
import { ToastType } from '../state/ducks/toast';
|
2022-03-04 21:14:52 +00:00
|
|
|
import { getAvatarColor } from '../types/Colors';
|
2022-04-22 18:36:34 +00:00
|
|
|
import { getStoryBackground } from '../util/getStoryBackground';
|
2022-04-12 19:29:30 +00:00
|
|
|
import { getStoryDuration } from '../util/getStoryDuration';
|
2022-07-29 19:27:02 +00:00
|
|
|
import { isVideoAttachment } from '../types/Attachment';
|
2022-04-14 17:02:12 +00:00
|
|
|
import { graphemeAwareSlice } from '../util/graphemeAwareSlice';
|
2022-03-04 21:14:52 +00:00
|
|
|
import { useEscapeHandling } from '../hooks/useEscapeHandling';
|
|
|
|
|
|
|
|
export type PropsType = {
|
2022-07-06 19:06:20 +00:00
|
|
|
currentIndex: number;
|
2022-07-25 18:55:44 +00:00
|
|
|
deleteStoryForEveryone: (story: StoryViewType) => unknown;
|
2022-03-04 21:14:52 +00:00
|
|
|
getPreferredBadge: PreferredBadgeSelectorType;
|
2022-04-15 00:08:46 +00:00
|
|
|
group?: Pick<
|
|
|
|
ConversationType,
|
|
|
|
| 'acceptedMessageRequest'
|
|
|
|
| 'avatarPath'
|
|
|
|
| 'color'
|
|
|
|
| 'id'
|
|
|
|
| 'name'
|
|
|
|
| 'profileName'
|
|
|
|
| 'sharedGroupNames'
|
2022-08-23 18:02:51 +00:00
|
|
|
| 'sortedGroupMembers'
|
2022-04-15 00:08:46 +00:00
|
|
|
| 'title'
|
|
|
|
>;
|
2022-08-05 01:07:46 +00:00
|
|
|
hasActiveCall?: boolean;
|
2022-05-06 19:02:44 +00:00
|
|
|
hasAllStoriesMuted: boolean;
|
2022-03-04 21:14:52 +00:00
|
|
|
i18n: LocalizerType;
|
2022-04-15 00:08:46 +00:00
|
|
|
loadStoryReplies: (conversationId: string, messageId: string) => unknown;
|
2022-03-04 21:14:52 +00:00
|
|
|
markStoryRead: (mId: string) => unknown;
|
2022-07-06 19:06:20 +00:00
|
|
|
numStories: number;
|
2022-04-29 17:43:24 +00:00
|
|
|
onGoToConversation: (conversationId: string) => unknown;
|
|
|
|
onHideStory: (conversationId: string) => unknown;
|
2022-03-04 21:14:52 +00:00
|
|
|
onSetSkinTone: (tone: number) => unknown;
|
|
|
|
onTextTooLong: () => unknown;
|
|
|
|
onReactToStory: (emoji: string, story: StoryViewType) => unknown;
|
|
|
|
onReplyToStory: (
|
|
|
|
message: string,
|
|
|
|
mentions: Array<BodyRangeType>,
|
|
|
|
timestamp: number,
|
|
|
|
story: StoryViewType
|
|
|
|
) => unknown;
|
|
|
|
onUseEmoji: (_: EmojiPickDataType) => unknown;
|
|
|
|
preferredReactionEmoji: Array<string>;
|
2022-03-29 01:10:08 +00:00
|
|
|
queueStoryDownload: (storyId: string) => unknown;
|
2022-03-04 21:14:52 +00:00
|
|
|
recentEmojis?: Array<string>;
|
|
|
|
renderEmojiPicker: (props: RenderEmojiPickerProps) => JSX.Element;
|
2022-04-15 00:08:46 +00:00
|
|
|
replyState?: ReplyStateType;
|
2022-07-25 18:55:44 +00:00
|
|
|
shouldShowDetailsModal?: boolean;
|
2022-07-12 16:41:41 +00:00
|
|
|
showToast: ShowToastActionCreatorType;
|
2022-03-04 21:14:52 +00:00
|
|
|
skinTone?: number;
|
2022-07-06 19:06:20 +00:00
|
|
|
story: StoryViewType;
|
2022-08-22 17:44:23 +00:00
|
|
|
storyViewMode: StoryViewModeType;
|
2022-05-06 19:02:44 +00:00
|
|
|
toggleHasAllStoriesMuted: () => unknown;
|
2022-07-06 19:06:20 +00:00
|
|
|
viewStory: ViewStoryActionCreatorType;
|
2022-03-04 21:14:52 +00:00
|
|
|
};
|
|
|
|
|
2022-04-14 17:02:12 +00:00
|
|
|
const CAPTION_BUFFER = 20;
|
|
|
|
const CAPTION_INITIAL_LENGTH = 200;
|
|
|
|
const CAPTION_MAX_LENGTH = 700;
|
2022-05-03 23:50:44 +00:00
|
|
|
const MOUSE_IDLE_TIME = 3000;
|
|
|
|
|
|
|
|
enum Arrow {
|
|
|
|
None,
|
|
|
|
Left,
|
|
|
|
Right,
|
|
|
|
}
|
2022-04-14 17:02:12 +00:00
|
|
|
|
2022-03-04 21:14:52 +00:00
|
|
|
export const StoryViewer = ({
|
2022-07-06 19:06:20 +00:00
|
|
|
currentIndex,
|
2022-07-25 18:55:44 +00:00
|
|
|
deleteStoryForEveryone,
|
2022-03-04 21:14:52 +00:00
|
|
|
getPreferredBadge,
|
|
|
|
group,
|
2022-08-05 01:07:46 +00:00
|
|
|
hasActiveCall,
|
2022-05-06 19:02:44 +00:00
|
|
|
hasAllStoriesMuted,
|
2022-03-04 21:14:52 +00:00
|
|
|
i18n,
|
2022-04-15 00:08:46 +00:00
|
|
|
loadStoryReplies,
|
2022-03-04 21:14:52 +00:00
|
|
|
markStoryRead,
|
2022-07-06 19:06:20 +00:00
|
|
|
numStories,
|
2022-04-29 17:43:24 +00:00
|
|
|
onGoToConversation,
|
|
|
|
onHideStory,
|
2022-03-04 21:14:52 +00:00
|
|
|
onReactToStory,
|
|
|
|
onReplyToStory,
|
|
|
|
onSetSkinTone,
|
|
|
|
onTextTooLong,
|
|
|
|
onUseEmoji,
|
|
|
|
preferredReactionEmoji,
|
2022-03-29 01:10:08 +00:00
|
|
|
queueStoryDownload,
|
2022-03-04 21:14:52 +00:00
|
|
|
recentEmojis,
|
|
|
|
renderEmojiPicker,
|
2022-04-15 00:08:46 +00:00
|
|
|
replyState,
|
2022-07-25 18:55:44 +00:00
|
|
|
shouldShowDetailsModal,
|
2022-07-12 16:41:41 +00:00
|
|
|
showToast,
|
2022-03-04 21:14:52 +00:00
|
|
|
skinTone,
|
2022-07-06 19:06:20 +00:00
|
|
|
story,
|
|
|
|
storyViewMode,
|
2022-05-06 19:02:44 +00:00
|
|
|
toggleHasAllStoriesMuted,
|
2022-07-06 19:06:20 +00:00
|
|
|
viewStory,
|
2022-03-04 21:14:52 +00:00
|
|
|
}: PropsType): JSX.Element => {
|
2022-07-25 18:55:44 +00:00
|
|
|
const [isShowingContextMenu, setIsShowingContextMenu] =
|
|
|
|
useState<boolean>(false);
|
2022-04-12 19:29:30 +00:00
|
|
|
const [storyDuration, setStoryDuration] = useState<number | undefined>();
|
2022-04-29 17:43:24 +00:00
|
|
|
const [hasConfirmHideStory, setHasConfirmHideStory] = useState(false);
|
2022-05-04 17:43:22 +00:00
|
|
|
const [reactionEmoji, setReactionEmoji] = useState<string | undefined>();
|
2022-07-25 18:55:44 +00:00
|
|
|
const [confirmDeleteStory, setConfirmDeleteStory] = useState<
|
|
|
|
StoryViewType | undefined
|
|
|
|
>();
|
2022-03-04 21:14:52 +00:00
|
|
|
|
2022-07-01 00:52:03 +00:00
|
|
|
const { attachment, canReply, isHidden, messageId, sendState, timestamp } =
|
2022-07-06 19:06:20 +00:00
|
|
|
story;
|
2022-03-04 21:14:52 +00:00
|
|
|
const {
|
|
|
|
acceptedMessageRequest,
|
|
|
|
avatarPath,
|
|
|
|
color,
|
|
|
|
isMe,
|
2022-04-29 17:43:24 +00:00
|
|
|
id,
|
|
|
|
firstName,
|
2022-03-04 21:14:52 +00:00
|
|
|
name,
|
|
|
|
profileName,
|
|
|
|
sharedGroupNames,
|
|
|
|
title,
|
2022-07-06 19:06:20 +00:00
|
|
|
} = story.sender;
|
2022-03-04 21:14:52 +00:00
|
|
|
|
2022-07-25 18:55:44 +00:00
|
|
|
const [hasStoryViewsNRepliesModal, setHasStoryViewsNRepliesModal] =
|
|
|
|
useState(false);
|
|
|
|
const [hasStoryDetailsModal, setHasStoryDetailsModal] = useState(
|
|
|
|
Boolean(shouldShowDetailsModal)
|
|
|
|
);
|
2022-03-04 21:14:52 +00:00
|
|
|
|
2022-07-06 19:06:20 +00:00
|
|
|
const onClose = useCallback(() => {
|
2022-07-25 18:55:44 +00:00
|
|
|
viewStory({
|
|
|
|
closeViewer: true,
|
|
|
|
});
|
2022-07-06 19:06:20 +00:00
|
|
|
}, [viewStory]);
|
|
|
|
|
2022-03-04 21:14:52 +00:00
|
|
|
const onEscape = useCallback(() => {
|
2022-07-25 18:55:44 +00:00
|
|
|
if (hasStoryViewsNRepliesModal) {
|
|
|
|
setHasStoryViewsNRepliesModal(false);
|
2022-03-04 21:14:52 +00:00
|
|
|
} else {
|
|
|
|
onClose();
|
|
|
|
}
|
2022-07-25 18:55:44 +00:00
|
|
|
}, [hasStoryViewsNRepliesModal, onClose]);
|
2022-03-04 21:14:52 +00:00
|
|
|
|
|
|
|
useEscapeHandling(onEscape);
|
|
|
|
|
2022-04-14 17:02:12 +00:00
|
|
|
// Caption related hooks
|
|
|
|
const [hasExpandedCaption, setHasExpandedCaption] = useState<boolean>(false);
|
|
|
|
|
|
|
|
const caption = useMemo(() => {
|
|
|
|
if (!attachment?.caption) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
return graphemeAwareSlice(
|
|
|
|
attachment.caption,
|
|
|
|
hasExpandedCaption ? CAPTION_MAX_LENGTH : CAPTION_INITIAL_LENGTH,
|
|
|
|
CAPTION_BUFFER
|
|
|
|
);
|
|
|
|
}, [attachment?.caption, hasExpandedCaption]);
|
|
|
|
|
|
|
|
// Reset expansion if messageId changes
|
|
|
|
useEffect(() => {
|
|
|
|
setHasExpandedCaption(false);
|
|
|
|
}, [messageId]);
|
|
|
|
|
2022-08-05 01:08:15 +00:00
|
|
|
// messageId is set as a dependency so that we can reset the story duration
|
|
|
|
// when a new story is selected in case the same story (and same attachment)
|
|
|
|
// are sequentially posted.
|
2022-04-12 19:29:30 +00:00
|
|
|
useEffect(() => {
|
|
|
|
let shouldCancel = false;
|
|
|
|
(async function hydrateStoryDuration() {
|
|
|
|
if (!attachment) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const duration = await getStoryDuration(attachment);
|
|
|
|
if (shouldCancel) {
|
|
|
|
return;
|
|
|
|
}
|
2022-05-09 16:38:32 +00:00
|
|
|
log.info('stories.setStoryDuration', {
|
|
|
|
contentType: attachment.textAttachment
|
|
|
|
? 'text'
|
|
|
|
: attachment.contentType,
|
|
|
|
duration,
|
|
|
|
});
|
2022-04-12 19:29:30 +00:00
|
|
|
setStoryDuration(duration);
|
|
|
|
})();
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
shouldCancel = true;
|
|
|
|
};
|
2022-08-05 01:08:15 +00:00
|
|
|
}, [attachment, messageId]);
|
2022-04-12 19:29:30 +00:00
|
|
|
|
2022-07-19 20:38:32 +00:00
|
|
|
const unmountRef = useRef<boolean>(false);
|
|
|
|
useEffect(() => {
|
|
|
|
return () => {
|
|
|
|
unmountRef.current = true;
|
|
|
|
};
|
2022-07-22 01:38:27 +00:00
|
|
|
}, []);
|
2022-07-19 20:38:32 +00:00
|
|
|
|
2022-04-20 23:38:38 +00:00
|
|
|
const [styles, spring] = useSpring(
|
|
|
|
() => ({
|
2022-03-04 21:14:52 +00:00
|
|
|
from: { width: 0 },
|
|
|
|
to: { width: 100 },
|
2022-04-20 23:38:38 +00:00
|
|
|
loop: true,
|
2022-03-29 01:10:08 +00:00
|
|
|
onRest: {
|
|
|
|
width: ({ value }) => {
|
2022-07-19 20:38:32 +00:00
|
|
|
if (unmountRef.current) {
|
|
|
|
log.info(
|
|
|
|
'stories.StoryViewer.spring.onRest: called after component unmounted'
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-03-29 01:10:08 +00:00
|
|
|
if (value === 100) {
|
2022-07-25 18:55:44 +00:00
|
|
|
viewStory({
|
|
|
|
storyId: story.messageId,
|
2022-07-06 19:06:20 +00:00
|
|
|
storyViewMode,
|
2022-07-25 18:55:44 +00:00
|
|
|
viewDirection: StoryViewDirectionType.Next,
|
|
|
|
});
|
2022-03-29 01:10:08 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
2022-04-20 23:38:38 +00:00
|
|
|
}),
|
2022-07-06 19:06:20 +00:00
|
|
|
[story.messageId, storyViewMode, viewStory]
|
2022-04-20 23:38:38 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
// We need to be careful about this effect refreshing, it should only run
|
|
|
|
// every time a story changes or its duration changes.
|
|
|
|
useEffect(() => {
|
2022-04-25 17:25:50 +00:00
|
|
|
if (!storyDuration) {
|
|
|
|
spring.stop();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-04-20 23:38:38 +00:00
|
|
|
spring.start({
|
|
|
|
config: {
|
|
|
|
duration: storyDuration,
|
|
|
|
},
|
|
|
|
from: { width: 0 },
|
|
|
|
to: { width: 100 },
|
2022-03-04 21:14:52 +00:00
|
|
|
});
|
2022-03-29 01:10:08 +00:00
|
|
|
|
|
|
|
return () => {
|
|
|
|
spring.stop();
|
|
|
|
};
|
2022-07-06 19:06:20 +00:00
|
|
|
}, [currentIndex, spring, storyDuration]);
|
2022-03-04 21:14:52 +00:00
|
|
|
|
2022-05-06 19:02:44 +00:00
|
|
|
const [pauseStory, setPauseStory] = useState(false);
|
|
|
|
|
2022-04-29 17:43:24 +00:00
|
|
|
const shouldPauseViewing =
|
2022-08-05 01:07:46 +00:00
|
|
|
hasActiveCall ||
|
2022-04-29 17:43:24 +00:00
|
|
|
hasConfirmHideStory ||
|
|
|
|
hasExpandedCaption ||
|
2022-07-25 18:55:44 +00:00
|
|
|
hasStoryDetailsModal ||
|
|
|
|
hasStoryViewsNRepliesModal ||
|
2022-05-04 17:43:22 +00:00
|
|
|
isShowingContextMenu ||
|
2022-05-06 19:02:44 +00:00
|
|
|
pauseStory ||
|
2022-05-04 17:43:22 +00:00
|
|
|
Boolean(reactionEmoji);
|
2022-04-29 17:43:24 +00:00
|
|
|
|
2022-03-04 21:14:52 +00:00
|
|
|
useEffect(() => {
|
2022-04-29 17:43:24 +00:00
|
|
|
if (shouldPauseViewing) {
|
2022-03-04 21:14:52 +00:00
|
|
|
spring.pause();
|
|
|
|
} else {
|
|
|
|
spring.resume();
|
|
|
|
}
|
2022-04-29 17:43:24 +00:00
|
|
|
}, [shouldPauseViewing, spring]);
|
2022-03-04 21:14:52 +00:00
|
|
|
|
|
|
|
useEffect(() => {
|
2022-03-29 01:10:08 +00:00
|
|
|
markStoryRead(messageId);
|
2022-05-09 16:38:32 +00:00
|
|
|
log.info('stories.markStoryRead', { messageId });
|
2022-03-29 01:10:08 +00:00
|
|
|
}, [markStoryRead, messageId]);
|
|
|
|
|
2022-08-22 17:44:23 +00:00
|
|
|
const canFreelyNavigateStories =
|
|
|
|
storyViewMode === StoryViewModeType.All ||
|
|
|
|
storyViewMode === StoryViewModeType.Unread;
|
|
|
|
|
|
|
|
const canNavigateLeft =
|
|
|
|
(storyViewMode === StoryViewModeType.User && currentIndex > 0) ||
|
|
|
|
canFreelyNavigateStories;
|
|
|
|
|
|
|
|
const canNavigateRight =
|
|
|
|
(storyViewMode === StoryViewModeType.User &&
|
|
|
|
currentIndex < numStories - 1) ||
|
|
|
|
canFreelyNavigateStories;
|
|
|
|
|
2022-03-04 21:14:52 +00:00
|
|
|
const navigateStories = useCallback(
|
|
|
|
(ev: KeyboardEvent) => {
|
2022-08-22 17:44:23 +00:00
|
|
|
if (canNavigateRight && ev.key === 'ArrowRight') {
|
2022-07-25 18:55:44 +00:00
|
|
|
viewStory({
|
|
|
|
storyId: story.messageId,
|
|
|
|
storyViewMode,
|
|
|
|
viewDirection: StoryViewDirectionType.Next,
|
|
|
|
});
|
2022-03-04 21:14:52 +00:00
|
|
|
ev.preventDefault();
|
|
|
|
ev.stopPropagation();
|
2022-08-22 17:44:23 +00:00
|
|
|
} else if (canNavigateLeft && ev.key === 'ArrowLeft') {
|
2022-07-25 18:55:44 +00:00
|
|
|
viewStory({
|
|
|
|
storyId: story.messageId,
|
2022-07-06 19:06:20 +00:00
|
|
|
storyViewMode,
|
2022-07-25 18:55:44 +00:00
|
|
|
viewDirection: StoryViewDirectionType.Previous,
|
|
|
|
});
|
2022-03-04 21:14:52 +00:00
|
|
|
ev.preventDefault();
|
|
|
|
ev.stopPropagation();
|
|
|
|
}
|
|
|
|
},
|
2022-08-22 17:44:23 +00:00
|
|
|
[
|
|
|
|
canNavigateLeft,
|
|
|
|
canNavigateRight,
|
|
|
|
story.messageId,
|
|
|
|
storyViewMode,
|
|
|
|
viewStory,
|
|
|
|
]
|
2022-03-04 21:14:52 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
document.addEventListener('keydown', navigateStories);
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
document.removeEventListener('keydown', navigateStories);
|
|
|
|
};
|
|
|
|
}, [navigateStories]);
|
|
|
|
|
2022-07-06 19:06:20 +00:00
|
|
|
const groupId = group?.id;
|
|
|
|
const isGroupStory = Boolean(groupId);
|
2022-04-15 00:08:46 +00:00
|
|
|
useEffect(() => {
|
2022-07-06 19:06:20 +00:00
|
|
|
if (!groupId) {
|
2022-04-15 00:08:46 +00:00
|
|
|
return;
|
|
|
|
}
|
2022-07-06 19:06:20 +00:00
|
|
|
loadStoryReplies(groupId, messageId);
|
|
|
|
}, [groupId, loadStoryReplies, messageId]);
|
2022-04-15 00:08:46 +00:00
|
|
|
|
2022-05-03 23:50:44 +00:00
|
|
|
const [arrowToShow, setArrowToShow] = useState<Arrow>(Arrow.None);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (arrowToShow === Arrow.None) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let lastMouseMove: number | undefined;
|
|
|
|
|
|
|
|
function updateLastMouseMove() {
|
|
|
|
lastMouseMove = Date.now();
|
|
|
|
}
|
|
|
|
|
|
|
|
function checkMouseIdle() {
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
if (lastMouseMove && Date.now() - lastMouseMove > MOUSE_IDLE_TIME) {
|
|
|
|
setArrowToShow(Arrow.None);
|
|
|
|
} else {
|
|
|
|
checkMouseIdle();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
checkMouseIdle();
|
|
|
|
|
|
|
|
document.addEventListener('mousemove', updateLastMouseMove);
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
lastMouseMove = undefined;
|
|
|
|
document.removeEventListener('mousemove', updateLastMouseMove);
|
|
|
|
};
|
|
|
|
}, [arrowToShow]);
|
|
|
|
|
2022-04-15 00:08:46 +00:00
|
|
|
const replies =
|
|
|
|
replyState && replyState.messageId === messageId ? replyState.replies : [];
|
2022-07-01 00:52:03 +00:00
|
|
|
const views = sendState
|
|
|
|
? sendState.filter(({ status }) => status === SendStatus.Viewed)
|
|
|
|
: [];
|
2022-04-15 00:08:46 +00:00
|
|
|
const replyCount = replies.length;
|
2022-07-01 00:52:03 +00:00
|
|
|
const viewCount = views.length;
|
|
|
|
|
2022-07-29 19:27:02 +00:00
|
|
|
const canMuteStory = isVideoAttachment(attachment);
|
|
|
|
const isStoryMuted = hasAllStoriesMuted || !canMuteStory;
|
|
|
|
|
|
|
|
let muteClassName: string;
|
|
|
|
let muteAriaLabel: string;
|
|
|
|
if (canMuteStory) {
|
|
|
|
muteAriaLabel = hasAllStoriesMuted
|
|
|
|
? i18n('StoryViewer__unmute')
|
|
|
|
: i18n('StoryViewer__mute');
|
|
|
|
|
|
|
|
muteClassName = hasAllStoriesMuted
|
|
|
|
? 'StoryViewer__unmute'
|
|
|
|
: 'StoryViewer__mute';
|
|
|
|
} else {
|
|
|
|
muteAriaLabel = i18n('Stories__toast--hasNoSound');
|
|
|
|
muteClassName = 'StoryViewer__soundless';
|
|
|
|
}
|
|
|
|
|
2022-07-25 18:55:44 +00:00
|
|
|
const contextMenuOptions: ReadonlyArray<ContextMenuOptionType<unknown>> =
|
|
|
|
sendState
|
|
|
|
? [
|
|
|
|
{
|
|
|
|
icon: 'StoryListItem__icon--info',
|
|
|
|
label: i18n('StoryListItem__info'),
|
|
|
|
onClick: () => setHasStoryDetailsModal(true),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
icon: 'StoryListItem__icon--delete',
|
|
|
|
label: i18n('StoryListItem__delete'),
|
|
|
|
onClick: () => setConfirmDeleteStory(story),
|
|
|
|
},
|
|
|
|
]
|
|
|
|
: [
|
|
|
|
{
|
|
|
|
icon: 'StoryListItem__icon--info',
|
|
|
|
label: i18n('StoryListItem__info'),
|
|
|
|
onClick: () => setHasStoryDetailsModal(true),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
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);
|
|
|
|
},
|
|
|
|
},
|
|
|
|
];
|
|
|
|
|
2022-03-04 21:14:52 +00:00
|
|
|
return (
|
2022-04-08 18:50:26 +00:00
|
|
|
<FocusTrap focusTrapOptions={{ allowOutsideClick: true }}>
|
2022-04-07 21:11:33 +00:00
|
|
|
<div className="StoryViewer">
|
2022-04-22 18:36:34 +00:00
|
|
|
<div
|
|
|
|
className="StoryViewer__overlay"
|
|
|
|
style={{ background: getStoryBackground(attachment) }}
|
|
|
|
/>
|
2022-04-07 21:11:33 +00:00
|
|
|
<div className="StoryViewer__content">
|
2022-08-22 17:44:23 +00:00
|
|
|
{canNavigateLeft && (
|
2022-07-01 00:52:03 +00:00
|
|
|
<button
|
|
|
|
aria-label={i18n('back')}
|
|
|
|
className={classNames(
|
|
|
|
'StoryViewer__arrow StoryViewer__arrow--left',
|
|
|
|
{
|
|
|
|
'StoryViewer__arrow--visible': arrowToShow === Arrow.Left,
|
|
|
|
}
|
|
|
|
)}
|
2022-07-06 19:06:20 +00:00
|
|
|
onClick={() =>
|
2022-07-25 18:55:44 +00:00
|
|
|
viewStory({
|
|
|
|
storyId: story.messageId,
|
2022-07-06 19:06:20 +00:00
|
|
|
storyViewMode,
|
2022-07-25 18:55:44 +00:00
|
|
|
viewDirection: StoryViewDirectionType.Previous,
|
|
|
|
})
|
2022-07-06 19:06:20 +00:00
|
|
|
}
|
2022-07-01 00:52:03 +00:00
|
|
|
onMouseMove={() => setArrowToShow(Arrow.Left)}
|
|
|
|
type="button"
|
|
|
|
/>
|
|
|
|
)}
|
2022-05-03 23:50:44 +00:00
|
|
|
<div className="StoryViewer__protection StoryViewer__protection--top" />
|
2022-04-07 21:11:33 +00:00
|
|
|
<div className="StoryViewer__container">
|
|
|
|
<StoryImage
|
|
|
|
attachment={attachment}
|
2022-08-04 00:38:41 +00:00
|
|
|
firstName={firstName || title}
|
2022-03-04 21:14:52 +00:00
|
|
|
i18n={i18n}
|
2022-05-02 16:24:41 +00:00
|
|
|
isPaused={shouldPauseViewing}
|
2022-07-29 19:27:02 +00:00
|
|
|
isMuted={isStoryMuted}
|
2022-04-07 21:11:33 +00:00
|
|
|
label={i18n('lightboxImageAlt')}
|
|
|
|
moduleClassName="StoryViewer__story"
|
|
|
|
queueStoryDownload={queueStoryDownload}
|
|
|
|
storyId={messageId}
|
2022-05-04 17:43:22 +00:00
|
|
|
>
|
|
|
|
{reactionEmoji && (
|
|
|
|
<div className="StoryViewer__animated-emojis">
|
|
|
|
<AnimatedEmojiGalore
|
|
|
|
emoji={reactionEmoji}
|
|
|
|
onAnimationEnd={() => {
|
|
|
|
setReactionEmoji(undefined);
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</StoryImage>
|
2022-04-14 17:02:12 +00:00
|
|
|
{hasExpandedCaption && (
|
2022-04-22 18:36:34 +00:00
|
|
|
<button
|
|
|
|
aria-label={i18n('close-popup')}
|
|
|
|
className="StoryViewer__caption__overlay"
|
|
|
|
onClick={() => setHasExpandedCaption(false)}
|
|
|
|
type="button"
|
|
|
|
/>
|
2022-04-14 17:02:12 +00:00
|
|
|
)}
|
2022-05-04 18:45:32 +00:00
|
|
|
</div>
|
|
|
|
<div className="StoryViewer__meta">
|
|
|
|
{caption && (
|
|
|
|
<div className="StoryViewer__caption">
|
|
|
|
{caption.text}
|
|
|
|
{caption.hasReadMore && !hasExpandedCaption && (
|
|
|
|
<button
|
|
|
|
className="MessageBody__read-more"
|
|
|
|
onClick={() => {
|
|
|
|
setHasExpandedCaption(true);
|
|
|
|
}}
|
|
|
|
onKeyDown={(ev: React.KeyboardEvent) => {
|
|
|
|
if (ev.key === 'Space' || ev.key === 'Enter') {
|
2022-04-14 17:02:12 +00:00
|
|
|
setHasExpandedCaption(true);
|
2022-05-04 18:45:32 +00:00
|
|
|
}
|
|
|
|
}}
|
|
|
|
type="button"
|
|
|
|
>
|
|
|
|
...
|
|
|
|
{i18n('MessageBody--read-more')}
|
|
|
|
</button>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
)}
|
2022-05-06 19:02:44 +00:00
|
|
|
<div className="StoryViewer__meta__playback-bar">
|
|
|
|
<div>
|
|
|
|
<Avatar
|
|
|
|
acceptedMessageRequest={acceptedMessageRequest}
|
|
|
|
avatarPath={avatarPath}
|
|
|
|
badge={undefined}
|
|
|
|
color={getAvatarColor(color)}
|
|
|
|
conversationType="direct"
|
|
|
|
i18n={i18n}
|
|
|
|
isMe={Boolean(isMe)}
|
|
|
|
name={name}
|
|
|
|
profileName={profileName}
|
|
|
|
sharedGroupNames={sharedGroupNames}
|
|
|
|
size={AvatarSize.TWENTY_EIGHT}
|
|
|
|
title={title}
|
|
|
|
/>
|
|
|
|
{group && (
|
|
|
|
<Avatar
|
|
|
|
acceptedMessageRequest={group.acceptedMessageRequest}
|
|
|
|
avatarPath={group.avatarPath}
|
|
|
|
badge={undefined}
|
|
|
|
className="StoryViewer__meta--group-avatar"
|
|
|
|
color={getAvatarColor(group.color)}
|
|
|
|
conversationType="group"
|
|
|
|
i18n={i18n}
|
|
|
|
isMe={false}
|
|
|
|
name={group.name}
|
|
|
|
profileName={group.profileName}
|
|
|
|
sharedGroupNames={group.sharedGroupNames}
|
|
|
|
size={AvatarSize.TWENTY_EIGHT}
|
|
|
|
title={group.title}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
<div className="StoryViewer__meta--title">
|
|
|
|
{group
|
|
|
|
? i18n('Stories__from-to-group', {
|
|
|
|
name: title,
|
|
|
|
group: group.title,
|
|
|
|
})
|
|
|
|
: title}
|
|
|
|
</div>
|
|
|
|
<MessageTimestamp
|
|
|
|
i18n={i18n}
|
|
|
|
isRelativeTime
|
|
|
|
module="StoryViewer__meta--timestamp"
|
|
|
|
timestamp={timestamp}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
<div className="StoryViewer__meta__playback-controls">
|
|
|
|
<button
|
|
|
|
aria-label={
|
|
|
|
pauseStory
|
|
|
|
? i18n('StoryViewer__play')
|
|
|
|
: i18n('StoryViewer__pause')
|
|
|
|
}
|
|
|
|
className={
|
|
|
|
pauseStory ? 'StoryViewer__play' : 'StoryViewer__pause'
|
|
|
|
}
|
|
|
|
onClick={() => setPauseStory(!pauseStory)}
|
|
|
|
type="button"
|
|
|
|
/>
|
|
|
|
<button
|
2022-07-29 19:27:02 +00:00
|
|
|
aria-label={muteAriaLabel}
|
|
|
|
className={muteClassName}
|
|
|
|
onClick={
|
|
|
|
canMuteStory
|
|
|
|
? toggleHasAllStoriesMuted
|
|
|
|
: () => showToast(ToastType.StoryMuted)
|
2022-05-06 19:02:44 +00:00
|
|
|
}
|
|
|
|
type="button"
|
|
|
|
/>
|
2022-07-25 18:55:44 +00:00
|
|
|
<ContextMenu
|
|
|
|
aria-label={i18n('MyStories__more')}
|
|
|
|
i18n={i18n}
|
|
|
|
menuOptions={contextMenuOptions}
|
|
|
|
moduleClassName="StoryViewer__more"
|
|
|
|
onMenuShowingChanged={setIsShowingContextMenu}
|
|
|
|
theme={Theme.Dark}
|
|
|
|
/>
|
2022-05-06 19:02:44 +00:00
|
|
|
</div>
|
2022-05-04 18:45:32 +00:00
|
|
|
</div>
|
|
|
|
<div className="StoryViewer__progress">
|
2022-07-06 19:06:20 +00:00
|
|
|
{Array.from(Array(numStories), (_, index) => (
|
|
|
|
<div className="StoryViewer__progress--container" key={index}>
|
|
|
|
{currentIndex === index ? (
|
2022-05-04 18:45:32 +00:00
|
|
|
<animated.div
|
|
|
|
className="StoryViewer__progress--bar"
|
|
|
|
style={{
|
|
|
|
width: to([styles.width], width => `${width}%`),
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
) : (
|
|
|
|
<div
|
|
|
|
className="StoryViewer__progress--bar"
|
|
|
|
style={{
|
2022-07-06 19:06:20 +00:00
|
|
|
width: currentIndex < index ? '0%' : '100%',
|
2022-05-04 18:45:32 +00:00
|
|
|
}}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
))}
|
|
|
|
</div>
|
|
|
|
<div className="StoryViewer__actions">
|
2022-07-29 00:15:20 +00:00
|
|
|
{(canReply || sendState) && (
|
2022-05-04 18:45:32 +00:00
|
|
|
<button
|
|
|
|
className="StoryViewer__reply"
|
2022-07-25 18:55:44 +00:00
|
|
|
onClick={() => setHasStoryViewsNRepliesModal(true)}
|
2022-05-04 18:45:32 +00:00
|
|
|
tabIndex={0}
|
|
|
|
type="button"
|
|
|
|
>
|
|
|
|
<>
|
2022-07-25 18:55:44 +00:00
|
|
|
{sendState || replyCount > 0 ? (
|
2022-05-06 16:17:33 +00:00
|
|
|
<span className="StoryViewer__reply__chevron">
|
2022-07-25 18:55:44 +00:00
|
|
|
{sendState &&
|
2022-05-06 16:17:33 +00:00
|
|
|
(viewCount === 1 ? (
|
|
|
|
<Intl
|
|
|
|
i18n={i18n}
|
|
|
|
id="MyStories__views--singular"
|
|
|
|
components={[<strong>{viewCount}</strong>]}
|
|
|
|
/>
|
|
|
|
) : (
|
|
|
|
<Intl
|
|
|
|
i18n={i18n}
|
|
|
|
id="MyStories__views--plural"
|
|
|
|
components={[<strong>{viewCount}</strong>]}
|
|
|
|
/>
|
|
|
|
))}
|
|
|
|
{viewCount > 0 && replyCount > 0 && ' '}
|
|
|
|
{replyCount > 0 &&
|
|
|
|
(replyCount === 1 ? (
|
|
|
|
<Intl
|
|
|
|
i18n={i18n}
|
|
|
|
id="MyStories__replies--singular"
|
|
|
|
components={[<strong>{replyCount}</strong>]}
|
|
|
|
/>
|
|
|
|
) : (
|
|
|
|
<Intl
|
|
|
|
i18n={i18n}
|
|
|
|
id="MyStories__replies--plural"
|
|
|
|
components={[<strong>{replyCount}</strong>]}
|
|
|
|
/>
|
|
|
|
))}
|
|
|
|
</span>
|
|
|
|
) : null}
|
2022-07-25 18:55:44 +00:00
|
|
|
{!sendState && !replyCount && (
|
2022-05-06 16:17:33 +00:00
|
|
|
<span className="StoryViewer__reply__arrow">
|
|
|
|
{isGroupStory
|
|
|
|
? i18n('StoryViewer__reply-group')
|
|
|
|
: i18n('StoryViewer__reply')}
|
|
|
|
</span>
|
|
|
|
)}
|
2022-05-04 18:45:32 +00:00
|
|
|
</>
|
|
|
|
</button>
|
2022-04-07 21:11:33 +00:00
|
|
|
)}
|
2022-03-04 21:14:52 +00:00
|
|
|
</div>
|
2022-04-07 21:11:33 +00:00
|
|
|
</div>
|
2022-08-22 17:44:23 +00:00
|
|
|
{canNavigateRight && (
|
2022-07-01 00:52:03 +00:00
|
|
|
<button
|
|
|
|
aria-label={i18n('forward')}
|
|
|
|
className={classNames(
|
|
|
|
'StoryViewer__arrow StoryViewer__arrow--right',
|
|
|
|
{
|
|
|
|
'StoryViewer__arrow--visible': arrowToShow === Arrow.Right,
|
|
|
|
}
|
|
|
|
)}
|
2022-07-06 19:06:20 +00:00
|
|
|
onClick={() =>
|
2022-07-25 18:55:44 +00:00
|
|
|
viewStory({
|
|
|
|
storyId: story.messageId,
|
2022-07-06 19:06:20 +00:00
|
|
|
storyViewMode,
|
2022-07-25 18:55:44 +00:00
|
|
|
viewDirection: StoryViewDirectionType.Next,
|
|
|
|
})
|
2022-07-06 19:06:20 +00:00
|
|
|
}
|
2022-07-01 00:52:03 +00:00
|
|
|
onMouseMove={() => setArrowToShow(Arrow.Right)}
|
|
|
|
type="button"
|
|
|
|
/>
|
|
|
|
)}
|
2022-05-03 23:50:44 +00:00
|
|
|
<div className="StoryViewer__protection StoryViewer__protection--bottom" />
|
2022-04-22 18:36:34 +00:00
|
|
|
<button
|
|
|
|
aria-label={i18n('close')}
|
|
|
|
className="StoryViewer__close-button"
|
|
|
|
onClick={onClose}
|
|
|
|
tabIndex={0}
|
|
|
|
type="button"
|
|
|
|
/>
|
2022-03-04 21:14:52 +00:00
|
|
|
</div>
|
2022-07-25 18:55:44 +00:00
|
|
|
{hasStoryDetailsModal && (
|
|
|
|
<StoryDetailsModal
|
|
|
|
getPreferredBadge={getPreferredBadge}
|
|
|
|
i18n={i18n}
|
|
|
|
onClose={() => setHasStoryDetailsModal(false)}
|
|
|
|
sender={story.sender}
|
|
|
|
sendState={sendState}
|
|
|
|
size={attachment?.size}
|
|
|
|
timestamp={timestamp}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
{hasStoryViewsNRepliesModal && (
|
2022-04-07 21:11:33 +00:00
|
|
|
<StoryViewsNRepliesModal
|
2022-05-10 19:02:21 +00:00
|
|
|
authorTitle={firstName || title}
|
2022-07-25 18:55:44 +00:00
|
|
|
canReply={Boolean(canReply)}
|
2022-04-07 21:11:33 +00:00
|
|
|
getPreferredBadge={getPreferredBadge}
|
|
|
|
i18n={i18n}
|
2022-04-23 03:16:13 +00:00
|
|
|
isGroupStory={isGroupStory}
|
2022-04-07 21:11:33 +00:00
|
|
|
isMyStory={isMe}
|
2022-07-25 18:55:44 +00:00
|
|
|
onClose={() => setHasStoryViewsNRepliesModal(false)}
|
2022-04-07 21:11:33 +00:00
|
|
|
onReact={emoji => {
|
2022-07-06 19:06:20 +00:00
|
|
|
onReactToStory(emoji, story);
|
2022-07-29 00:17:00 +00:00
|
|
|
if (!isGroupStory) {
|
|
|
|
setHasStoryViewsNRepliesModal(false);
|
|
|
|
}
|
2022-05-04 17:43:22 +00:00
|
|
|
setReactionEmoji(emoji);
|
2022-07-12 16:41:41 +00:00
|
|
|
showToast(ToastType.StoryReact);
|
2022-04-07 21:11:33 +00:00
|
|
|
}}
|
|
|
|
onReply={(message, mentions, replyTimestamp) => {
|
2022-04-23 03:16:13 +00:00
|
|
|
if (!isGroupStory) {
|
2022-07-25 18:55:44 +00:00
|
|
|
setHasStoryViewsNRepliesModal(false);
|
2022-04-23 03:16:13 +00:00
|
|
|
}
|
2022-07-06 19:06:20 +00:00
|
|
|
onReplyToStory(message, mentions, replyTimestamp, story);
|
2022-07-12 16:41:41 +00:00
|
|
|
showToast(ToastType.StoryReply);
|
2022-04-07 21:11:33 +00:00
|
|
|
}}
|
|
|
|
onSetSkinTone={onSetSkinTone}
|
|
|
|
onTextTooLong={onTextTooLong}
|
|
|
|
onUseEmoji={onUseEmoji}
|
|
|
|
preferredReactionEmoji={preferredReactionEmoji}
|
|
|
|
recentEmojis={recentEmojis}
|
|
|
|
renderEmojiPicker={renderEmojiPicker}
|
2022-04-15 00:08:46 +00:00
|
|
|
replies={replies}
|
2022-04-07 21:11:33 +00:00
|
|
|
skinTone={skinTone}
|
2022-08-23 18:02:51 +00:00
|
|
|
sortedGroupMembers={group?.sortedGroupMembers}
|
2022-04-07 21:11:33 +00:00
|
|
|
storyPreviewAttachment={attachment}
|
2022-07-01 00:52:03 +00:00
|
|
|
views={views}
|
2022-04-07 21:11:33 +00:00
|
|
|
/>
|
|
|
|
)}
|
2022-04-29 17:43:24 +00:00
|
|
|
{hasConfirmHideStory && (
|
|
|
|
<ConfirmationDialog
|
|
|
|
actions={[
|
|
|
|
{
|
2022-08-11 19:38:24 +00:00
|
|
|
action: () => {
|
|
|
|
onHideStory(id);
|
|
|
|
onClose();
|
|
|
|
},
|
2022-04-29 17:43:24 +00:00
|
|
|
style: 'affirmative',
|
|
|
|
text: i18n('StoryListItem__hide-modal--confirm'),
|
|
|
|
},
|
|
|
|
]}
|
|
|
|
i18n={i18n}
|
|
|
|
onClose={() => {
|
|
|
|
setHasConfirmHideStory(false);
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{i18n('StoryListItem__hide-modal--body', [String(firstName)])}
|
|
|
|
</ConfirmationDialog>
|
|
|
|
)}
|
2022-07-25 18:55:44 +00:00
|
|
|
{confirmDeleteStory && (
|
|
|
|
<ConfirmationDialog
|
|
|
|
actions={[
|
|
|
|
{
|
|
|
|
text: i18n('delete'),
|
|
|
|
action: () => deleteStoryForEveryone(confirmDeleteStory),
|
|
|
|
style: 'negative',
|
|
|
|
},
|
|
|
|
]}
|
|
|
|
i18n={i18n}
|
|
|
|
onClose={() => setConfirmDeleteStory(undefined)}
|
|
|
|
>
|
|
|
|
{i18n('MyStories__delete')}
|
|
|
|
</ConfirmationDialog>
|
|
|
|
)}
|
2022-03-04 21:14:52 +00:00
|
|
|
</div>
|
2022-04-07 21:11:33 +00:00
|
|
|
</FocusTrap>
|
2022-03-04 21:14:52 +00:00
|
|
|
);
|
|
|
|
};
|