Adds playback bar to story viewer

This commit is contained in:
Josh Perez 2022-05-06 15:02:44 -04:00 committed by GitHub
parent 9817946afc
commit 85c8ff76dc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 291 additions and 125 deletions

View file

@ -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"

View 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

View 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

View 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

View 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

View file

@ -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;

View file

@ -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>
); );
} }

View file

@ -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} />

View file

@ -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({

View file

@ -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

View file

@ -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>
); );
} }

View file

@ -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',

View file

@ -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 =>

View file

@ -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}
/> />
); );

View file

@ -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;

View file

@ -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',
]; ];

View file

@ -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',