diff --git a/stylesheets/components/StoryViewer.scss b/stylesheets/components/StoryViewer.scss index ed3b8eb7a2..a5248d59d6 100644 --- a/stylesheets/components/StoryViewer.scss +++ b/stylesheets/components/StoryViewer.scss @@ -43,6 +43,7 @@ &__container { flex-grow: 1; overflow: hidden; + outline: none; } &__story { diff --git a/ts/components/StoryViewer.tsx b/ts/components/StoryViewer.tsx index 54c669d0b2..28453ca5d4 100644 --- a/ts/components/StoryViewer.tsx +++ b/ts/components/StoryViewer.tsx @@ -2,6 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import FocusTrap from 'focus-trap-react'; +import type { UIEvent } from 'react'; import React, { useCallback, useEffect, @@ -57,6 +58,7 @@ import { strictAssert } from '../util/assert'; import { MessageBody } from './conversation/MessageBody'; import { RenderLocation } from './conversation/MessageTextRenderer'; import { arrow } from '../util/keyboard'; +import { useElementId } from '../hooks/useUniqueId'; function renderStrong(parts: Array) { return {parts}; @@ -188,6 +190,8 @@ export function StoryViewer({ StoryViewType | undefined >(); + const [viewerId, viewerSelector] = useElementId('StoryViewer'); + const { attachment, bodyRanges, @@ -355,6 +359,24 @@ export function StoryViewer({ }, [story.messageId, storyDuration]); const [pauseStory, setPauseStory] = useState(false); + const [pressing, setPressing] = useState(false); + const [longPress, setLongPress] = useState(false); + + useEffect(() => { + let timer: NodeJS.Timeout | undefined; + if (pressing) { + timer = setTimeout(() => { + setLongPress(true); + }, 200); + } else { + setLongPress(false); + } + return () => { + if (timer) { + clearTimeout(timer); + } + }; + }, [pressing]); useEffect(() => { if (!isWindowActive) { @@ -373,7 +395,8 @@ export function StoryViewer({ hasExpandedCaption || isShowingContextMenu || pauseStory || - Boolean(reactionEmoji); + Boolean(reactionEmoji) || + pressing; useEffect(() => { if (shouldPauseViewing) { @@ -593,9 +616,49 @@ export function StoryViewer({ retryMessageSend(messageId); } + function isDescendentEvent(event: UIEvent) { + // Can occur when the user clicks on the overlay of an open modal + return event.currentTarget.contains(event.target as Node); + } + + // .StoryViewer has events to pause the story, but certain elements it + // contains should not trigger that behavior. + const stopPauseBubblingProps = { + onMouseDown: (event: UIEvent) => event.stopPropagation(), + onKeyDown: (event: UIEvent) => event.stopPropagation(), + }; + return ( - -
+ + {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */} +
{ + if (isDescendentEvent(event)) { + setPressing(true); + } + }} + onDragStart={() => setPressing(false)} + onMouseUp={() => setPressing(false)} + onKeyDown={event => { + if (isDescendentEvent(event) && event.code === 'Space') { + setPressing(true); + } + }} + onKeyUp={event => { + if (event.code === 'Space') { + setPressing(false); + } + }} + > {alertElement}
-
+
-
+
{Array.from(Array(numStories), (_, index) => (
{currentIndex === index ? ( @@ -831,7 +899,7 @@ export function StoryViewer({
))}
-
+
{sendStatus === ResolvedSendStatus.Failed && !wasManuallyRetried && (