Pause story on mouse/space hold

Co-authored-by: Jamie Kyle <113370520+jamiebuilds-signal@users.noreply.github.com>
This commit is contained in:
automated-signal 2024-04-15 15:45:54 -05:00 committed by GitHub
parent 7bd2a84c0a
commit 70319b317a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 93 additions and 9 deletions

View file

@ -43,6 +43,7 @@
&__container {
flex-grow: 1;
overflow: hidden;
outline: none;
}
&__story {

View file

@ -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<JSX.Element | string>) {
return <strong>{parts}</strong>;
@ -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 (
<FocusTrap focusTrapOptions={{ clickOutsideDeactivates: true }}>
<div className="StoryViewer">
<FocusTrap
focusTrapOptions={{
clickOutsideDeactivates: true,
initialFocus: viewerSelector,
}}
>
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
<div
className="StoryViewer"
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
tabIndex={0}
id={viewerId}
onMouseDown={event => {
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}
<div
className="StoryViewer__overlay"
@ -773,15 +836,20 @@ export function StoryViewer({
)}
</div>
</div>
<div className="StoryViewer__meta__playback-controls">
<div
className="StoryViewer__meta__playback-controls"
{...stopPauseBubblingProps}
>
<button
aria-label={
pauseStory
pauseStory || longPress
? i18n('icu:StoryViewer__play')
: i18n('icu:StoryViewer__pause')
}
className={
pauseStory ? 'StoryViewer__play' : 'StoryViewer__pause'
pauseStory || longPress
? 'StoryViewer__play'
: 'StoryViewer__pause'
}
onClick={() => setPauseStory(!pauseStory)}
type="button"
@ -808,7 +876,7 @@ export function StoryViewer({
)}
</div>
</div>
<div className="StoryViewer__progress">
<div className="StoryViewer__progress" {...stopPauseBubblingProps}>
{Array.from(Array(numStories), (_, index) => (
<div className="StoryViewer__progress--container" key={index}>
{currentIndex === index ? (
@ -831,7 +899,7 @@ export function StoryViewer({
</div>
))}
</div>
<div className="StoryViewer__actions">
<div className="StoryViewer__actions" {...stopPauseBubblingProps}>
{sendStatus === ResolvedSendStatus.Failed &&
!wasManuallyRetried && (
<button

View file

@ -1,9 +1,24 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { useMemo } from 'react';
import { useMemo, useState } from 'react';
import { v4 as uuid } from 'uuid';
export function useUniqueId(): string {
return useMemo(() => uuid(), []);
}
let nextElementId = 0;
export function useElementId(
namePrefix: string
): [id: string, selector: string] {
// Prefixed to avoid starting with a number (which is invalid in CSS selectors)
const [id] = useState(() => {
const currentId = nextElementId;
nextElementId += 1;
return `${namePrefix}-${currentId}`;
});
// Return the ID and a selector that can be used in CSS or JS
return [id, `#${id}`] as const;
}