ConversationView: Move call/mute functions into redux

This commit is contained in:
Scott Nonnenberg 2022-12-06 09:31:44 -08:00 committed by GitHub
parent 8fe51cc854
commit 92a512a16d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 353 additions and 287 deletions

View file

@ -1,20 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import type { LocalizerType } from '../types/Util';
import { Toast } from './Toast';
type PropsType = {
i18n: LocalizerType;
onClose: () => unknown;
};
export function ToastCannotStartGroupCall({
i18n,
onClose,
}: PropsType): JSX.Element {
return (
<Toast onClose={onClose}>{i18n('GroupV2--cannot-start-group-call')}</Toast>
);
}

View file

@ -30,72 +30,6 @@ export function ToastManager({
}
const { toastType } = toast;
if (toastType === ToastType.Error) {
return (
<Toast
autoDismissDisabled
onClose={hideToast}
toastAction={{
label: i18n('Toast--error--action'),
onClick: () => window.showDebugLog(),
}}
>
{i18n('Toast--error')}
</Toast>
);
}
if (toastType === ToastType.MessageBodyTooLong) {
return <ToastMessageBodyTooLong i18n={i18n} onClose={hideToast} />;
}
if (toastType === ToastType.StoryReact) {
return (
<Toast onClose={hideToast} timeout={SHORT_TIMEOUT}>
{i18n('Stories__toast--sending-reaction')}
</Toast>
);
}
if (toastType === ToastType.StoryReply) {
return (
<Toast onClose={hideToast} timeout={SHORT_TIMEOUT}>
{i18n('Stories__toast--sending-reply')}
</Toast>
);
}
if (toastType === ToastType.StoryMuted) {
return (
<Toast onClose={hideToast} timeout={SHORT_TIMEOUT}>
{i18n('Stories__toast--hasNoSound')}
</Toast>
);
}
if (toastType === ToastType.StoryVideoTooLong) {
return (
<Toast onClose={hideToast}>
{i18n('StoryCreator__error--video-too-long')}
</Toast>
);
}
if (toastType === ToastType.StoryVideoUnsupported) {
return (
<Toast onClose={hideToast}>
{i18n('StoryCreator__error--video-unsupported')}
</Toast>
);
}
if (toastType === ToastType.StoryVideoError) {
return (
<Toast onClose={hideToast}>
{i18n('StoryCreator__error--video-error')}
</Toast>
);
}
if (toastType === ToastType.AddingUserToGroup) {
return (
@ -108,21 +42,10 @@ export function ToastManager({
);
}
if (toastType === ToastType.UserAddedToGroup) {
if (toastType === ToastType.CannotStartGroupCall) {
return (
<Toast onClose={hideToast}>
{i18n(
'AddUserToAnotherGroupModal__toast--user-added-to-group',
toast.parameters
)}
</Toast>
);
}
if (toastType === ToastType.FailedToDeleteUsername) {
return (
<Toast onClose={hideToast}>
{i18n('ProfileEditor--username--delete-general-error')}
{i18n('GroupV2--cannot-start-group-call', toast.parameters)}
</Toast>
);
}
@ -147,5 +70,91 @@ export function ToastManager({
return <Toast onClose={hideToast}>{i18n('deleteForEveryoneFailed')}</Toast>;
}
if (toastType === ToastType.Error) {
return (
<Toast
autoDismissDisabled
onClose={hideToast}
toastAction={{
label: i18n('Toast--error--action'),
onClick: () => window.showDebugLog(),
}}
>
{i18n('Toast--error')}
</Toast>
);
}
if (toastType === ToastType.FailedToDeleteUsername) {
return (
<Toast onClose={hideToast}>
{i18n('ProfileEditor--username--delete-general-error')}
</Toast>
);
}
if (toastType === ToastType.MessageBodyTooLong) {
return <ToastMessageBodyTooLong i18n={i18n} onClose={hideToast} />;
}
if (toastType === ToastType.StoryMuted) {
return (
<Toast onClose={hideToast} timeout={SHORT_TIMEOUT}>
{i18n('Stories__toast--hasNoSound')}
</Toast>
);
}
if (toastType === ToastType.StoryReact) {
return (
<Toast onClose={hideToast} timeout={SHORT_TIMEOUT}>
{i18n('Stories__toast--sending-reaction')}
</Toast>
);
}
if (toastType === ToastType.StoryReply) {
return (
<Toast onClose={hideToast} timeout={SHORT_TIMEOUT}>
{i18n('Stories__toast--sending-reply')}
</Toast>
);
}
if (toastType === ToastType.StoryVideoError) {
return (
<Toast onClose={hideToast}>
{i18n('StoryCreator__error--video-error')}
</Toast>
);
}
if (toastType === ToastType.StoryVideoTooLong) {
return (
<Toast onClose={hideToast}>
{i18n('StoryCreator__error--video-too-long')}
</Toast>
);
}
if (toastType === ToastType.StoryVideoUnsupported) {
return (
<Toast onClose={hideToast}>
{i18n('StoryCreator__error--video-unsupported')}
</Toast>
);
}
if (toastType === ToastType.UserAddedToGroup) {
return (
<Toast onClose={hideToast}>
{i18n(
'AddUserToAnotherGroupModal__toast--user-added-to-group',
toast.parameters
)}
</Toast>
);
}
throw missingCaseError(toastType);
}

View file

@ -41,7 +41,6 @@ const commonProps = {
setDisappearingMessages: action('setDisappearingMessages'),
destroyMessages: action('destroyMessages'),
onSearchInConversation: action('onSearchInConversation'),
onSetMuteNotifications: action('onSetMuteNotifications'),
onOutgoingAudioCallInConversation: action(
'onOutgoingAudioCallInConversation'
),
@ -57,6 +56,7 @@ const commonProps = {
onMarkUnread: action('onMarkUnread'),
onMoveToInbox: action('onMoveToInbox'),
onSetPin: action('onSetPin'),
setMuteExpiration: action('onSetMuteNotifications'),
viewUserStories: action('viewUserStories'),
};

View file

@ -80,11 +80,10 @@ export type PropsDataType = {
>;
export type PropsActionsType = {
onSetMuteNotifications: (seconds: number) => void;
destroyMessages: (conversationId: string) => void;
onSearchInConversation: () => void;
onOutgoingAudioCallInConversation: () => void;
onOutgoingVideoCallInConversation: () => void;
onOutgoingAudioCallInConversation: (conversationId: string) => void;
onOutgoingVideoCallInConversation: (conversationId: string) => void;
onSetPin: (value: boolean) => void;
onShowConversationDetails: () => void;
@ -95,6 +94,7 @@ export type PropsActionsType = {
onArchive: () => void;
onMarkUnread: () => void;
onMoveToInbox: () => void;
setMuteExpiration: (conversationId: string, seconds: number) => void;
setDisappearingMessages: (
conversationId: string,
seconds: DurationInSeconds
@ -349,12 +349,12 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
onArchive,
onMarkUnread,
onMoveToInbox,
onSetMuteNotifications,
onSetPin,
onShowAllMedia,
onShowConversationDetails,
onShowGroupMembers,
setDisappearingMessages,
setMuteExpiration,
type,
} = this.props;
@ -371,7 +371,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
{isMuted ? (
<MenuItem
onClick={() => {
onSetMuteNotifications(0);
setMuteExpiration(id, 0);
}}
>
{i18n('unmute')}
@ -379,7 +379,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
) : (
<MenuItem
onClick={() => {
onSetMuteNotifications(Number.MAX_SAFE_INTEGER);
setMuteExpiration(id, Number.MAX_SAFE_INTEGER);
}}
>
{i18n('muteAlways')}
@ -465,7 +465,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
key={item.name}
disabled={item.disabled}
onClick={() => {
onSetMuteNotifications(item.value);
setMuteExpiration(id, item.value);
}}
>
{item.name}
@ -676,6 +676,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
announcementsOnly={announcementsOnly}
areWeAdmin={areWeAdmin}
i18n={i18n}
id={id}
isNarrow={isNarrow}
onOutgoingAudioCallInConversation={
onOutgoingAudioCallInConversation
@ -702,6 +703,7 @@ function OutgoingCallButtons({
announcementsOnly,
areWeAdmin,
i18n,
id,
isNarrow,
onOutgoingAudioCallInConversation,
onOutgoingVideoCallInConversation,
@ -712,6 +714,7 @@ function OutgoingCallButtons({
| 'announcementsOnly'
| 'areWeAdmin'
| 'i18n'
| 'id'
| 'onOutgoingAudioCallInConversation'
| 'onOutgoingVideoCallInConversation'
| 'outgoingCallButtonStyle'
@ -729,14 +732,14 @@ function OutgoingCallButtons({
: undefined
)}
disabled={showBackButton}
onClick={onOutgoingVideoCallInConversation}
onClick={() => onOutgoingVideoCallInConversation(id)}
type="button"
/>
);
const startCallShortcuts = useStartCallShortcuts(
onOutgoingAudioCallInConversation,
onOutgoingVideoCallInConversation
() => onOutgoingAudioCallInConversation(id),
() => onOutgoingVideoCallInConversation(id)
);
useKeyboardShortcuts(startCallShortcuts);
@ -751,7 +754,7 @@ function OutgoingCallButtons({
{videoButton}
<button
type="button"
onClick={onOutgoingAudioCallInConversation}
onClick={() => onOutgoingAudioCallInConversation(id)}
className={classNames(
'module-ConversationHeader__button',
'module-ConversationHeader__button--audio',
@ -772,7 +775,7 @@ function OutgoingCallButtons({
showBackButton ? null : 'module-ConversationHeader__button--show'
)}
disabled={showBackButton}
onClick={onOutgoingVideoCallInConversation}
onClick={() => onOutgoingVideoCallInConversation(id)}
type="button"
>
{isNarrow ? null : i18n('joinOngoingCall')}

View file

@ -101,9 +101,6 @@ export type StateProps = {
onUnblock: () => void;
theme: ThemeType;
userAvatarData: Array<AvatarDataType>;
setMuteExpiration: (muteExpiresAt: undefined | number) => unknown;
onOutgoingAudioCallInConversation: () => unknown;
onOutgoingVideoCallInConversation: () => unknown;
renderChooseGroupMembersModal: (
props: SmartChooseGroupMembersModalPropsType
) => JSX.Element;
@ -115,10 +112,13 @@ export type StateProps = {
type ActionProps = {
deleteAvatarFromDisk: DeleteAvatarFromDiskActionType;
loadRecentMediaItems: (id: string, limit: number) => void;
onOutgoingAudioCallInConversation: (conversationId: string) => unknown;
onOutgoingVideoCallInConversation: (conversationId: string) => unknown;
replaceAvatar: ReplaceAvatarActionType;
saveAvatarToDisk: SaveAvatarToDiskActionType;
searchInConversation: (id: string) => unknown;
setDisappearingMessages: (id: string, seconds: DurationInSeconds) => void;
setMuteExpiration: (id: string, muteExpiresAt: undefined | number) => unknown;
showContactModal: (contactId: string, conversationId?: string) => void;
showConversation: ShowConversationType;
toggleAddUserToAnotherGroupModal: (contactId?: string) => void;
@ -291,6 +291,7 @@ export function ConversationDetails({
modalNode = (
<ConversationNotificationsModal
i18n={i18n}
id={conversation.id}
muteExpiresAt={conversation.muteExpiresAt}
onClose={() => {
setModalState(ModalState.NothingOpen);
@ -305,7 +306,7 @@ export function ConversationDetails({
dialogName="ConversationDetails.unmuteNotifications"
actions={[
{
action: () => setMuteExpiration(0),
action: () => setMuteExpiration(conversation.id, 0),
style: 'affirmative',
text: i18n('unmute'),
},
@ -354,14 +355,16 @@ export function ConversationDetails({
<ConversationDetailsCallButton
disabled={hasActiveCall}
i18n={i18n}
onClick={onOutgoingVideoCallInConversation}
onClick={() => onOutgoingVideoCallInConversation(conversation.id)}
type="video"
/>
{!isGroup && (
<ConversationDetailsCallButton
disabled={hasActiveCall}
i18n={i18n}
onClick={onOutgoingAudioCallInConversation}
onClick={() =>
onOutgoingAudioCallInConversation(conversation.id)
}
type="audio"
/>
)}

View file

@ -12,13 +12,18 @@ import { Button, ButtonVariant } from '../../Button';
type PropsType = {
i18n: LocalizerType;
id: string;
muteExpiresAt: undefined | number;
onClose: () => unknown;
setMuteExpiration: (muteExpiresAt: undefined | number) => unknown;
setMuteExpiration: (
conversationId: string,
muteExpiresAt: undefined | number
) => unknown;
};
export function ConversationNotificationsModal({
i18n,
id,
muteExpiresAt,
onClose,
setMuteExpiration,
@ -40,7 +45,7 @@ export function ConversationNotificationsModal({
muteExpirationValue,
'NotificationSettings: mute ms was not an integer'
);
setMuteExpiration(ms);
setMuteExpiration(id, ms);
onClose();
};

View file

@ -17,6 +17,7 @@ export default {
};
const getCommonProps = () => ({
id: 'conversation-id',
muteExpiresAt: undefined,
conversationType: 'group' as const,
dontNotifyForMentionsIfMuted: false,

View file

@ -15,17 +15,23 @@ import { parseIntOrThrow } from '../../../util/parseIntOrThrow';
import { useUniqueId } from '../../../hooks/useUniqueId';
type PropsType = {
id: string;
conversationType: ConversationTypeType;
dontNotifyForMentionsIfMuted: boolean;
i18n: LocalizerType;
muteExpiresAt: undefined | number;
setDontNotifyForMentionsIfMuted: (
conversationId: string,
dontNotifyForMentionsIfMuted: boolean
) => unknown;
setMuteExpiration: (muteExpiresAt: undefined | number) => unknown;
setMuteExpiration: (
conversationId: string,
muteExpiresAt: undefined | number
) => unknown;
};
export function ConversationNotificationsSettings({
id,
conversationType,
dontNotifyForMentionsIfMuted,
i18n,
@ -62,11 +68,11 @@ export function ConversationNotificationsSettings({
rawValue,
'NotificationSettings: mute ms was not an integer'
);
setMuteExpiration(ms);
setMuteExpiration(id, ms);
};
const onChangeDontNotifyForMentionsIfMuted = (rawValue: string) => {
setDontNotifyForMentionsIfMuted(rawValue === 'yes');
setDontNotifyForMentionsIfMuted(id, rawValue === 'yes');
};
return (

View file

@ -47,6 +47,10 @@ import * as log from '../../logging/log';
import { strictAssert } from '../../util/assert';
import { waitForOnline } from '../../util/waitForOnline';
import * as mapUtil from '../../util/mapUtil';
import { isCallSafe } from '../../util/isCallSafe';
import { isDirectConversation } from '../../util/whatTypeOfConversation';
import { SHOW_TOAST, ToastType } from './toast';
import type { ShowToastActionType } from './toast';
// State
@ -1189,6 +1193,106 @@ function setOutgoingRing(payload: boolean): SetOutgoingRingActionType {
};
}
function onOutgoingVideoCallInConversation(
conversationId: string
): ThunkAction<
void,
RootStateType,
unknown,
StartCallingLobbyActionType | ShowToastActionType
> {
return async (dispatch, getState) => {
const conversation = window.ConversationController.get(conversationId);
if (!conversation) {
throw new Error(
`onOutgoingVideoCallInConversation: No conversation found for conversation ${conversationId}`
);
}
log.info('onOutgoingVideoCallInConversation: about to start a video call');
// if it's a group call on an announcementsOnly group
// only allow join if the call has already been started (presumably by the admin)
if (conversation.get('announcementsOnly') && !conversation.areWeAdmin()) {
const call = getOwn(
getState().calling.callsByConversation,
conversationId
);
// technically not necessary, but isAnybodyElseInGroupCall requires it
const ourUuid = window.storage.user.getCheckedUuid().toString();
const isOngoingGroupCall =
call &&
ourUuid &&
call.callMode === CallMode.Group &&
call.peekInfo &&
isAnybodyElseInGroupCall(call.peekInfo, ourUuid);
if (!isOngoingGroupCall) {
dispatch({
type: SHOW_TOAST,
payload: {
toastType: ToastType.CannotStartGroupCall,
},
});
return;
}
}
if (await isCallSafe(conversation.attributes)) {
log.info(
'onOutgoingVideoCallInConversation: call is deemed "safe". Making call'
);
startCallingLobby({
conversationId,
isVideoCall: true,
})(dispatch, getState, undefined);
log.info('onOutgoingVideoCallInConversation: started the call');
} else {
log.info(
'onOutgoingVideoCallInConversation: call is deemed "unsafe". Stopping'
);
}
};
}
function onOutgoingAudioCallInConversation(
conversationId: string
): ThunkAction<void, RootStateType, unknown, StartCallingLobbyActionType> {
return async (dispatch, getState) => {
const conversation = window.ConversationController.get(conversationId);
if (!conversation) {
throw new Error(
`onOutgoingAudioCallInConversation: No conversation found for conversation ${conversationId}`
);
}
if (!isDirectConversation(conversation.attributes)) {
throw new Error(
`onOutgoingAudioCallInConversation: Conversation ${conversation.idForLogging()} is not 1:1`
);
}
log.info('onOutgoingAudioCallInConversation: about to start an audio call');
if (await isCallSafe(conversation.attributes)) {
log.info(
'onOutgoingAudioCallInConversation: call is deemed "safe". Making call'
);
startCallingLobby({
conversationId,
isVideoCall: false,
})(dispatch, getState, undefined);
log.info('onOutgoingAudioCallInConversation: started the call');
} else {
log.info(
'onOutgoingAudioCallInConversation: call is deemed "unsafe". Stopping'
);
}
};
}
function startCallingLobby({
conversationId,
isVideoCall,
@ -1346,6 +1450,8 @@ export const actions = {
hangUpActiveCall,
keyChangeOk,
keyChanged,
onOutgoingVideoCallInConversation,
onOutgoingAudioCallInConversation,
openSystemPreferencesAction,
outgoingCall,
peekGroupCallForTheFirstTime,

View file

@ -897,9 +897,11 @@ export const actions = {
setComposeGroupName,
setComposeSearchTerm,
setDisappearingMessages,
setDontNotifyForMentionsIfMuted,
setIsFetchingUUID,
setIsNearBottom,
setMessageLoadingState,
setMuteExpiration,
setPreJoinConversation,
setSelectedConversationHeaderTitle,
setSelectedConversationPanelDepth,
@ -943,7 +945,7 @@ async function getAvatarsAndUpdateConversation(
): Promise<Array<AvatarDataType>> {
const conversation = window.ConversationController.get(conversationId);
if (!conversation) {
throw new Error('No conversation found');
throw new Error('getAvatarsAndUpdateConversation: No conversation found');
}
const { conversationLookup } = conversations;
@ -1004,7 +1006,7 @@ function changeHasGroupLink(
return async dispatch => {
const conversation = window.ConversationController.get(conversationId);
if (!conversation) {
throw new Error('No conversation found');
throw new Error('changeHasGroupLink: No conversation found');
}
await longRunningTaskWrapper({
@ -1026,7 +1028,7 @@ function setAnnouncementsOnly(
return async dispatch => {
const conversation = window.ConversationController.get(conversationId);
if (!conversation) {
throw new Error('No conversation found');
throw new Error('setAnnouncementsOnly: No conversation found');
}
await longRunningTaskWrapper({
@ -1048,7 +1050,7 @@ function setAccessControlMembersSetting(
return async dispatch => {
const conversation = window.ConversationController.get(conversationId);
if (!conversation) {
throw new Error('No conversation found');
throw new Error('setAccessControlMembersSetting: No conversation found');
}
await longRunningTaskWrapper({
@ -1070,7 +1072,9 @@ function setAccessControlAttributesSetting(
return async dispatch => {
const conversation = window.ConversationController.get(conversationId);
if (!conversation) {
throw new Error('No conversation found');
throw new Error(
'setAccessControlAttributesSetting: No conversation found'
);
}
await longRunningTaskWrapper({
@ -1092,7 +1096,7 @@ function setDisappearingMessages(
return async dispatch => {
const conversation = window.ConversationController.get(conversationId);
if (!conversation) {
throw new Error('No conversation found');
throw new Error('setDisappearingMessages: No conversation found');
}
const valueToSet = seconds > 0 ? seconds : undefined;
@ -1112,13 +1116,51 @@ function setDisappearingMessages(
};
}
function setDontNotifyForMentionsIfMuted(
conversationId: string,
newValue: boolean
): NoopActionType {
const conversation = window.ConversationController.get(conversationId);
if (!conversation) {
throw new Error('setDontNotifyForMentionsIfMuted: No conversation found');
}
conversation.setDontNotifyForMentionsIfMuted(newValue);
return {
type: 'NOOP',
payload: null,
};
}
function setMuteExpiration(
conversationId: string,
muteExpiresAt = 0
): NoopActionType {
const conversation = window.ConversationController.get(conversationId);
if (!conversation) {
throw new Error('setMuteExpiration: No conversation found');
}
conversation.setMuteExpiration(
muteExpiresAt >= Number.MAX_SAFE_INTEGER
? muteExpiresAt
: Date.now() + muteExpiresAt
);
return {
type: 'NOOP',
payload: null,
};
}
function destroyMessages(
conversationId: string
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
return async dispatch => {
const conversation = window.ConversationController.get(conversationId);
if (!conversation) {
throw new Error('No conversation found');
throw new Error('destroyMessages: No conversation found');
}
await longRunningTaskWrapper({
@ -1144,7 +1186,7 @@ function generateNewGroupLink(
return async dispatch => {
const conversation = window.ConversationController.get(conversationId);
if (!conversation) {
throw new Error('No conversation found');
throw new Error('generateNewGroupLink: No conversation found');
}
await longRunningTaskWrapper({
@ -1217,7 +1259,9 @@ function setAccessControlAddFromInviteLinkSetting(
return async dispatch => {
const conversation = window.ConversationController.get(conversationId);
if (!conversation) {
throw new Error('No conversation found');
throw new Error(
'setAccessControlAddFromInviteLinkSetting: No conversation found'
);
}
await longRunningTaskWrapper({
@ -1280,7 +1324,7 @@ function saveAvatarToDisk(
): ThunkAction<void, RootStateType, unknown, ReplaceAvatarsActionType> {
return async (dispatch, getState) => {
if (!avatarData.buffer) {
throw new Error('No avatar Uint8Array provided');
throw new Error('saveAvatarToDisk: No avatar Uint8Array provided');
}
strictAssert(conversationId, 'conversationId not provided');

View file

@ -5,7 +5,13 @@ import { useBoundActions } from '../../hooks/useBoundActions';
import type { ReplacementValuesType } from '../../types/Util';
export enum ToastType {
AddingUserToGroup = 'AddingUserToGroup',
CannotStartGroupCall = 'CannotStartGroupCall',
CopiedUsername = 'CopiedUsername',
CopiedUsernameLink = 'CopiedUsernameLink',
DeleteForEveryoneFailed = 'DeleteForEveryoneFailed',
Error = 'Error',
FailedToDeleteUsername = 'FailedToDeleteUsername',
MessageBodyTooLong = 'MessageBodyTooLong',
StoryMuted = 'StoryMuted',
StoryReact = 'StoryReact',
@ -13,12 +19,7 @@ export enum ToastType {
StoryVideoError = 'StoryVideoError',
StoryVideoTooLong = 'StoryVideoTooLong',
StoryVideoUnsupported = 'StoryVideoUnsupported',
AddingUserToGroup = 'AddingUserToGroup',
UserAddedToGroup = 'UserAddedToGroup',
FailedToDeleteUsername = 'FailedToDeleteUsername',
CopiedUsername = 'CopiedUsername',
CopiedUsernameLink = 'CopiedUsernameLink',
DeleteForEveryoneFailed = 'DeleteForEveryoneFailed',
}
// State

View file

@ -57,9 +57,6 @@ export type SmartConversationDetailsProps = {
onBlock: () => void;
onLeave: () => void;
onUnblock: () => void;
setMuteExpiration: (muteExpiresAt: undefined | number) => unknown;
onOutgoingAudioCallInConversation: () => unknown;
onOutgoingVideoCallInConversation: () => unknown;
};
const ACCESS_ENUM = Proto.AccessControl.AccessRequired;

View file

@ -33,10 +33,7 @@ export type OwnProps = {
onGoBack: () => void;
onMarkUnread: () => void;
onMoveToInbox: () => void;
onOutgoingAudioCallInConversation: () => void;
onOutgoingVideoCallInConversation: () => void;
onSearchInConversation: () => void;
onSetMuteNotifications: (seconds: number) => void;
onSetPin: (value: boolean) => void;
onShowAllMedia: () => void;
onShowConversationDetails: () => void;

View file

@ -7,36 +7,31 @@ import type { StateType } from '../reducer';
import { getIntl } from '../selectors/user';
import { getConversationByIdSelector } from '../selectors/conversations';
import { strictAssert } from '../../util/assert';
import { mapDispatchToProps } from '../actions';
export type OwnProps = {
conversationId: string;
setDontNotifyForMentionsIfMuted: (
dontNotifyForMentionsIfMuted: boolean
) => unknown;
setMuteExpiration: (muteExpiresAt: undefined | number) => unknown;
};
const mapStateToProps = (state: StateType, props: OwnProps) => {
const { conversationId, setDontNotifyForMentionsIfMuted, setMuteExpiration } =
props;
const { conversationId } = props;
const conversationSelector = getConversationByIdSelector(state);
const conversation = conversationSelector(conversationId);
strictAssert(conversation, 'Expected a conversation to be found');
return {
id: conversationId,
conversationType: conversation.type,
dontNotifyForMentionsIfMuted: Boolean(
conversation.dontNotifyForMentionsIfMuted
),
i18n: getIntl(state),
muteExpiresAt: conversation.muteExpiresAt,
setDontNotifyForMentionsIfMuted,
setMuteExpiration,
};
};
const smart = connect(mapStateToProps, {});
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartConversationNotificationsSettings = smart(
ConversationNotificationsSettings

38
ts/util/isCallSafe.ts Normal file
View file

@ -0,0 +1,38 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ConversationAttributesType } from '../model-types';
import type { RecipientsByConversation } from '../state/ducks/stories';
import * as log from '../logging/log';
import { SafetyNumberChangeSource } from '../components/SafetyNumberChangeDialog';
import { blockSendUntilConversationsAreVerified } from './blockSendUntilConversationsAreVerified';
import { getConversationMembers } from './getConversationMembers';
import { UUID } from '../types/UUID';
import { isNotNil } from './isNotNil';
export async function isCallSafe(
attributes: ConversationAttributesType
): Promise<boolean> {
const recipientsByConversation: RecipientsByConversation = {
[attributes.id]: {
uuids: getConversationMembers(attributes)
.map(member =>
member.uuid ? UUID.checkedLookup(member.uuid).toString() : undefined
)
.filter(isNotNil),
},
};
const callAnyway = await blockSendUntilConversationsAreVerified(
recipientsByConversation,
SafetyNumberChangeSource.Calling
);
if (!callAnyway) {
log.info('Safety number change dialog not accepted, new call not allowed.');
return false;
}
return true;
}

View file

@ -13,7 +13,6 @@ import type {
ToastCannotOpenGiftBadge,
ToastPropsType as ToastCannotOpenGiftBadgePropsType,
} from '../components/ToastCannotOpenGiftBadge';
import type { ToastCannotStartGroupCall } from '../components/ToastCannotStartGroupCall';
import type { ToastCaptchaFailed } from '../components/ToastCaptchaFailed';
import type { ToastCaptchaSolved } from '../components/ToastCaptchaSolved';
import type {
@ -60,7 +59,6 @@ export function showToast(Toast: typeof ToastAlreadyRequestedToJoin): void;
export function showToast(Toast: typeof ToastBlocked): void;
export function showToast(Toast: typeof ToastBlockedGroup): void;
export function showToast(Toast: typeof ToastUnsupportedMultiAttachment): void;
export function showToast(Toast: typeof ToastCannotStartGroupCall): void;
export function showToast(
Toast: typeof ToastCannotOpenGiftBadge,
props: Omit<ToastCannotOpenGiftBadgePropsType, 'i18n' | 'onClose'>

View file

@ -61,7 +61,6 @@ import { SignalService as Proto } from '../protobuf';
import { ToastBlocked } from '../components/ToastBlocked';
import { ToastBlockedGroup } from '../components/ToastBlockedGroup';
import { ToastCannotMixMultiAndNonMultiAttachments } from '../components/ToastCannotMixMultiAndNonMultiAttachments';
import { ToastCannotStartGroupCall } from '../components/ToastCannotStartGroupCall';
import { ToastConversationArchived } from '../components/ToastConversationArchived';
import { ToastConversationMarkedUnread } from '../components/ToastConversationMarkedUnread';
import { ToastConversationUnarchived } from '../components/ToastConversationUnarchived';
@ -108,10 +107,8 @@ import { saveAttachment } from '../util/saveAttachment';
import { SECOND } from '../util/durations';
import { blockSendUntilConversationsAreVerified } from '../util/blockSendUntilConversationsAreVerified';
import { SafetyNumberChangeSource } from '../components/SafetyNumberChangeDialog';
import { getOwn } from '../util/getOwn';
import { CallMode } from '../types/Calling';
import { isAnybodyElseInGroupCall } from '../state/ducks/calling';
import { startConversation } from '../util/startConversation';
import { longRunningTaskWrapper } from '../util/longRunningTaskWrapper';
type AttachmentOptions = {
messageId: string;
@ -306,12 +303,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
return this;
}
setMuteExpiration(ms = 0): void {
this.model.setMuteExpiration(
ms >= Number.MAX_SAFE_INTEGER ? ms : Date.now() + ms
);
}
setPin(value: boolean): void {
if (value) {
const pinnedConversationIds = window.storage.get(
@ -338,15 +329,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
const { searchInConversation } = window.reduxActions.search;
searchInConversation(this.model.id);
},
onSetMuteNotifications: this.setMuteExpiration.bind(this),
onSetPin: this.setPin.bind(this),
// These are view only and don't update the Conversation model, so they
// need a manual update call.
onOutgoingAudioCallInConversation:
this.onOutgoingAudioCallInConversation.bind(this),
onOutgoingVideoCallInConversation:
this.onOutgoingVideoCallInConversation.bind(this),
onShowConversationDetails: () => {
this.showConversationDetails();
},
@ -505,7 +488,8 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
messageRequestEnum.ACCEPT
),
removeMember: (conversationId: string) => {
this.longRunningTaskWrapper({
longRunningTaskWrapper({
idForLogging: this.model.idForLogging(),
name: 'removeMember',
task: () => this.model.removeFromGroupV2(conversationId),
});
@ -575,7 +559,8 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
okText: window.i18n('GroupV2--join--cancel-request-to-join--yes'),
cancelText: window.i18n('GroupV2--join--cancel-request-to-join--no'),
resolve: () => {
this.longRunningTaskWrapper({
longRunningTaskWrapper({
idForLogging: this.model.idForLogging(),
name: 'onCancelJoinRequest',
task: async () => this.model.cancelJoinRequest(),
});
@ -630,83 +615,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
this.$('.ConversationView__template').append(this.conversationView.el);
}
async onOutgoingVideoCallInConversation(): Promise<void> {
log.info('onOutgoingVideoCallInConversation: about to start a video call');
// if it's a group call on an announcementsOnly group
// only allow join if the call has already been started (presumably by the admin)
if (this.model.get('announcementsOnly') && !this.model.areWeAdmin()) {
const call = getOwn(
window.reduxStore.getState().calling.callsByConversation,
this.model.id
);
// technically not necessary, but isAnybodyElseInGroupCall requires it
const ourUuid = window.storage.user.getCheckedUuid().toString();
const isOngoingGroupCall =
call &&
ourUuid &&
call.callMode === CallMode.Group &&
call.peekInfo &&
isAnybodyElseInGroupCall(call.peekInfo, ourUuid);
if (!isOngoingGroupCall) {
showToast(ToastCannotStartGroupCall);
return;
}
}
if (await this.isCallSafe()) {
log.info(
'onOutgoingVideoCallInConversation: call is deemed "safe". Making call'
);
window.reduxActions.calling.startCallingLobby({
conversationId: this.model.id,
isVideoCall: true,
});
log.info('onOutgoingVideoCallInConversation: started the call');
} else {
log.info(
'onOutgoingVideoCallInConversation: call is deemed "unsafe". Stopping'
);
}
}
async onOutgoingAudioCallInConversation(): Promise<void> {
log.info('onOutgoingAudioCallInConversation: about to start an audio call');
if (await this.isCallSafe()) {
log.info(
'onOutgoingAudioCallInConversation: call is deemed "safe". Making call'
);
window.reduxActions.calling.startCallingLobby({
conversationId: this.model.id,
isVideoCall: false,
});
log.info('onOutgoingAudioCallInConversation: started the call');
} else {
log.info(
'onOutgoingAudioCallInConversation: call is deemed "unsafe". Stopping'
);
}
}
async longRunningTaskWrapper<T>({
name,
task,
}: {
name: string;
task: () => Promise<T>;
}): Promise<T> {
const idForLogging = this.model.idForLogging();
return window.Signal.Util.longRunningTaskWrapper({
name,
idForLogging,
task,
});
}
getMessageActions(): MessageActionsType {
const reactToMessage = async (
messageId: string,
@ -896,7 +804,8 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
const migrate = () => {
onClose();
this.longRunningTaskWrapper({
longRunningTaskWrapper({
idForLogging: this.model.idForLogging(),
name: 'initiateMigrationToGroupV2',
task: () => window.Signal.Groups.initiateMigrationToGroupV2(this.model),
});
@ -905,7 +814,8 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
// Note: this call will throw if, after generating member lists, we are no longer a
// member or are in the pending member list.
const { droppedGV2MemberIds, pendingMembersV2 } =
await this.longRunningTaskWrapper({
await longRunningTaskWrapper({
idForLogging: this.model.idForLogging(),
name: 'getGroupMigrationMembers',
task: () => window.Signal.Groups.getGroupMigrationMembers(this.model),
});
@ -1103,7 +1013,8 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
model: ConversationModel,
messageRequestType: number
): Promise<void> {
return this.longRunningTaskWrapper({
return longRunningTaskWrapper({
idForLogging: this.model.idForLogging(),
name,
task: model.syncMessageRequestResponse.bind(model, messageRequestType),
});
@ -1112,7 +1023,8 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
blockAndReportSpam(model: ConversationModel): Promise<void> {
const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type;
return this.longRunningTaskWrapper({
return longRunningTaskWrapper({
idForLogging: this.model.idForLogging(),
name: 'blockAndReportSpam',
task: async () => {
await Promise.all([
@ -1862,9 +1774,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
window.reduxStore,
{
conversationId: this.model.id,
setDontNotifyForMentionsIfMuted:
this.model.setDontNotifyForMentionsIfMuted.bind(this.model),
setMuteExpiration: this.setMuteExpiration.bind(this),
}
),
});
@ -1900,7 +1809,8 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
// dried up and hoisted to methods on ConversationView
const onLeave = () => {
this.longRunningTaskWrapper({
longRunningTaskWrapper({
idForLogging: this.model.idForLogging(),
name: 'onLeave',
task: () => this.model.leaveGroupV2(),
});
@ -1938,11 +1848,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
messageRequestEnum.ACCEPT
);
},
setMuteExpiration: this.setMuteExpiration.bind(this),
onOutgoingAudioCallInConversation:
this.onOutgoingAudioCallInConversation.bind(this),
onOutgoingVideoCallInConversation:
this.onOutgoingVideoCallInConversation.bind(this),
};
const view = new ReactWrapperView({
@ -2128,28 +2033,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
);
}
async isCallSafe(): Promise<boolean> {
const recipientsByConversation = {
[this.model.id]: {
uuids: this.model.getMemberUuids().map(uuid => uuid.toString()),
},
};
const callAnyway = await blockSendUntilConversationsAreVerified(
recipientsByConversation,
SafetyNumberChangeSource.Calling
);
if (!callAnyway) {
log.info(
'Safety number change dialog not accepted, new call not allowed.'
);
return false;
}
return true;
}
async sendStickerMessage(options: {
packId: string;
stickerId: number;