stories: muted by default, muted on app blur
This commit is contained in:
parent
cd1a1a00a2
commit
9f85db3fd8
12 changed files with 88 additions and 60 deletions
|
@ -417,6 +417,12 @@ export async function startApp(): Promise<void> {
|
|||
}
|
||||
});
|
||||
|
||||
window.SignalContext.activeWindowService.registerForChange(isActive => {
|
||||
if (!isActive) {
|
||||
window.reduxActions.stories.setHasAllStoriesUnmuted(false);
|
||||
}
|
||||
});
|
||||
|
||||
let resolveOnAppView: (() => void) | undefined;
|
||||
const onAppView = new Promise<void>(resolve => {
|
||||
resolveOnAppView = resolve;
|
||||
|
|
|
@ -68,6 +68,7 @@ export default {
|
|||
},
|
||||
toggleHasAllStoriesMuted: { action: true },
|
||||
viewStory: { action: true },
|
||||
isWindowActive: { defaultValue: true },
|
||||
},
|
||||
args: {
|
||||
currentIndex: 0,
|
||||
|
|
|
@ -66,7 +66,7 @@ export type PropsType = {
|
|||
| 'left'
|
||||
>;
|
||||
hasActiveCall?: boolean;
|
||||
hasAllStoriesMuted: boolean;
|
||||
hasAllStoriesUnmuted: boolean;
|
||||
hasViewReceiptSetting: boolean;
|
||||
i18n: LocalizerType;
|
||||
isSignalConversation?: boolean;
|
||||
|
@ -95,8 +95,9 @@ export type PropsType = {
|
|||
skinTone?: number;
|
||||
story: StoryViewType;
|
||||
storyViewMode: StoryViewModeType;
|
||||
toggleHasAllStoriesMuted: () => unknown;
|
||||
setHasAllStoriesUnmuted: (isUnmuted: boolean) => unknown;
|
||||
viewStory: ViewStoryActionCreatorType;
|
||||
isWindowActive: boolean;
|
||||
deleteGroupStoryReply: (id: string) => void;
|
||||
deleteGroupStoryReplyForEveryone: (id: string) => void;
|
||||
};
|
||||
|
@ -119,7 +120,7 @@ export const StoryViewer = ({
|
|||
getPreferredBadge,
|
||||
group,
|
||||
hasActiveCall,
|
||||
hasAllStoriesMuted,
|
||||
hasAllStoriesUnmuted,
|
||||
hasViewReceiptSetting,
|
||||
i18n,
|
||||
isSignalConversation,
|
||||
|
@ -143,8 +144,9 @@ export const StoryViewer = ({
|
|||
skinTone,
|
||||
story,
|
||||
storyViewMode,
|
||||
toggleHasAllStoriesMuted,
|
||||
setHasAllStoriesUnmuted,
|
||||
viewStory,
|
||||
isWindowActive,
|
||||
deleteGroupStoryReply,
|
||||
deleteGroupStoryReplyForEveryone,
|
||||
}: PropsType): JSX.Element => {
|
||||
|
@ -305,6 +307,12 @@ export const StoryViewer = ({
|
|||
|
||||
const [pauseStory, setPauseStory] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isWindowActive) {
|
||||
setPauseStory(true);
|
||||
}
|
||||
}, [isWindowActive]);
|
||||
|
||||
const shouldPauseViewing =
|
||||
Boolean(confirmDeleteStory) ||
|
||||
currentViewTarget != null ||
|
||||
|
@ -436,19 +444,19 @@ export const StoryViewer = ({
|
|||
const replyCount = replies.length;
|
||||
const viewCount = views.length;
|
||||
|
||||
const canMuteStory = isVideoAttachment(attachment);
|
||||
const isStoryMuted = hasAllStoriesMuted || !canMuteStory;
|
||||
const hasAudio = isVideoAttachment(attachment);
|
||||
const isStoryMuted = !hasAllStoriesUnmuted || !hasAudio;
|
||||
|
||||
let muteClassName: string;
|
||||
let muteAriaLabel: string;
|
||||
if (canMuteStory) {
|
||||
muteAriaLabel = hasAllStoriesMuted
|
||||
? i18n('StoryViewer__unmute')
|
||||
: i18n('StoryViewer__mute');
|
||||
if (hasAudio) {
|
||||
muteAriaLabel = hasAllStoriesUnmuted
|
||||
? i18n('StoryViewer__mute')
|
||||
: i18n('StoryViewer__unmute');
|
||||
|
||||
muteClassName = hasAllStoriesMuted
|
||||
? 'StoryViewer__unmute'
|
||||
: 'StoryViewer__mute';
|
||||
muteClassName = hasAllStoriesUnmuted
|
||||
? 'StoryViewer__mute'
|
||||
: 'StoryViewer__unmute';
|
||||
} else {
|
||||
muteAriaLabel = i18n('Stories__toast--hasNoSound');
|
||||
muteClassName = 'StoryViewer__soundless';
|
||||
|
@ -686,8 +694,8 @@ export const StoryViewer = ({
|
|||
aria-label={muteAriaLabel}
|
||||
className={muteClassName}
|
||||
onClick={
|
||||
canMuteStory
|
||||
? toggleHasAllStoriesMuted
|
||||
hasAudio
|
||||
? () => setHasAllStoriesUnmuted(!hasAllStoriesUnmuted)
|
||||
: () => showToast(ToastType.StoryMuted)
|
||||
}
|
||||
type="button"
|
||||
|
|
|
@ -91,7 +91,6 @@ export const actions = {
|
|||
removeItem,
|
||||
removeItemExternal,
|
||||
resetItems,
|
||||
toggleHasAllStoriesMuted,
|
||||
};
|
||||
|
||||
export const useActions = (): typeof actions => useBoundActions(actions);
|
||||
|
@ -112,19 +111,6 @@ 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',
|
||||
|
|
|
@ -106,21 +106,22 @@ export type AddStoryData =
|
|||
|
||||
// State
|
||||
|
||||
export type StoriesStateType = {
|
||||
readonly lastOpenedAtTimestamp: number | undefined;
|
||||
readonly openedAtTimestamp: number | undefined;
|
||||
readonly replyState?: {
|
||||
export type StoriesStateType = Readonly<{
|
||||
lastOpenedAtTimestamp: number | undefined;
|
||||
openedAtTimestamp: number | undefined;
|
||||
replyState?: Readonly<{
|
||||
messageId: string;
|
||||
replies: Array<MessageAttributesType>;
|
||||
};
|
||||
readonly selectedStoryData?: SelectedStoryDataType;
|
||||
readonly addStoryData: AddStoryData;
|
||||
readonly sendStoryModalData?: {
|
||||
untrustedUuids: Array<string>;
|
||||
verifiedUuids: Array<string>;
|
||||
};
|
||||
readonly stories: Array<StoryDataType>;
|
||||
};
|
||||
}>;
|
||||
selectedStoryData?: SelectedStoryDataType;
|
||||
addStoryData: AddStoryData;
|
||||
sendStoryModalData?: Readonly<{
|
||||
untrustedUuids: ReadonlyArray<string>;
|
||||
verifiedUuids: ReadonlyArray<string>;
|
||||
}>;
|
||||
stories: ReadonlyArray<StoryDataType>;
|
||||
hasAllStoriesUnmuted: boolean;
|
||||
}>;
|
||||
|
||||
// Actions
|
||||
|
||||
|
@ -138,6 +139,7 @@ const STORY_REPLY_DELETED = 'stories/STORY_REPLY_DELETED';
|
|||
const REMOVE_ALL_STORIES = 'stories/REMOVE_ALL_STORIES';
|
||||
const SET_ADD_STORY_DATA = 'stories/SET_ADD_STORY_DATA';
|
||||
const SET_STORY_SENDING = 'stories/SET_STORY_SENDING';
|
||||
const SET_HAS_ALL_STORIES_UNMUTED = 'stories/SET_HAS_ALL_STORIES_UNMUTED';
|
||||
|
||||
type DOEStoryActionType = {
|
||||
type: typeof DOE_STORY;
|
||||
|
@ -211,6 +213,11 @@ type SetStorySendingType = {
|
|||
payload: boolean;
|
||||
};
|
||||
|
||||
type SetHasAllStoriesUnmutedType = {
|
||||
type: typeof SET_HAS_ALL_STORIES_UNMUTED;
|
||||
payload: boolean;
|
||||
};
|
||||
|
||||
export type StoriesActionType =
|
||||
| DOEStoryActionType
|
||||
| ListMembersVerified
|
||||
|
@ -227,7 +234,8 @@ export type StoriesActionType =
|
|||
| StoryReplyDeletedActionType
|
||||
| RemoveAllStoriesActionType
|
||||
| SetAddStoryDataType
|
||||
| SetStorySendingType;
|
||||
| SetStorySendingType
|
||||
| SetHasAllStoriesUnmutedType;
|
||||
|
||||
// Action Creators
|
||||
|
||||
|
@ -1235,6 +1243,15 @@ function setStorySending(sending: boolean): SetStorySendingType {
|
|||
};
|
||||
}
|
||||
|
||||
function setHasAllStoriesUnmuted(
|
||||
isUnmuted: boolean
|
||||
): SetHasAllStoriesUnmutedType {
|
||||
return {
|
||||
type: SET_HAS_ALL_STORIES_UNMUTED,
|
||||
payload: isUnmuted,
|
||||
};
|
||||
}
|
||||
|
||||
function setStoriesDisabled(
|
||||
value: boolean
|
||||
): ThunkAction<void, RootStateType, unknown, never> {
|
||||
|
@ -1262,6 +1279,7 @@ export const actions = {
|
|||
deleteGroupStoryReplyForEveryone,
|
||||
setAddStoryData,
|
||||
setStoriesDisabled,
|
||||
setHasAllStoriesUnmuted,
|
||||
setStorySending,
|
||||
};
|
||||
|
||||
|
@ -1277,6 +1295,7 @@ export function getEmptyState(
|
|||
openedAtTimestamp: undefined,
|
||||
addStoryData: undefined,
|
||||
stories: [],
|
||||
hasAllStoriesUnmuted: false,
|
||||
...overrideState,
|
||||
};
|
||||
}
|
||||
|
@ -1644,5 +1663,12 @@ export function reducer(
|
|||
};
|
||||
}
|
||||
|
||||
if (action.type === SET_HAS_ALL_STORIES_UNMUTED) {
|
||||
return {
|
||||
...state,
|
||||
hasAllStoriesUnmuted: action.payload,
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -555,8 +555,8 @@ export const getNonGroupStories = createSelector(
|
|||
|
||||
export const selectMostRecentActiveStoryTimestampByGroupOrDistributionList =
|
||||
createSelector(
|
||||
(state: StateType): Array<StoryDataType> => state.stories.stories,
|
||||
(stories: Array<StoryDataType>): Record<string, number> => {
|
||||
(state: StateType): ReadonlyArray<StoryDataType> => state.stories.stories,
|
||||
(stories: ReadonlyArray<StoryDataType>): Record<string, number> => {
|
||||
return reduce<StoryDataType, Record<string, number>>(
|
||||
stories,
|
||||
(acc, story) => {
|
||||
|
|
|
@ -23,11 +23,6 @@ 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<ItemsStateType>): boolean =>
|
||||
|
|
|
@ -541,3 +541,8 @@ export const getStoryByIdSelector = createSelector(
|
|||
};
|
||||
}
|
||||
);
|
||||
|
||||
export const getHasAllStoriesUnmuted = createSelector(
|
||||
getStoriesState,
|
||||
({ hasAllStoriesUnmuted }): boolean => hasAllStoriesUnmuted
|
||||
);
|
||||
|
|
|
@ -13,7 +13,6 @@ import { ToastType, useToastActions } from '../ducks/toast';
|
|||
import { getConversationSelector } from '../selectors/conversations';
|
||||
import {
|
||||
getEmojiSkinTone,
|
||||
getHasAllStoriesMuted,
|
||||
getHasStoryViewReceiptSetting,
|
||||
getPreferredReactionEmoji,
|
||||
} from '../selectors/items';
|
||||
|
@ -23,24 +22,28 @@ import {
|
|||
getSelectedStoryData,
|
||||
getStoryReplies,
|
||||
getStoryByIdSelector,
|
||||
getHasAllStoriesUnmuted,
|
||||
} from '../selectors/stories';
|
||||
import { isInFullScreenCall } from '../selectors/calling';
|
||||
import { isSignalConversation } from '../../util/isSignalConversation';
|
||||
import { renderEmojiPicker } from './renderEmojiPicker';
|
||||
import { strictAssert } from '../../util/assert';
|
||||
import { useActions as useEmojisActions } from '../ducks/emojis';
|
||||
import { useActions as useItemsActions } from '../ducks/items';
|
||||
import { useConversationsActions } from '../ducks/conversations';
|
||||
import { useRecentEmojis } from '../selectors/emojis';
|
||||
import { useActions as useItemsActions } from '../ducks/items';
|
||||
import { useStoriesActions } from '../ducks/stories';
|
||||
import { useIsWindowActive } from '../../hooks/useIsWindowActive';
|
||||
|
||||
export function SmartStoryViewer(): JSX.Element | null {
|
||||
const storiesActions = useStoriesActions();
|
||||
const { onSetSkinTone, toggleHasAllStoriesMuted } = useItemsActions();
|
||||
const { onUseEmoji } = useEmojisActions();
|
||||
const { showConversation, toggleHideStories } = useConversationsActions();
|
||||
const { onSetSkinTone } = useItemsActions();
|
||||
const { showToast } = useToastActions();
|
||||
|
||||
const isWindowActive = useIsWindowActive();
|
||||
|
||||
const i18n = useSelector<StateType, LocalizerType>(getIntl);
|
||||
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
|
||||
const preferredReactionEmoji = useSelector<StateType, Array<string>>(
|
||||
|
@ -63,8 +66,8 @@ export function SmartStoryViewer(): JSX.Element | null {
|
|||
const recentEmojis = useRecentEmojis();
|
||||
const skinTone = useSelector<StateType, number>(getEmojiSkinTone);
|
||||
const replyState = useSelector(getStoryReplies);
|
||||
const hasAllStoriesMuted = useSelector<StateType, boolean>(
|
||||
getHasAllStoriesMuted
|
||||
const hasAllStoriesUnmuted = useSelector<StateType, boolean>(
|
||||
getHasAllStoriesUnmuted
|
||||
);
|
||||
|
||||
const hasActiveCall = useSelector(isInFullScreenCall);
|
||||
|
@ -90,7 +93,7 @@ export function SmartStoryViewer(): JSX.Element | null {
|
|||
getPreferredBadge={getPreferredBadge}
|
||||
group={conversationStory.group}
|
||||
hasActiveCall={hasActiveCall}
|
||||
hasAllStoriesMuted={hasAllStoriesMuted}
|
||||
hasAllStoriesUnmuted={hasAllStoriesUnmuted}
|
||||
hasViewReceiptSetting={hasViewReceiptSetting}
|
||||
i18n={i18n}
|
||||
isSignalConversation={isSignalConversation({
|
||||
|
@ -127,7 +130,7 @@ export function SmartStoryViewer(): JSX.Element | null {
|
|||
skinTone={skinTone}
|
||||
story={storyView}
|
||||
storyViewMode={selectedStoryData.storyViewMode}
|
||||
toggleHasAllStoriesMuted={toggleHasAllStoriesMuted}
|
||||
isWindowActive={isWindowActive}
|
||||
{...storiesActions}
|
||||
/>
|
||||
);
|
||||
|
|
1
ts/types/Storage.d.ts
vendored
1
ts/types/Storage.d.ts
vendored
|
@ -141,7 +141,6 @@ export type StorageAccessType = {
|
|||
subscriberCurrencyCode: string;
|
||||
displayBadgesOnProfile: boolean;
|
||||
keepMutedChatsArchived: boolean;
|
||||
hasAllStoriesMuted: boolean;
|
||||
|
||||
// Deprecated
|
||||
senderCertificateWithUuid: never;
|
||||
|
|
|
@ -17,7 +17,6 @@ export const STORAGE_UI_KEYS: ReadonlyArray<keyof StorageAccessType> = [
|
|||
'call-system-notification',
|
||||
'customColors',
|
||||
'defaultConversationColor',
|
||||
'hasAllStoriesMuted',
|
||||
'hide-menu-bar',
|
||||
'incoming-call-notification',
|
||||
'notification-draw-attention',
|
||||
|
|
|
@ -10,7 +10,7 @@ import { onStoryRecipientUpdate } from './onStoryRecipientUpdate';
|
|||
import { sendDeleteForEveryoneMessage } from './sendDeleteForEveryoneMessage';
|
||||
|
||||
export async function deleteStoryForEveryone(
|
||||
stories: Array<StoryDataType>,
|
||||
stories: ReadonlyArray<StoryDataType>,
|
||||
story: StoryDataType
|
||||
): Promise<void> {
|
||||
if (!story.sendStateByConversationId) {
|
||||
|
|
Loading…
Add table
Reference in a new issue