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 {
|
&__container {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__story {
|
&__story {
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import FocusTrap from 'focus-trap-react';
|
import FocusTrap from 'focus-trap-react';
|
||||||
|
import type { UIEvent } from 'react';
|
||||||
import React, {
|
import React, {
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
@ -57,6 +58,7 @@ import { strictAssert } from '../util/assert';
|
||||||
import { MessageBody } from './conversation/MessageBody';
|
import { MessageBody } from './conversation/MessageBody';
|
||||||
import { RenderLocation } from './conversation/MessageTextRenderer';
|
import { RenderLocation } from './conversation/MessageTextRenderer';
|
||||||
import { arrow } from '../util/keyboard';
|
import { arrow } from '../util/keyboard';
|
||||||
|
import { useElementId } from '../hooks/useUniqueId';
|
||||||
|
|
||||||
function renderStrong(parts: Array<JSX.Element | string>) {
|
function renderStrong(parts: Array<JSX.Element | string>) {
|
||||||
return <strong>{parts}</strong>;
|
return <strong>{parts}</strong>;
|
||||||
|
@ -188,6 +190,8 @@ export function StoryViewer({
|
||||||
StoryViewType | undefined
|
StoryViewType | undefined
|
||||||
>();
|
>();
|
||||||
|
|
||||||
|
const [viewerId, viewerSelector] = useElementId('StoryViewer');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
attachment,
|
attachment,
|
||||||
bodyRanges,
|
bodyRanges,
|
||||||
|
@ -355,6 +359,24 @@ export function StoryViewer({
|
||||||
}, [story.messageId, storyDuration]);
|
}, [story.messageId, storyDuration]);
|
||||||
|
|
||||||
const [pauseStory, setPauseStory] = useState(false);
|
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(() => {
|
useEffect(() => {
|
||||||
if (!isWindowActive) {
|
if (!isWindowActive) {
|
||||||
|
@ -373,7 +395,8 @@ export function StoryViewer({
|
||||||
hasExpandedCaption ||
|
hasExpandedCaption ||
|
||||||
isShowingContextMenu ||
|
isShowingContextMenu ||
|
||||||
pauseStory ||
|
pauseStory ||
|
||||||
Boolean(reactionEmoji);
|
Boolean(reactionEmoji) ||
|
||||||
|
pressing;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (shouldPauseViewing) {
|
if (shouldPauseViewing) {
|
||||||
|
@ -593,9 +616,49 @@ export function StoryViewer({
|
||||||
retryMessageSend(messageId);
|
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 (
|
return (
|
||||||
<FocusTrap focusTrapOptions={{ clickOutsideDeactivates: true }}>
|
<FocusTrap
|
||||||
<div className="StoryViewer">
|
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}
|
{alertElement}
|
||||||
<div
|
<div
|
||||||
className="StoryViewer__overlay"
|
className="StoryViewer__overlay"
|
||||||
|
@ -773,15 +836,20 @@ export function StoryViewer({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="StoryViewer__meta__playback-controls">
|
<div
|
||||||
|
className="StoryViewer__meta__playback-controls"
|
||||||
|
{...stopPauseBubblingProps}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
aria-label={
|
aria-label={
|
||||||
pauseStory
|
pauseStory || longPress
|
||||||
? i18n('icu:StoryViewer__play')
|
? i18n('icu:StoryViewer__play')
|
||||||
: i18n('icu:StoryViewer__pause')
|
: i18n('icu:StoryViewer__pause')
|
||||||
}
|
}
|
||||||
className={
|
className={
|
||||||
pauseStory ? 'StoryViewer__play' : 'StoryViewer__pause'
|
pauseStory || longPress
|
||||||
|
? 'StoryViewer__play'
|
||||||
|
: 'StoryViewer__pause'
|
||||||
}
|
}
|
||||||
onClick={() => setPauseStory(!pauseStory)}
|
onClick={() => setPauseStory(!pauseStory)}
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -808,7 +876,7 @@ export function StoryViewer({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="StoryViewer__progress">
|
<div className="StoryViewer__progress" {...stopPauseBubblingProps}>
|
||||||
{Array.from(Array(numStories), (_, index) => (
|
{Array.from(Array(numStories), (_, index) => (
|
||||||
<div className="StoryViewer__progress--container" key={index}>
|
<div className="StoryViewer__progress--container" key={index}>
|
||||||
{currentIndex === index ? (
|
{currentIndex === index ? (
|
||||||
|
@ -831,7 +899,7 @@ export function StoryViewer({
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="StoryViewer__actions">
|
<div className="StoryViewer__actions" {...stopPauseBubblingProps}>
|
||||||
{sendStatus === ResolvedSendStatus.Failed &&
|
{sendStatus === ResolvedSendStatus.Failed &&
|
||||||
!wasManuallyRetried && (
|
!wasManuallyRetried && (
|
||||||
<button
|
<button
|
||||||
|
|
|
@ -1,9 +1,24 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
export function useUniqueId(): string {
|
export function useUniqueId(): string {
|
||||||
return useMemo(() => uuid(), []);
|
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…
Add table
Reference in a new issue