Pause story on mouse/space hold
Co-authored-by: Jamie Kyle <113370520+jamiebuilds-signal@users.noreply.github.com>
This commit is contained in:
parent
7bd2a84c0a
commit
70319b317a
3 changed files with 93 additions and 9 deletions
|
@ -43,6 +43,7 @@
|
|||
&__container {
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&__story {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue