- {option.icon && (
-
- )}
-
-
{option.label}
- {option.description && (
-
- {option.description}
-
+
+
+ {title &&
{title}
}
+ {menuOptions.map((option, index) => (
+
- {typeof value !== 'undefined' &&
- typeof option.value !== 'undefined' &&
- value === option.value ? (
-
- ) : null}
-
- ))}
+ {typeof value !== 'undefined' &&
+ typeof option.value !== 'undefined' &&
+ value === option.value ? (
+
+ ) : null}
+
+ ))}
+
);
}
diff --git a/ts/components/StoryImage.tsx b/ts/components/StoryImage.tsx
index 7174d07ffa74..6032655b16cf 100644
--- a/ts/components/StoryImage.tsx
+++ b/ts/components/StoryImage.tsx
@@ -25,6 +25,7 @@ export type PropsType = {
readonly attachment?: AttachmentType;
readonly children?: ReactNode;
readonly i18n: LocalizerType;
+ readonly isMuted?: boolean;
readonly isPaused?: boolean;
readonly isThumbnail?: boolean;
readonly label: string;
@@ -37,6 +38,7 @@ export const StoryImage = ({
attachment,
children,
i18n,
+ isMuted,
isPaused,
isThumbnail,
label,
@@ -106,6 +108,7 @@ export const StoryImage = ({
controls={false}
key={attachment.url}
loop={shouldLoop}
+ muted={isMuted}
ref={videoRef}
>
diff --git a/ts/components/StoryViewer.stories.tsx b/ts/components/StoryViewer.stories.tsx
index f64c6695d547..1fc04654e97d 100644
--- a/ts/components/StoryViewer.stories.tsx
+++ b/ts/components/StoryViewer.stories.tsx
@@ -23,6 +23,7 @@ function getDefaultProps(): PropsType {
conversationId: sender.id,
getPreferredBadge: () => undefined,
group: undefined,
+ hasAllStoriesMuted: false,
i18n,
loadStoryReplies: action('loadStoryReplies'),
markStoryRead: action('markStoryRead'),
@@ -51,6 +52,7 @@ function getDefaultProps(): PropsType {
timestamp: Date.now(),
},
],
+ toggleHasAllStoriesMuted: action('toggleHasAllStoriesMuted'),
};
}
@@ -153,6 +155,7 @@ story.add('Caption', () => (
story.add('Long Caption', () => (
;
+ hasAllStoriesMuted: boolean;
i18n: LocalizerType;
loadStoryReplies: (conversationId: string, messageId: string) => unknown;
markStoryRead: (mId: string) => unknown;
@@ -72,6 +74,7 @@ export type PropsType = {
replyState?: ReplyStateType;
skinTone?: number;
stories: Array;
+ toggleHasAllStoriesMuted: () => unknown;
views?: Array;
};
@@ -90,6 +93,7 @@ export const StoryViewer = ({
conversationId,
getPreferredBadge,
group,
+ hasAllStoriesMuted,
i18n,
loadStoryReplies,
markStoryRead,
@@ -110,6 +114,7 @@ export const StoryViewer = ({
replyState,
skinTone,
stories,
+ toggleHasAllStoriesMuted,
views,
}: PropsType): JSX.Element => {
const [currentStoryIndex, setCurrentStoryIndex] = useState(0);
@@ -261,11 +266,14 @@ export const StoryViewer = ({
};
}, [currentStoryIndex, spring, storyDuration]);
+ const [pauseStory, setPauseStory] = useState(false);
+
const shouldPauseViewing =
hasConfirmHideStory ||
hasExpandedCaption ||
hasReplyModal ||
isShowingContextMenu ||
+ pauseStory ||
Boolean(reactionEmoji);
useEffect(() => {
@@ -388,6 +396,7 @@ export const StoryViewer = ({
attachment={attachment}
i18n={i18n}
isPaused={shouldPauseViewing}
+ isMuted={hasAllStoriesMuted}
label={i18n('lightboxImageAlt')}
moduleClassName="StoryViewer__story"
queueStoryDownload={queueStoryDownload}
@@ -436,50 +445,90 @@ export const StoryViewer = ({
)}
)}
-
- {group
- ? i18n('Stories__from-to-group', {
- name: title,
- group: group.title,
- })
- : title}
+
+
+
+ {group && (
+
+ )}
+
+ {group
+ ? i18n('Stories__from-to-group', {
+ name: title,
+ group: group.title,
+ })
+ : title}
+
+
+
+
+
-
{stories.map((story, index) => (
-
setIsShowingContextMenu(true)}
- ref={setReferenceElement}
- tabIndex={0}
- type="button"
- />
setIsShowingContextMenu(false)}
- popperOptions={{
- placement: 'bottom',
- strategy: 'absolute',
- }}
referenceElement={referenceElement}
+ theme={Theme.Dark}
/>
{hasReplyModal && canReply && (
- {formatTime(i18n, timestamp, now)}
+ {formatTime(i18n, timestamp, now, isRelativeTime)}
);
}
diff --git a/ts/state/ducks/items.ts b/ts/state/ducks/items.ts
index c75c61993775..01eadbe54375 100644
--- a/ts/state/ducks/items.ts
+++ b/ts/state/ducks/items.ts
@@ -91,6 +91,7 @@ export const actions = {
removeItem,
removeItemExternal,
resetItems,
+ toggleHasAllStoriesMuted,
};
export const useActions = (): typeof actions => useBoundActions(actions);
@@ -111,6 +112,19 @@ function onSetSkinTone(tone: number): ItemPutAction {
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 {
return {
type: 'items/PUT_EXTERNAL',
diff --git a/ts/state/selectors/items.ts b/ts/state/selectors/items.ts
index 1da1acd4a79b..83b466ec4111 100644
--- a/ts/state/selectors/items.ts
+++ b/ts/state/selectors/items.ts
@@ -20,6 +20,11 @@ const DEFAULT_PREFERRED_LEFT_PANE_WIDTH = 320;
export const getItems = (state: StateType): ItemsStateType => state.items;
+export const getHasAllStoriesMuted = createSelector(
+ getItems,
+ ({ hasAllStoriesMuted }): boolean => Boolean(hasAllStoriesMuted)
+);
+
export const getAreWeASubscriber = createSelector(
getItems,
({ areWeASubscriber }: Readonly): boolean =>
diff --git a/ts/state/smart/StoryViewer.tsx b/ts/state/smart/StoryViewer.tsx
index e1188b00b961..52610be05210 100644
--- a/ts/state/smart/StoryViewer.tsx
+++ b/ts/state/smart/StoryViewer.tsx
@@ -11,6 +11,7 @@ import { StoryViewer } from '../../components/StoryViewer';
import { ToastMessageBodyTooLong } from '../../components/ToastMessageBodyTooLong';
import {
getEmojiSkinTone,
+ getHasAllStoriesMuted,
getPreferredReactionEmoji,
} from '../selectors/items';
import { getIntl } from '../selectors/user';
@@ -38,7 +39,7 @@ export function SmartStoryViewer({
onPrevUserStories,
}: PropsType): JSX.Element | null {
const storiesActions = useStoriesActions();
- const { onSetSkinTone } = useItemsActions();
+ const { onSetSkinTone, toggleHasAllStoriesMuted } = useItemsActions();
const { onUseEmoji } = useEmojisActions();
const { openConversationInternal, toggleHideStories } =
useConversationsActions();
@@ -59,12 +60,16 @@ export function SmartStoryViewer({
const recentEmojis = useRecentEmojis();
const skinTone = useSelector(getEmojiSkinTone);
const replyState = useSelector(getStoryReplies);
+ const hasAllStoriesMuted = useSelector(
+ getHasAllStoriesMuted
+ );
return (
);
diff --git a/ts/types/Storage.d.ts b/ts/types/Storage.d.ts
index a4acc64a427a..a4791a0d4977 100644
--- a/ts/types/Storage.d.ts
+++ b/ts/types/Storage.d.ts
@@ -140,6 +140,7 @@ export type StorageAccessType = {
subscriberId: Uint8Array;
subscriberCurrencyCode: string;
displayBadgesOnProfile: boolean;
+ hasAllStoriesMuted: boolean;
// Deprecated
senderCertificateWithUuid: never;
diff --git a/ts/types/StorageUIKeys.ts b/ts/types/StorageUIKeys.ts
index e8f6c13455c6..f8ab3d30c9ac 100644
--- a/ts/types/StorageUIKeys.ts
+++ b/ts/types/StorageUIKeys.ts
@@ -11,23 +11,24 @@ export const STORAGE_UI_KEYS: ReadonlyArray = [
'badge-count-muted-conversations',
'call-ringtone-notification',
'call-system-notification',
+ 'customColors',
+ 'defaultConversationColor',
+ 'hasAllStoriesMuted',
'hide-menu-bar',
- 'system-tray-setting',
'incoming-call-notification',
'notification-draw-attention',
'notification-setting',
- 'spell-check',
- 'theme-setting',
- 'defaultConversationColor',
- 'customColors',
- 'showStickerPickerHint',
- 'showStickersIntroduction',
- 'preferred-video-input-device',
'preferred-audio-input-device',
'preferred-audio-output-device',
+ 'preferred-video-input-device',
'preferredLeftPaneWidth',
'preferredReactionEmoji',
'previousAudioDeviceModule',
+ 'showStickerPickerHint',
+ 'showStickersIntroduction',
'skinTone',
+ 'spell-check',
+ 'system-tray-setting',
+ 'theme-setting',
'zoomFactor',
];
diff --git a/ts/util/timestamp.ts b/ts/util/timestamp.ts
index 739de59eedcc..bf3620f8de7a 100644
--- a/ts/util/timestamp.ts
+++ b/ts/util/timestamp.ts
@@ -103,7 +103,8 @@ export function formatDateTimeLong(
export function formatTime(
i18n: LocalizerType,
rawTimestamp: RawTimestamp,
- now: RawTimestamp
+ now: RawTimestamp,
+ isRelativeTime?: boolean
): string {
const timestamp = rawTimestamp.valueOf();
const diff = now.valueOf() - timestamp;
@@ -116,6 +117,10 @@ export function formatTime(
return i18n('minutesAgo', [Math.floor(diff / MINUTE).toString()]);
}
+ if (isRelativeTime) {
+ return i18n('hoursAgo', [Math.floor(diff / HOUR).toString()]);
+ }
+
return new Date(timestamp).toLocaleTimeString([], {
hour: 'numeric',
minute: '2-digit',