Adds playback bar to story viewer
This commit is contained in:
parent
9817946afc
commit
85c8ff76dc
17 changed files with 291 additions and 125 deletions
|
@ -7018,6 +7018,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"StoryViewer__pause": {
|
||||||
|
"message": "Pause",
|
||||||
|
"description": "Aria label for pausing a story"
|
||||||
|
},
|
||||||
|
"StoryViewer__play": {
|
||||||
|
"message": "Play",
|
||||||
|
"description": "Aria label for playing a story"
|
||||||
|
},
|
||||||
"StoryViewer__reply": {
|
"StoryViewer__reply": {
|
||||||
"message": "Reply",
|
"message": "Reply",
|
||||||
"description": "Button label to reply to a story"
|
"description": "Button label to reply to a story"
|
||||||
|
@ -7026,6 +7034,14 @@
|
||||||
"message": "Reply to Group",
|
"message": "Reply to Group",
|
||||||
"description": "Button label to reply to a group story"
|
"description": "Button label to reply to a group story"
|
||||||
},
|
},
|
||||||
|
"StoryViewer__mute": {
|
||||||
|
"message": "Mute",
|
||||||
|
"description": "Aria label for muting stories"
|
||||||
|
},
|
||||||
|
"StoryViewer__unmute": {
|
||||||
|
"message": "Unmute",
|
||||||
|
"description": "Aria label for unmuting stories"
|
||||||
|
},
|
||||||
"StoryViewsNRepliesModal__no-replies": {
|
"StoryViewsNRepliesModal__no-replies": {
|
||||||
"message": "No replies yet",
|
"message": "No replies yet",
|
||||||
"description": "Placeholder text for when there are no replies"
|
"description": "Placeholder text for when there are no replies"
|
||||||
|
|
1
images/icons/v2/pause_solid_20.svg
Normal file
1
images/icons/v2/pause_solid_20.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg fill="none" height="20" viewBox="0 0 20 20" width="20" xmlns="http://www.w3.org/2000/svg"><path clip-rule="evenodd" d="m6.5 3c-.82843 0-1.5.67157-1.5 1.5v11c0 .8284.67157 1.5 1.5 1.5s1.5-.6716 1.5-1.5v-11c0-.82843-.67157-1.5-1.5-1.5zm7 0c-.8284 0-1.5.67157-1.5 1.5v11c0 .8284.6716 1.5 1.5 1.5s1.5-.6716 1.5-1.5v-11c0-.82843-.6716-1.5-1.5-1.5z" fill="#000" fill-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 388 B |
1
images/icons/v2/play_solid_20.svg
Normal file
1
images/icons/v2/play_solid_20.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg fill="none" height="20" viewBox="0 0 20 20" width="20" xmlns="http://www.w3.org/2000/svg"><path d="m15.5762 9.20477c.1244.0515.2317.14425.3074.26577.0758.12152.1164.26599.1164.41401 0 .14805-.0406.29245-.1164.41405-.0757.1215-.183.2142-.3074.2657l-10.51991 6.7979c-.58014.3752-1.05629.0698-1.05629-.678v-13.59569c0-.74777.47452-1.05322 1.05629-.67797z" fill="#000"/></svg>
|
After Width: | Height: | Size: 377 B |
1
images/icons/v2/speaker-off-solid-20.svg
Normal file
1
images/icons/v2/speaker-off-solid-20.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg fill="none" height="20" viewBox="0 0 20 20" width="20" xmlns="http://www.w3.org/2000/svg"><path d="m18.775 7.94166-.8833-.88333-2.0584 2.0575-2.0583-2.0575-.8833.88333 2.0575 2.05833-2.0575 2.05831.8833.8834 2.0583-2.0575 2.0584 2.0575.8833-.8834-2.0575-2.05831zm-7.1083-5.3275v14.77164c.0001.0809-.0234.1601-.0676.2279s-.1071.1213-.1812.1539c-.074.0326-.156.043-.2358.0298-.0799-.0132-.1541-.0494-.2138-.1041l-4.30164-3.9433h-3.33333c-.44203 0-.86595-.1756-1.17851-.4882-.31256-.3125-.48816-.7364-.48816-1.1785v-4.16664c0-.44203.1756-.86595.48816-1.17851s.73648-.48816 1.17851-.48816h3.33333l4.30164-3.94333c.0597-.0547.1339-.09086.2138-.10405.0798-.01319.1618-.00285.2358.02976.0741.03261.137.08608.1812.15388s.0677.14699.0676.22791z" fill="#000"/></svg>
|
After Width: | Height: | Size: 761 B |
1
images/icons/v2/speaker-on-solid-20.svg
Normal file
1
images/icons/v2/speaker-on-solid-20.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg fill="none" height="20" viewBox="0 0 20 20" width="20" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><clipPath id="a"><path d="m0 0h20v20h-20z"/></clipPath><g clip-path="url(#a)"><path d="m16.4817 4.51833-.8834.88333c1.4825 1.48599 2.315 3.49932 2.315 5.59834 0 2.099-.8325 4.1123-2.315 5.5983l.8834.8834c1.7165-1.7204 2.6805-4.0514 2.6805-6.4817 0-2.43027-.964-4.7613-2.6805-6.48167zm-.6484 6.48167c.0019-.7664-.1481-1.52561-.4415-2.23365s-.7242-1.35089-1.2676-1.89136l-.8834.88417c.8595.85954 1.3424 2.02529 1.3424 3.24084 0 1.2155-.4829 2.3813-1.3424 3.2408l.8834.8834c.5434-.5403.9743-1.183 1.2677-1.8909s.4434-1.467.4414-2.2333zm-4.1666-7.38584v14.77164c.0001.0809-.0234.1601-.0676.2279s-.1071.1213-.1812.1539c-.074.0326-.156.043-.2358.0298-.0799-.0132-.1541-.0494-.2138-.1041l-4.30163-3.9433h-3.33334c-.44202 0-.86595-.1756-1.17851-.4882-.31256-.3125-.48815-.7364-.48815-1.1785v-4.16664c0-.44203.17559-.86595.48815-1.17851s.73649-.48816 1.17851-.48816h3.33334l4.30163-3.94333c.0597-.0547.1339-.09086.2138-.10405.0798-.01319.1618-.00285.2358.02976.0741.03261.137.08608.1812.15388s.0677.14699.0676.22791z" fill="#000"/></g></svg>
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -35,24 +35,6 @@
|
||||||
z-index: $z-index-above-above-base;
|
z-index: $z-index-above-above-base;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__more {
|
|
||||||
@include button-reset;
|
|
||||||
height: 24px;
|
|
||||||
position: absolute;
|
|
||||||
right: 80px;
|
|
||||||
top: var(--title-bar-drag-area-height);
|
|
||||||
width: 24px;
|
|
||||||
z-index: $z-index-above-base;
|
|
||||||
|
|
||||||
@include color-svg('../images/icons/v2/more-horiz-24.svg', $color-white);
|
|
||||||
|
|
||||||
@include keyboard-mode {
|
|
||||||
&:focus {
|
|
||||||
background-color: $color-ultramarine;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__container {
|
&__container {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -89,6 +71,16 @@
|
||||||
@include font-body-2;
|
@include font-body-2;
|
||||||
color: $color-white-alpha-60;
|
color: $color-white-alpha-60;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__playback-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__playback-controls {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__caption {
|
&__caption {
|
||||||
|
@ -154,6 +146,76 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__more {
|
||||||
|
@include button-reset;
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
|
|
||||||
|
@include color-svg('../images/icons/v2/more-horiz-24.svg', $color-white);
|
||||||
|
|
||||||
|
@include keyboard-mode {
|
||||||
|
&:focus {
|
||||||
|
background-color: $color-black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__mute {
|
||||||
|
@include button-reset;
|
||||||
|
height: 20px;
|
||||||
|
margin: 0 24px;
|
||||||
|
width: 20px;
|
||||||
|
@include color-svg(
|
||||||
|
'../images/icons/v2/speaker-on-solid-20.svg',
|
||||||
|
$color-white
|
||||||
|
);
|
||||||
|
@include keyboard-mode {
|
||||||
|
&:focus {
|
||||||
|
background-color: $color-white-alpha-80;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__pause {
|
||||||
|
@include button-reset;
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
@include color-svg('../images/icons/v2/pause_solid_20.svg', $color-white);
|
||||||
|
@include keyboard-mode {
|
||||||
|
&:focus {
|
||||||
|
background-color: $color-white-alpha-80;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__play {
|
||||||
|
@include button-reset;
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
@include color-svg('../images/icons/v2/play_solid_20.svg', $color-white);
|
||||||
|
@include keyboard-mode {
|
||||||
|
&:focus {
|
||||||
|
background-color: $color-white-alpha-80;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__unmute {
|
||||||
|
@include button-reset;
|
||||||
|
height: 20px;
|
||||||
|
margin: 0 18px;
|
||||||
|
width: 20px;
|
||||||
|
@include color-svg(
|
||||||
|
'../images/icons/v2/speaker-off-solid-20.svg',
|
||||||
|
$color-white
|
||||||
|
);
|
||||||
|
@include keyboard-mode {
|
||||||
|
&:focus {
|
||||||
|
background-color: $color-white-alpha-80;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__progress {
|
&__progress {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
|
|
|
@ -49,6 +49,7 @@ export function ContextMenuPopper<T>({
|
||||||
onClose,
|
onClose,
|
||||||
referenceElement,
|
referenceElement,
|
||||||
title,
|
title,
|
||||||
|
theme,
|
||||||
value,
|
value,
|
||||||
}: ContextMenuPropsType<T>): JSX.Element | null {
|
}: ContextMenuPropsType<T>): JSX.Element | null {
|
||||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(
|
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(
|
||||||
|
@ -84,49 +85,54 @@ export function ContextMenuPopper<T>({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={theme ? themeClassName(theme) : undefined}>
|
||||||
className="ContextMenu__popper"
|
<div
|
||||||
ref={setPopperElement}
|
className="ContextMenu__popper"
|
||||||
style={styles.popper}
|
ref={setPopperElement}
|
||||||
{...attributes.popper}
|
style={styles.popper}
|
||||||
>
|
{...attributes.popper}
|
||||||
{title && <div className="ContextMenu__title">{title}</div>}
|
>
|
||||||
{menuOptions.map((option, index) => (
|
{title && <div className="ContextMenu__title">{title}</div>}
|
||||||
<button
|
{menuOptions.map((option, index) => (
|
||||||
aria-label={option.label}
|
<button
|
||||||
className={classNames({
|
aria-label={option.label}
|
||||||
ContextMenu__option: true,
|
className={classNames({
|
||||||
'ContextMenu__option--focused': focusedIndex === index,
|
ContextMenu__option: true,
|
||||||
})}
|
'ContextMenu__option--focused': focusedIndex === index,
|
||||||
key={option.label}
|
})}
|
||||||
type="button"
|
key={option.label}
|
||||||
onClick={() => {
|
type="button"
|
||||||
option.onClick(option.value);
|
onClick={() => {
|
||||||
onClose();
|
option.onClick(option.value);
|
||||||
}}
|
onClose();
|
||||||
>
|
}}
|
||||||
<div className="ContextMenu__option--container">
|
>
|
||||||
{option.icon && (
|
<div className="ContextMenu__option--container">
|
||||||
<div
|
{option.icon && (
|
||||||
className={classNames('ContextMenu__option--icon', option.icon)}
|
<div
|
||||||
/>
|
className={classNames(
|
||||||
)}
|
'ContextMenu__option--icon',
|
||||||
<div>
|
option.icon
|
||||||
<div className="ContextMenu__option--title">{option.label}</div>
|
)}
|
||||||
{option.description && (
|
/>
|
||||||
<div className="ContextMenu__option--description">
|
|
||||||
{option.description}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
<div>
|
||||||
|
<div className="ContextMenu__option--title">{option.label}</div>
|
||||||
|
{option.description && (
|
||||||
|
<div className="ContextMenu__option--description">
|
||||||
|
{option.description}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{typeof value !== 'undefined' &&
|
||||||
{typeof value !== 'undefined' &&
|
typeof option.value !== 'undefined' &&
|
||||||
typeof option.value !== 'undefined' &&
|
value === option.value ? (
|
||||||
value === option.value ? (
|
<div className="ContextMenu__option--selected" />
|
||||||
<div className="ContextMenu__option--selected" />
|
) : null}
|
||||||
) : null}
|
</button>
|
||||||
</button>
|
))}
|
||||||
))}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ export type PropsType = {
|
||||||
readonly attachment?: AttachmentType;
|
readonly attachment?: AttachmentType;
|
||||||
readonly children?: ReactNode;
|
readonly children?: ReactNode;
|
||||||
readonly i18n: LocalizerType;
|
readonly i18n: LocalizerType;
|
||||||
|
readonly isMuted?: boolean;
|
||||||
readonly isPaused?: boolean;
|
readonly isPaused?: boolean;
|
||||||
readonly isThumbnail?: boolean;
|
readonly isThumbnail?: boolean;
|
||||||
readonly label: string;
|
readonly label: string;
|
||||||
|
@ -37,6 +38,7 @@ export const StoryImage = ({
|
||||||
attachment,
|
attachment,
|
||||||
children,
|
children,
|
||||||
i18n,
|
i18n,
|
||||||
|
isMuted,
|
||||||
isPaused,
|
isPaused,
|
||||||
isThumbnail,
|
isThumbnail,
|
||||||
label,
|
label,
|
||||||
|
@ -106,6 +108,7 @@ export const StoryImage = ({
|
||||||
controls={false}
|
controls={false}
|
||||||
key={attachment.url}
|
key={attachment.url}
|
||||||
loop={shouldLoop}
|
loop={shouldLoop}
|
||||||
|
muted={isMuted}
|
||||||
ref={videoRef}
|
ref={videoRef}
|
||||||
>
|
>
|
||||||
<source src={attachment.url} />
|
<source src={attachment.url} />
|
||||||
|
|
|
@ -23,6 +23,7 @@ function getDefaultProps(): PropsType {
|
||||||
conversationId: sender.id,
|
conversationId: sender.id,
|
||||||
getPreferredBadge: () => undefined,
|
getPreferredBadge: () => undefined,
|
||||||
group: undefined,
|
group: undefined,
|
||||||
|
hasAllStoriesMuted: false,
|
||||||
i18n,
|
i18n,
|
||||||
loadStoryReplies: action('loadStoryReplies'),
|
loadStoryReplies: action('loadStoryReplies'),
|
||||||
markStoryRead: action('markStoryRead'),
|
markStoryRead: action('markStoryRead'),
|
||||||
|
@ -51,6 +52,7 @@ function getDefaultProps(): PropsType {
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
toggleHasAllStoriesMuted: action('toggleHasAllStoriesMuted'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,6 +155,7 @@ story.add('Caption', () => (
|
||||||
story.add('Long Caption', () => (
|
story.add('Long Caption', () => (
|
||||||
<StoryViewer
|
<StoryViewer
|
||||||
{...getDefaultProps()}
|
{...getDefaultProps()}
|
||||||
|
hasAllStoriesMuted
|
||||||
stories={[
|
stories={[
|
||||||
{
|
{
|
||||||
attachment: fakeAttachment({
|
attachment: fakeAttachment({
|
||||||
|
|
|
@ -26,6 +26,7 @@ import { Intl } from './Intl';
|
||||||
import { MessageTimestamp } from './conversation/MessageTimestamp';
|
import { MessageTimestamp } from './conversation/MessageTimestamp';
|
||||||
import { StoryImage } from './StoryImage';
|
import { StoryImage } from './StoryImage';
|
||||||
import { StoryViewsNRepliesModal } from './StoryViewsNRepliesModal';
|
import { StoryViewsNRepliesModal } from './StoryViewsNRepliesModal';
|
||||||
|
import { Theme } from '../util/theme';
|
||||||
import { getAvatarColor } from '../types/Colors';
|
import { getAvatarColor } from '../types/Colors';
|
||||||
import { getStoryBackground } from '../util/getStoryBackground';
|
import { getStoryBackground } from '../util/getStoryBackground';
|
||||||
import { getStoryDuration } from '../util/getStoryDuration';
|
import { getStoryDuration } from '../util/getStoryDuration';
|
||||||
|
@ -47,6 +48,7 @@ export type PropsType = {
|
||||||
| 'sharedGroupNames'
|
| 'sharedGroupNames'
|
||||||
| 'title'
|
| 'title'
|
||||||
>;
|
>;
|
||||||
|
hasAllStoriesMuted: boolean;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
loadStoryReplies: (conversationId: string, messageId: string) => unknown;
|
loadStoryReplies: (conversationId: string, messageId: string) => unknown;
|
||||||
markStoryRead: (mId: string) => unknown;
|
markStoryRead: (mId: string) => unknown;
|
||||||
|
@ -72,6 +74,7 @@ export type PropsType = {
|
||||||
replyState?: ReplyStateType;
|
replyState?: ReplyStateType;
|
||||||
skinTone?: number;
|
skinTone?: number;
|
||||||
stories: Array<StoryViewType>;
|
stories: Array<StoryViewType>;
|
||||||
|
toggleHasAllStoriesMuted: () => unknown;
|
||||||
views?: Array<string>;
|
views?: Array<string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -90,6 +93,7 @@ export const StoryViewer = ({
|
||||||
conversationId,
|
conversationId,
|
||||||
getPreferredBadge,
|
getPreferredBadge,
|
||||||
group,
|
group,
|
||||||
|
hasAllStoriesMuted,
|
||||||
i18n,
|
i18n,
|
||||||
loadStoryReplies,
|
loadStoryReplies,
|
||||||
markStoryRead,
|
markStoryRead,
|
||||||
|
@ -110,6 +114,7 @@ export const StoryViewer = ({
|
||||||
replyState,
|
replyState,
|
||||||
skinTone,
|
skinTone,
|
||||||
stories,
|
stories,
|
||||||
|
toggleHasAllStoriesMuted,
|
||||||
views,
|
views,
|
||||||
}: PropsType): JSX.Element => {
|
}: PropsType): JSX.Element => {
|
||||||
const [currentStoryIndex, setCurrentStoryIndex] = useState(0);
|
const [currentStoryIndex, setCurrentStoryIndex] = useState(0);
|
||||||
|
@ -261,11 +266,14 @@ export const StoryViewer = ({
|
||||||
};
|
};
|
||||||
}, [currentStoryIndex, spring, storyDuration]);
|
}, [currentStoryIndex, spring, storyDuration]);
|
||||||
|
|
||||||
|
const [pauseStory, setPauseStory] = useState(false);
|
||||||
|
|
||||||
const shouldPauseViewing =
|
const shouldPauseViewing =
|
||||||
hasConfirmHideStory ||
|
hasConfirmHideStory ||
|
||||||
hasExpandedCaption ||
|
hasExpandedCaption ||
|
||||||
hasReplyModal ||
|
hasReplyModal ||
|
||||||
isShowingContextMenu ||
|
isShowingContextMenu ||
|
||||||
|
pauseStory ||
|
||||||
Boolean(reactionEmoji);
|
Boolean(reactionEmoji);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -388,6 +396,7 @@ export const StoryViewer = ({
|
||||||
attachment={attachment}
|
attachment={attachment}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
isPaused={shouldPauseViewing}
|
isPaused={shouldPauseViewing}
|
||||||
|
isMuted={hasAllStoriesMuted}
|
||||||
label={i18n('lightboxImageAlt')}
|
label={i18n('lightboxImageAlt')}
|
||||||
moduleClassName="StoryViewer__story"
|
moduleClassName="StoryViewer__story"
|
||||||
queueStoryDownload={queueStoryDownload}
|
queueStoryDownload={queueStoryDownload}
|
||||||
|
@ -436,50 +445,90 @@ export const StoryViewer = ({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Avatar
|
<div className="StoryViewer__meta__playback-bar">
|
||||||
acceptedMessageRequest={acceptedMessageRequest}
|
<div>
|
||||||
avatarPath={avatarPath}
|
<Avatar
|
||||||
badge={undefined}
|
acceptedMessageRequest={acceptedMessageRequest}
|
||||||
color={getAvatarColor(color)}
|
avatarPath={avatarPath}
|
||||||
conversationType="direct"
|
badge={undefined}
|
||||||
i18n={i18n}
|
color={getAvatarColor(color)}
|
||||||
isMe={Boolean(isMe)}
|
conversationType="direct"
|
||||||
name={name}
|
i18n={i18n}
|
||||||
profileName={profileName}
|
isMe={Boolean(isMe)}
|
||||||
sharedGroupNames={sharedGroupNames}
|
name={name}
|
||||||
size={AvatarSize.TWENTY_EIGHT}
|
profileName={profileName}
|
||||||
title={title}
|
sharedGroupNames={sharedGroupNames}
|
||||||
/>
|
size={AvatarSize.TWENTY_EIGHT}
|
||||||
{group && (
|
title={title}
|
||||||
<Avatar
|
/>
|
||||||
acceptedMessageRequest={group.acceptedMessageRequest}
|
{group && (
|
||||||
avatarPath={group.avatarPath}
|
<Avatar
|
||||||
badge={undefined}
|
acceptedMessageRequest={group.acceptedMessageRequest}
|
||||||
className="StoryViewer__meta--group-avatar"
|
avatarPath={group.avatarPath}
|
||||||
color={getAvatarColor(group.color)}
|
badge={undefined}
|
||||||
conversationType="group"
|
className="StoryViewer__meta--group-avatar"
|
||||||
i18n={i18n}
|
color={getAvatarColor(group.color)}
|
||||||
isMe={false}
|
conversationType="group"
|
||||||
name={group.name}
|
i18n={i18n}
|
||||||
profileName={group.profileName}
|
isMe={false}
|
||||||
sharedGroupNames={group.sharedGroupNames}
|
name={group.name}
|
||||||
size={AvatarSize.TWENTY_EIGHT}
|
profileName={group.profileName}
|
||||||
title={group.title}
|
sharedGroupNames={group.sharedGroupNames}
|
||||||
/>
|
size={AvatarSize.TWENTY_EIGHT}
|
||||||
)}
|
title={group.title}
|
||||||
<div className="StoryViewer__meta--title">
|
/>
|
||||||
{group
|
)}
|
||||||
? i18n('Stories__from-to-group', {
|
<div className="StoryViewer__meta--title">
|
||||||
name: title,
|
{group
|
||||||
group: group.title,
|
? i18n('Stories__from-to-group', {
|
||||||
})
|
name: title,
|
||||||
: title}
|
group: group.title,
|
||||||
|
})
|
||||||
|
: title}
|
||||||
|
</div>
|
||||||
|
<MessageTimestamp
|
||||||
|
i18n={i18n}
|
||||||
|
isRelativeTime
|
||||||
|
module="StoryViewer__meta--timestamp"
|
||||||
|
timestamp={timestamp}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="StoryViewer__meta__playback-controls">
|
||||||
|
<button
|
||||||
|
aria-label={
|
||||||
|
pauseStory
|
||||||
|
? i18n('StoryViewer__play')
|
||||||
|
: i18n('StoryViewer__pause')
|
||||||
|
}
|
||||||
|
className={
|
||||||
|
pauseStory ? 'StoryViewer__play' : 'StoryViewer__pause'
|
||||||
|
}
|
||||||
|
onClick={() => setPauseStory(!pauseStory)}
|
||||||
|
type="button"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
aria-label={
|
||||||
|
hasAllStoriesMuted
|
||||||
|
? i18n('StoryViewer__unmute')
|
||||||
|
: i18n('StoryViewer__mute')
|
||||||
|
}
|
||||||
|
className={
|
||||||
|
hasAllStoriesMuted
|
||||||
|
? 'StoryViewer__unmute'
|
||||||
|
: 'StoryViewer__mute'
|
||||||
|
}
|
||||||
|
onClick={toggleHasAllStoriesMuted}
|
||||||
|
type="button"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
aria-label={i18n('MyStories__more')}
|
||||||
|
className="StoryViewer__more"
|
||||||
|
onClick={() => setIsShowingContextMenu(true)}
|
||||||
|
ref={setReferenceElement}
|
||||||
|
type="button"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<MessageTimestamp
|
|
||||||
i18n={i18n}
|
|
||||||
module="StoryViewer__meta--timestamp"
|
|
||||||
timestamp={timestamp}
|
|
||||||
/>
|
|
||||||
<div className="StoryViewer__progress">
|
<div className="StoryViewer__progress">
|
||||||
{stories.map((story, index) => (
|
{stories.map((story, index) => (
|
||||||
<div
|
<div
|
||||||
|
@ -571,14 +620,6 @@ export const StoryViewer = ({
|
||||||
type="button"
|
type="button"
|
||||||
/>
|
/>
|
||||||
<div className="StoryViewer__protection StoryViewer__protection--bottom" />
|
<div className="StoryViewer__protection StoryViewer__protection--bottom" />
|
||||||
<button
|
|
||||||
aria-label={i18n('MyStories__more')}
|
|
||||||
className="StoryViewer__more"
|
|
||||||
onClick={() => setIsShowingContextMenu(true)}
|
|
||||||
ref={setReferenceElement}
|
|
||||||
tabIndex={0}
|
|
||||||
type="button"
|
|
||||||
/>
|
|
||||||
<button
|
<button
|
||||||
aria-label={i18n('close')}
|
aria-label={i18n('close')}
|
||||||
className="StoryViewer__close-button"
|
className="StoryViewer__close-button"
|
||||||
|
@ -612,11 +653,8 @@ export const StoryViewer = ({
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
onClose={() => setIsShowingContextMenu(false)}
|
onClose={() => setIsShowingContextMenu(false)}
|
||||||
popperOptions={{
|
|
||||||
placement: 'bottom',
|
|
||||||
strategy: 'absolute',
|
|
||||||
}}
|
|
||||||
referenceElement={referenceElement}
|
referenceElement={referenceElement}
|
||||||
|
theme={Theme.Dark}
|
||||||
/>
|
/>
|
||||||
{hasReplyModal && canReply && (
|
{hasReplyModal && canReply && (
|
||||||
<StoryViewsNRepliesModal
|
<StoryViewsNRepliesModal
|
||||||
|
|
|
@ -15,6 +15,7 @@ export type Props = {
|
||||||
deletedForEveryone?: boolean;
|
deletedForEveryone?: boolean;
|
||||||
direction?: 'incoming' | 'outgoing';
|
direction?: 'incoming' | 'outgoing';
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
isRelativeTime?: boolean;
|
||||||
module?: string;
|
module?: string;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
withImageNoCaption?: boolean;
|
withImageNoCaption?: boolean;
|
||||||
|
@ -26,6 +27,7 @@ export function MessageTimestamp({
|
||||||
deletedForEveryone,
|
deletedForEveryone,
|
||||||
direction,
|
direction,
|
||||||
i18n,
|
i18n,
|
||||||
|
isRelativeTime,
|
||||||
module,
|
module,
|
||||||
timestamp,
|
timestamp,
|
||||||
withImageNoCaption,
|
withImageNoCaption,
|
||||||
|
@ -49,7 +51,7 @@ export function MessageTimestamp({
|
||||||
)}
|
)}
|
||||||
timestamp={timestamp}
|
timestamp={timestamp}
|
||||||
>
|
>
|
||||||
{formatTime(i18n, timestamp, now)}
|
{formatTime(i18n, timestamp, now, isRelativeTime)}
|
||||||
</Time>
|
</Time>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -91,6 +91,7 @@ export const actions = {
|
||||||
removeItem,
|
removeItem,
|
||||||
removeItemExternal,
|
removeItemExternal,
|
||||||
resetItems,
|
resetItems,
|
||||||
|
toggleHasAllStoriesMuted,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useActions = (): typeof actions => useBoundActions(actions);
|
export const useActions = (): typeof actions => useBoundActions(actions);
|
||||||
|
@ -111,6 +112,19 @@ function onSetSkinTone(tone: number): ItemPutAction {
|
||||||
return putItem('skinTone', tone);
|
return putItem('skinTone', tone);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleHasAllStoriesMuted(): ThunkAction<
|
||||||
|
void,
|
||||||
|
RootStateType,
|
||||||
|
unknown,
|
||||||
|
ItemPutAction
|
||||||
|
> {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const hasAllStoriesMuted = Boolean(getState().items.hasAllStoriesMuted);
|
||||||
|
|
||||||
|
dispatch(putItem('hasAllStoriesMuted', !hasAllStoriesMuted));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function putItemExternal(key: string, value: unknown): ItemPutExternalAction {
|
function putItemExternal(key: string, value: unknown): ItemPutExternalAction {
|
||||||
return {
|
return {
|
||||||
type: 'items/PUT_EXTERNAL',
|
type: 'items/PUT_EXTERNAL',
|
||||||
|
|
|
@ -20,6 +20,11 @@ const DEFAULT_PREFERRED_LEFT_PANE_WIDTH = 320;
|
||||||
|
|
||||||
export const getItems = (state: StateType): ItemsStateType => state.items;
|
export const getItems = (state: StateType): ItemsStateType => state.items;
|
||||||
|
|
||||||
|
export const getHasAllStoriesMuted = createSelector(
|
||||||
|
getItems,
|
||||||
|
({ hasAllStoriesMuted }): boolean => Boolean(hasAllStoriesMuted)
|
||||||
|
);
|
||||||
|
|
||||||
export const getAreWeASubscriber = createSelector(
|
export const getAreWeASubscriber = createSelector(
|
||||||
getItems,
|
getItems,
|
||||||
({ areWeASubscriber }: Readonly<ItemsStateType>): boolean =>
|
({ areWeASubscriber }: Readonly<ItemsStateType>): boolean =>
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { StoryViewer } from '../../components/StoryViewer';
|
||||||
import { ToastMessageBodyTooLong } from '../../components/ToastMessageBodyTooLong';
|
import { ToastMessageBodyTooLong } from '../../components/ToastMessageBodyTooLong';
|
||||||
import {
|
import {
|
||||||
getEmojiSkinTone,
|
getEmojiSkinTone,
|
||||||
|
getHasAllStoriesMuted,
|
||||||
getPreferredReactionEmoji,
|
getPreferredReactionEmoji,
|
||||||
} from '../selectors/items';
|
} from '../selectors/items';
|
||||||
import { getIntl } from '../selectors/user';
|
import { getIntl } from '../selectors/user';
|
||||||
|
@ -38,7 +39,7 @@ export function SmartStoryViewer({
|
||||||
onPrevUserStories,
|
onPrevUserStories,
|
||||||
}: PropsType): JSX.Element | null {
|
}: PropsType): JSX.Element | null {
|
||||||
const storiesActions = useStoriesActions();
|
const storiesActions = useStoriesActions();
|
||||||
const { onSetSkinTone } = useItemsActions();
|
const { onSetSkinTone, toggleHasAllStoriesMuted } = useItemsActions();
|
||||||
const { onUseEmoji } = useEmojisActions();
|
const { onUseEmoji } = useEmojisActions();
|
||||||
const { openConversationInternal, toggleHideStories } =
|
const { openConversationInternal, toggleHideStories } =
|
||||||
useConversationsActions();
|
useConversationsActions();
|
||||||
|
@ -59,12 +60,16 @@ export function SmartStoryViewer({
|
||||||
const recentEmojis = useRecentEmojis();
|
const recentEmojis = useRecentEmojis();
|
||||||
const skinTone = useSelector<StateType, number>(getEmojiSkinTone);
|
const skinTone = useSelector<StateType, number>(getEmojiSkinTone);
|
||||||
const replyState = useSelector(getStoryReplies);
|
const replyState = useSelector(getStoryReplies);
|
||||||
|
const hasAllStoriesMuted = useSelector<StateType, boolean>(
|
||||||
|
getHasAllStoriesMuted
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StoryViewer
|
<StoryViewer
|
||||||
conversationId={conversationId}
|
conversationId={conversationId}
|
||||||
getPreferredBadge={getPreferredBadge}
|
getPreferredBadge={getPreferredBadge}
|
||||||
group={group}
|
group={group}
|
||||||
|
hasAllStoriesMuted={hasAllStoriesMuted}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onHideStory={toggleHideStories}
|
onHideStory={toggleHideStories}
|
||||||
|
@ -96,6 +101,7 @@ export function SmartStoryViewer({
|
||||||
replyState={replyState}
|
replyState={replyState}
|
||||||
stories={stories}
|
stories={stories}
|
||||||
skinTone={skinTone}
|
skinTone={skinTone}
|
||||||
|
toggleHasAllStoriesMuted={toggleHasAllStoriesMuted}
|
||||||
{...storiesActions}
|
{...storiesActions}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
1
ts/types/Storage.d.ts
vendored
1
ts/types/Storage.d.ts
vendored
|
@ -140,6 +140,7 @@ export type StorageAccessType = {
|
||||||
subscriberId: Uint8Array;
|
subscriberId: Uint8Array;
|
||||||
subscriberCurrencyCode: string;
|
subscriberCurrencyCode: string;
|
||||||
displayBadgesOnProfile: boolean;
|
displayBadgesOnProfile: boolean;
|
||||||
|
hasAllStoriesMuted: boolean;
|
||||||
|
|
||||||
// Deprecated
|
// Deprecated
|
||||||
senderCertificateWithUuid: never;
|
senderCertificateWithUuid: never;
|
||||||
|
|
|
@ -11,23 +11,24 @@ export const STORAGE_UI_KEYS: ReadonlyArray<keyof StorageAccessType> = [
|
||||||
'badge-count-muted-conversations',
|
'badge-count-muted-conversations',
|
||||||
'call-ringtone-notification',
|
'call-ringtone-notification',
|
||||||
'call-system-notification',
|
'call-system-notification',
|
||||||
|
'customColors',
|
||||||
|
'defaultConversationColor',
|
||||||
|
'hasAllStoriesMuted',
|
||||||
'hide-menu-bar',
|
'hide-menu-bar',
|
||||||
'system-tray-setting',
|
|
||||||
'incoming-call-notification',
|
'incoming-call-notification',
|
||||||
'notification-draw-attention',
|
'notification-draw-attention',
|
||||||
'notification-setting',
|
'notification-setting',
|
||||||
'spell-check',
|
|
||||||
'theme-setting',
|
|
||||||
'defaultConversationColor',
|
|
||||||
'customColors',
|
|
||||||
'showStickerPickerHint',
|
|
||||||
'showStickersIntroduction',
|
|
||||||
'preferred-video-input-device',
|
|
||||||
'preferred-audio-input-device',
|
'preferred-audio-input-device',
|
||||||
'preferred-audio-output-device',
|
'preferred-audio-output-device',
|
||||||
|
'preferred-video-input-device',
|
||||||
'preferredLeftPaneWidth',
|
'preferredLeftPaneWidth',
|
||||||
'preferredReactionEmoji',
|
'preferredReactionEmoji',
|
||||||
'previousAudioDeviceModule',
|
'previousAudioDeviceModule',
|
||||||
|
'showStickerPickerHint',
|
||||||
|
'showStickersIntroduction',
|
||||||
'skinTone',
|
'skinTone',
|
||||||
|
'spell-check',
|
||||||
|
'system-tray-setting',
|
||||||
|
'theme-setting',
|
||||||
'zoomFactor',
|
'zoomFactor',
|
||||||
];
|
];
|
||||||
|
|
|
@ -103,7 +103,8 @@ export function formatDateTimeLong(
|
||||||
export function formatTime(
|
export function formatTime(
|
||||||
i18n: LocalizerType,
|
i18n: LocalizerType,
|
||||||
rawTimestamp: RawTimestamp,
|
rawTimestamp: RawTimestamp,
|
||||||
now: RawTimestamp
|
now: RawTimestamp,
|
||||||
|
isRelativeTime?: boolean
|
||||||
): string {
|
): string {
|
||||||
const timestamp = rawTimestamp.valueOf();
|
const timestamp = rawTimestamp.valueOf();
|
||||||
const diff = now.valueOf() - timestamp;
|
const diff = now.valueOf() - timestamp;
|
||||||
|
@ -116,6 +117,10 @@ export function formatTime(
|
||||||
return i18n('minutesAgo', [Math.floor(diff / MINUTE).toString()]);
|
return i18n('minutesAgo', [Math.floor(diff / MINUTE).toString()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isRelativeTime) {
|
||||||
|
return i18n('hoursAgo', [Math.floor(diff / HOUR).toString()]);
|
||||||
|
}
|
||||||
|
|
||||||
return new Date(timestamp).toLocaleTimeString([], {
|
return new Date(timestamp).toLocaleTimeString([], {
|
||||||
hour: 'numeric',
|
hour: 'numeric',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
|
|
Loading…
Reference in a new issue