// Copyright 2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import React, { useCallback, useEffect, useState } from 'react'; import { useSpring, animated, to } from '@react-spring/web'; import type { BodyRangeType, LocalizerType } from '../types/Util'; 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'; import type { StoryViewType } from './StoryListItem'; import { Avatar, AvatarSize } from './Avatar'; import { Intl } from './Intl'; import { MessageTimestamp } from './conversation/MessageTimestamp'; import { StoryViewsNRepliesModal } from './StoryViewsNRepliesModal'; import { getAvatarColor } from '../types/Colors'; import { useEscapeHandling } from '../hooks/useEscapeHandling'; const STORY_DURATION = 5000; export type PropsType = { getPreferredBadge: PreferredBadgeSelectorType; group?: ConversationType; i18n: LocalizerType; markStoryRead: (mId: string) => unknown; onClose: () => unknown; onNextUserStories: () => unknown; onPrevUserStories: () => unknown; onSetSkinTone: (tone: number) => unknown; onTextTooLong: () => unknown; onReactToStory: (emoji: string, story: StoryViewType) => unknown; onReplyToStory: ( message: string, mentions: Array, timestamp: number, story: StoryViewType ) => unknown; onUseEmoji: (_: EmojiPickDataType) => unknown; preferredReactionEmoji: Array; recentEmojis?: Array; replies?: number; renderEmojiPicker: (props: RenderEmojiPickerProps) => JSX.Element; skinTone?: number; stories: Array; views?: number; }; export const StoryViewer = ({ getPreferredBadge, group, i18n, markStoryRead, onClose, onNextUserStories, onPrevUserStories, onReactToStory, onReplyToStory, onSetSkinTone, onTextTooLong, onUseEmoji, preferredReactionEmoji, recentEmojis, renderEmojiPicker, replies, skinTone, stories, views, }: PropsType): JSX.Element => { const [currentStoryIndex, setCurrentStoryIndex] = useState(0); const visibleStory = stories[currentStoryIndex]; const { acceptedMessageRequest, avatarPath, color, isMe, name, profileName, sharedGroupNames, title, } = visibleStory.sender; const [hasReplyModal, setHasReplyModal] = useState(false); const onEscape = useCallback(() => { if (hasReplyModal) { setHasReplyModal(false); } else { onClose(); } }, [hasReplyModal, onClose]); useEscapeHandling(onEscape); const showNextStory = useCallback(() => { // Either we show the next story in the current user's stories or we ask // for the next user's stories. if (currentStoryIndex < stories.length - 1) { setCurrentStoryIndex(currentStoryIndex + 1); } else { onNextUserStories(); } }, [currentStoryIndex, onNextUserStories, stories.length]); const showPrevStory = useCallback(() => { // Either we show the previous story in the current user's stories or we ask // for the prior user's stories. if (currentStoryIndex === 0) { onPrevUserStories(); } else { setCurrentStoryIndex(currentStoryIndex - 1); } }, [currentStoryIndex, onPrevUserStories]); const [styles, spring] = useSpring(() => ({ config: { duration: STORY_DURATION, }, from: { width: 0 }, to: { width: 100 }, loop: true, })); // Adding "currentStoryIndex" to the dependency list here to explcitly signal // that this useEffect should run whenever the story changes. useEffect(() => { spring.start({ from: { width: 0 }, to: { width: 100 }, onRest: showNextStory, }); }, [currentStoryIndex, showNextStory, spring]); useEffect(() => { if (hasReplyModal) { spring.pause(); } else { spring.resume(); } }, [hasReplyModal, spring]); useEffect(() => { markStoryRead(visibleStory.messageId); }, [markStoryRead, visibleStory.messageId]); const navigateStories = useCallback( (ev: KeyboardEvent) => { if (ev.key === 'ArrowRight') { showNextStory(); ev.preventDefault(); ev.stopPropagation(); } else if (ev.key === 'ArrowLeft') { showPrevStory(); ev.preventDefault(); ev.stopPropagation(); } }, [showPrevStory, showNextStory] ); useEffect(() => { document.addEventListener('keydown', navigateStories); return () => { document.removeEventListener('keydown', navigateStories); }; }, [navigateStories]); return (
)}
{hasReplyModal && ( setHasReplyModal(false)} onReact={emoji => { onReactToStory(emoji, visibleStory); }} onReply={(message, mentions, timestamp) => { setHasReplyModal(false); onReplyToStory(message, mentions, timestamp, visibleStory); }} onSetSkinTone={onSetSkinTone} onTextTooLong={onTextTooLong} onUseEmoji={onUseEmoji} preferredReactionEmoji={preferredReactionEmoji} recentEmojis={recentEmojis} renderEmojiPicker={renderEmojiPicker} replies={[]} skinTone={skinTone} storyPreviewAttachment={visibleStory.attachment} views={[]} /> )}
); };