Moves parts of conversation view into redux
This commit is contained in:
parent
a49a6f2057
commit
9348940ecf
27 changed files with 693 additions and 461 deletions
|
@ -1,28 +0,0 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import { action } from '@storybook/addon-actions';
|
|
||||||
import { ToastDeleteForEveryoneFailed } from './ToastDeleteForEveryoneFailed';
|
|
||||||
|
|
||||||
import { setupI18n } from '../util/setupI18n';
|
|
||||||
import enMessages from '../../_locales/en/messages.json';
|
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
|
||||||
|
|
||||||
const defaultProps = {
|
|
||||||
i18n,
|
|
||||||
onClose: action('onClose'),
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
title: 'Components/ToastDeleteForEveryoneFailed',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const _ToastDeleteForEveryoneFailed = (): JSX.Element => (
|
|
||||||
<ToastDeleteForEveryoneFailed {...defaultProps} />
|
|
||||||
);
|
|
||||||
|
|
||||||
_ToastDeleteForEveryoneFailed.story = {
|
|
||||||
name: 'ToastDeleteForEveryoneFailed',
|
|
||||||
};
|
|
|
@ -1,18 +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 ToastDeleteForEveryoneFailed({
|
|
||||||
i18n,
|
|
||||||
onClose,
|
|
||||||
}: PropsType): JSX.Element {
|
|
||||||
return <Toast onClose={onClose}>{i18n('deleteForEveryoneFailed')}</Toast>;
|
|
||||||
}
|
|
|
@ -143,5 +143,9 @@ export function ToastManager({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (toastType === ToastType.DeleteForEveryoneFailed) {
|
||||||
|
return <Toast onClose={hideToast}>{i18n('deleteForEveryoneFailed')}</Toast>;
|
||||||
|
}
|
||||||
|
|
||||||
throw missingCaseError(toastType);
|
throw missingCaseError(toastType);
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,8 +38,8 @@ const commonProps = {
|
||||||
i18n,
|
i18n,
|
||||||
|
|
||||||
onShowConversationDetails: action('onShowConversationDetails'),
|
onShowConversationDetails: action('onShowConversationDetails'),
|
||||||
onSetDisappearingMessages: action('onSetDisappearingMessages'),
|
setDisappearingMessages: action('setDisappearingMessages'),
|
||||||
onDeleteMessages: action('onDeleteMessages'),
|
destroyMessages: action('destroyMessages'),
|
||||||
onSearchInConversation: action('onSearchInConversation'),
|
onSearchInConversation: action('onSearchInConversation'),
|
||||||
onSetMuteNotifications: action('onSetMuteNotifications'),
|
onSetMuteNotifications: action('onSetMuteNotifications'),
|
||||||
onOutgoingAudioCallInConversation: action(
|
onOutgoingAudioCallInConversation: action(
|
||||||
|
|
|
@ -28,6 +28,7 @@ import * as expirationTimer from '../../util/expirationTimer';
|
||||||
import { missingCaseError } from '../../util/missingCaseError';
|
import { missingCaseError } from '../../util/missingCaseError';
|
||||||
import { isInSystemContacts } from '../../util/isInSystemContacts';
|
import { isInSystemContacts } from '../../util/isInSystemContacts';
|
||||||
import { isConversationMuted } from '../../util/isConversationMuted';
|
import { isConversationMuted } from '../../util/isConversationMuted';
|
||||||
|
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||||
import { DurationInSeconds } from '../../util/durations';
|
import { DurationInSeconds } from '../../util/durations';
|
||||||
import {
|
import {
|
||||||
useStartCallShortcuts,
|
useStartCallShortcuts,
|
||||||
|
@ -80,8 +81,7 @@ export type PropsDataType = {
|
||||||
|
|
||||||
export type PropsActionsType = {
|
export type PropsActionsType = {
|
||||||
onSetMuteNotifications: (seconds: number) => void;
|
onSetMuteNotifications: (seconds: number) => void;
|
||||||
onSetDisappearingMessages: (seconds: DurationInSeconds) => void;
|
destroyMessages: (conversationId: string) => void;
|
||||||
onDeleteMessages: () => void;
|
|
||||||
onSearchInConversation: () => void;
|
onSearchInConversation: () => void;
|
||||||
onOutgoingAudioCallInConversation: () => void;
|
onOutgoingAudioCallInConversation: () => void;
|
||||||
onOutgoingVideoCallInConversation: () => void;
|
onOutgoingVideoCallInConversation: () => void;
|
||||||
|
@ -95,6 +95,10 @@ export type PropsActionsType = {
|
||||||
onArchive: () => void;
|
onArchive: () => void;
|
||||||
onMarkUnread: () => void;
|
onMarkUnread: () => void;
|
||||||
onMoveToInbox: () => void;
|
onMoveToInbox: () => void;
|
||||||
|
setDisappearingMessages: (
|
||||||
|
conversationId: string,
|
||||||
|
seconds: DurationInSeconds
|
||||||
|
) => void;
|
||||||
viewUserStories: ViewUserStoriesActionCreatorType;
|
viewUserStories: ViewUserStoriesActionCreatorType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -112,6 +116,7 @@ enum ModalState {
|
||||||
}
|
}
|
||||||
|
|
||||||
type StateType = {
|
type StateType = {
|
||||||
|
hasDeleteMessagesConfirmation: boolean;
|
||||||
isNarrow: boolean;
|
isNarrow: boolean;
|
||||||
modalState: ModalState;
|
modalState: ModalState;
|
||||||
};
|
};
|
||||||
|
@ -130,7 +135,11 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
||||||
public constructor(props: PropsType) {
|
public constructor(props: PropsType) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = { isNarrow: false, modalState: ModalState.NothingOpen };
|
this.state = {
|
||||||
|
hasDeleteMessagesConfirmation: false,
|
||||||
|
isNarrow: false,
|
||||||
|
modalState: ModalState.NothingOpen,
|
||||||
|
};
|
||||||
|
|
||||||
this.menuTriggerRef = React.createRef();
|
this.menuTriggerRef = React.createRef();
|
||||||
this.headerRef = React.createRef();
|
this.headerRef = React.createRef();
|
||||||
|
@ -329,6 +338,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
||||||
expireTimer,
|
expireTimer,
|
||||||
groupVersion,
|
groupVersion,
|
||||||
i18n,
|
i18n,
|
||||||
|
id,
|
||||||
isArchived,
|
isArchived,
|
||||||
isMissingMandatoryProfileSharing,
|
isMissingMandatoryProfileSharing,
|
||||||
isPinned,
|
isPinned,
|
||||||
|
@ -337,15 +347,14 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
||||||
markedUnread,
|
markedUnread,
|
||||||
muteExpiresAt,
|
muteExpiresAt,
|
||||||
onArchive,
|
onArchive,
|
||||||
onDeleteMessages,
|
|
||||||
onMarkUnread,
|
onMarkUnread,
|
||||||
onMoveToInbox,
|
onMoveToInbox,
|
||||||
onSetDisappearingMessages,
|
|
||||||
onSetMuteNotifications,
|
onSetMuteNotifications,
|
||||||
onSetPin,
|
onSetPin,
|
||||||
onShowAllMedia,
|
onShowAllMedia,
|
||||||
onShowConversationDetails,
|
onShowConversationDetails,
|
||||||
onShowGroupMembers,
|
onShowGroupMembers,
|
||||||
|
setDisappearingMessages,
|
||||||
type,
|
type,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
@ -425,7 +434,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
||||||
modalState: ModalState.CustomDisappearingTimeout,
|
modalState: ModalState.CustomDisappearingTimeout,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
onSetDisappearingMessages(seconds);
|
setDisappearingMessages(id, seconds);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -487,7 +496,11 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
||||||
) : (
|
) : (
|
||||||
<MenuItem onClick={onArchive}>{i18n('archiveConversation')}</MenuItem>
|
<MenuItem onClick={onArchive}>{i18n('archiveConversation')}</MenuItem>
|
||||||
)}
|
)}
|
||||||
<MenuItem onClick={onDeleteMessages}>{i18n('deleteMessages')}</MenuItem>
|
<MenuItem
|
||||||
|
onClick={() => this.setState({ hasDeleteMessagesConfirmation: true })}
|
||||||
|
>
|
||||||
|
{i18n('deleteMessages')}
|
||||||
|
</MenuItem>
|
||||||
{isPinned ? (
|
{isPinned ? (
|
||||||
<MenuItem onClick={() => onSetPin(false)}>
|
<MenuItem onClick={() => onSetPin(false)}>
|
||||||
{i18n('unpinConversation')}
|
{i18n('unpinConversation')}
|
||||||
|
@ -501,6 +514,37 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderConfirmationDialog(): ReactNode {
|
||||||
|
const { hasDeleteMessagesConfirmation } = this.state;
|
||||||
|
const { destroyMessages, i18n, id } = this.props;
|
||||||
|
|
||||||
|
if (!hasDeleteMessagesConfirmation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfirmationDialog
|
||||||
|
dialogName="ConversationHeader.destroyMessages"
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
action: () => {
|
||||||
|
this.setState({ hasDeleteMessagesConfirmation: false });
|
||||||
|
destroyMessages(id);
|
||||||
|
},
|
||||||
|
style: 'negative',
|
||||||
|
text: i18n('delete'),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
i18n={i18n}
|
||||||
|
onClose={() => {
|
||||||
|
this.setState({ hasDeleteMessagesConfirmation: false });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{i18n('deleteConversationConfirmation')}
|
||||||
|
</ConfirmationDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private renderHeader(): ReactNode {
|
private renderHeader(): ReactNode {
|
||||||
const { conversationTitle, groupVersion, onShowConversationDetails, type } =
|
const { conversationTitle, groupVersion, onShowConversationDetails, type } =
|
||||||
this.props;
|
this.props;
|
||||||
|
@ -579,8 +623,8 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
||||||
isSignalConversation,
|
isSignalConversation,
|
||||||
onOutgoingAudioCallInConversation,
|
onOutgoingAudioCallInConversation,
|
||||||
onOutgoingVideoCallInConversation,
|
onOutgoingVideoCallInConversation,
|
||||||
onSetDisappearingMessages,
|
|
||||||
outgoingCallButtonStyle,
|
outgoingCallButtonStyle,
|
||||||
|
setDisappearingMessages,
|
||||||
showBackButton,
|
showBackButton,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { isNarrow, modalState } = this.state;
|
const { isNarrow, modalState } = this.state;
|
||||||
|
@ -596,7 +640,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
||||||
initialValue={expireTimer}
|
initialValue={expireTimer}
|
||||||
onSubmit={value => {
|
onSubmit={value => {
|
||||||
this.setState({ modalState: ModalState.NothingOpen });
|
this.setState({ modalState: ModalState.NothingOpen });
|
||||||
onSetDisappearingMessages(value);
|
setDisappearingMessages(id, value);
|
||||||
}}
|
}}
|
||||||
onClose={() => this.setState({ modalState: ModalState.NothingOpen })}
|
onClose={() => this.setState({ modalState: ModalState.NothingOpen })}
|
||||||
/>
|
/>
|
||||||
|
@ -608,6 +652,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{modalNode}
|
{modalNode}
|
||||||
|
{this.renderConfirmationDialog()}
|
||||||
<Measure
|
<Measure
|
||||||
bounds
|
bounds
|
||||||
onResize={({ bounds }) => {
|
onResize={({ bounds }) => {
|
||||||
|
|
|
@ -326,7 +326,7 @@ export class MessageDetail extends React.Component<Props> {
|
||||||
displayLimit={Number.MAX_SAFE_INTEGER}
|
displayLimit={Number.MAX_SAFE_INTEGER}
|
||||||
displayTapToViewMessage={displayTapToViewMessage}
|
displayTapToViewMessage={displayTapToViewMessage}
|
||||||
downloadAttachment={() =>
|
downloadAttachment={() =>
|
||||||
log.warn('MessageDetail: deleteMessageForEveryone called!')
|
log.warn('MessageDetail: downloadAttachment called!')
|
||||||
}
|
}
|
||||||
doubleCheckMissingQuoteReference={doubleCheckMissingQuoteReference}
|
doubleCheckMissingQuoteReference={doubleCheckMissingQuoteReference}
|
||||||
getPreferredBadge={getPreferredBadge}
|
getPreferredBadge={getPreferredBadge}
|
||||||
|
@ -355,7 +355,7 @@ export class MessageDetail extends React.Component<Props> {
|
||||||
showExpiredOutgoingTapToViewToast
|
showExpiredOutgoingTapToViewToast
|
||||||
}
|
}
|
||||||
showMessageDetail={() => {
|
showMessageDetail={() => {
|
||||||
log.warn('MessageDetail: deleteMessageForEveryone called!');
|
log.warn('MessageDetail: showMessageDetail called!');
|
||||||
}}
|
}}
|
||||||
showVisualAttachment={showVisualAttachment}
|
showVisualAttachment={showVisualAttachment}
|
||||||
startConversation={startConversation}
|
startConversation={startConversation}
|
||||||
|
|
|
@ -26,6 +26,7 @@ import type {
|
||||||
} from './Message';
|
} from './Message';
|
||||||
import { doesMessageBodyOverflow } from './MessageBodyReadMore';
|
import { doesMessageBodyOverflow } from './MessageBodyReadMore';
|
||||||
import type { Props as ReactionPickerProps } from './ReactionPicker';
|
import type { Props as ReactionPickerProps } from './ReactionPicker';
|
||||||
|
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||||
import { useToggleReactionPicker } from '../../hooks/useKeyboardShortcuts';
|
import { useToggleReactionPicker } from '../../hooks/useKeyboardShortcuts';
|
||||||
|
|
||||||
export type PropsData = {
|
export type PropsData = {
|
||||||
|
@ -237,6 +238,8 @@ export function TimelineMessage(props: Props): JSX.Element {
|
||||||
|
|
||||||
const handleReact = canReact ? () => toggleReactionPicker() : undefined;
|
const handleReact = canReact ? () => toggleReactionPicker() : undefined;
|
||||||
|
|
||||||
|
const [hasDOEConfirmation, setHasDOEConfirmation] = useState(false);
|
||||||
|
|
||||||
const toggleReactionPickerKeyboard = useToggleReactionPicker(
|
const toggleReactionPickerKeyboard = useToggleReactionPicker(
|
||||||
handleReact || noop
|
handleReact || noop
|
||||||
);
|
);
|
||||||
|
@ -253,6 +256,22 @@ export function TimelineMessage(props: Props): JSX.Element {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{hasDOEConfirmation && canDeleteForEveryone && (
|
||||||
|
<ConfirmationDialog
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
action: () => deleteMessageForEveryone(id),
|
||||||
|
style: 'negative',
|
||||||
|
text: i18n('delete'),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
dialogName="TimelineMessage/deleteMessageForEveryone"
|
||||||
|
i18n={i18n}
|
||||||
|
onClose={() => setHasDOEConfirmation(false)}
|
||||||
|
>
|
||||||
|
{i18n('deleteForEveryoneWarning')}
|
||||||
|
</ConfirmationDialog>
|
||||||
|
)}
|
||||||
<Message
|
<Message
|
||||||
{...props}
|
{...props}
|
||||||
renderingContext="conversation/TimelineItem"
|
renderingContext="conversation/TimelineItem"
|
||||||
|
@ -319,7 +338,7 @@ export function TimelineMessage(props: Props): JSX.Element {
|
||||||
onForward={canForward ? () => showForwardMessageModal(id) : undefined}
|
onForward={canForward ? () => showForwardMessageModal(id) : undefined}
|
||||||
onDeleteForMe={() => deleteMessage(id)}
|
onDeleteForMe={() => deleteMessage(id)}
|
||||||
onDeleteForEveryone={
|
onDeleteForEveryone={
|
||||||
canDeleteForEveryone ? () => deleteMessageForEveryone(id) : undefined
|
canDeleteForEveryone ? () => setHasDOEConfirmation(true) : undefined
|
||||||
}
|
}
|
||||||
onMoreInfo={() => showMessageDetail(id)}
|
onMoreInfo={() => showMessageDetail(id)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -73,14 +73,12 @@ export type StateProps = {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
isGroup: boolean;
|
isGroup: boolean;
|
||||||
loadRecentMediaItems: (limit: number) => void;
|
|
||||||
groupsInCommon: Array<ConversationType>;
|
groupsInCommon: Array<ConversationType>;
|
||||||
maxGroupSize: number;
|
maxGroupSize: number;
|
||||||
maxRecommendedGroupSize: number;
|
maxRecommendedGroupSize: number;
|
||||||
memberships: Array<GroupV2Membership>;
|
memberships: Array<GroupV2Membership>;
|
||||||
pendingApprovalMemberships: ReadonlyArray<GroupV2RequestingMembership>;
|
pendingApprovalMemberships: ReadonlyArray<GroupV2RequestingMembership>;
|
||||||
pendingMemberships: ReadonlyArray<GroupV2PendingMembership>;
|
pendingMemberships: ReadonlyArray<GroupV2PendingMembership>;
|
||||||
setDisappearingMessages: (seconds: DurationInSeconds) => void;
|
|
||||||
showAllMedia: () => void;
|
showAllMedia: () => void;
|
||||||
showChatColorEditor: () => void;
|
showChatColorEditor: () => void;
|
||||||
showGroupLinkManagement: () => void;
|
showGroupLinkManagement: () => void;
|
||||||
|
@ -116,13 +114,15 @@ export type StateProps = {
|
||||||
|
|
||||||
type ActionProps = {
|
type ActionProps = {
|
||||||
deleteAvatarFromDisk: DeleteAvatarFromDiskActionType;
|
deleteAvatarFromDisk: DeleteAvatarFromDiskActionType;
|
||||||
|
loadRecentMediaItems: (id: string, limit: number) => void;
|
||||||
replaceAvatar: ReplaceAvatarActionType;
|
replaceAvatar: ReplaceAvatarActionType;
|
||||||
saveAvatarToDisk: SaveAvatarToDiskActionType;
|
saveAvatarToDisk: SaveAvatarToDiskActionType;
|
||||||
|
searchInConversation: (id: string) => unknown;
|
||||||
|
setDisappearingMessages: (id: string, seconds: DurationInSeconds) => void;
|
||||||
showContactModal: (contactId: string, conversationId?: string) => void;
|
showContactModal: (contactId: string, conversationId?: string) => void;
|
||||||
showConversation: ShowConversationType;
|
showConversation: ShowConversationType;
|
||||||
toggleSafetyNumberModal: (conversationId: string) => unknown;
|
|
||||||
searchInConversation: (id: string) => unknown;
|
|
||||||
toggleAddUserToAnotherGroupModal: (contactId?: string) => void;
|
toggleAddUserToAnotherGroupModal: (contactId?: string) => void;
|
||||||
|
toggleSafetyNumberModal: (conversationId: string) => unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Props = StateProps & ActionProps;
|
export type Props = StateProps & ActionProps;
|
||||||
|
@ -412,7 +412,9 @@ export function ConversationDetails({
|
||||||
<DisappearingTimerSelect
|
<DisappearingTimerSelect
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
value={conversation.expireTimer || DurationInSeconds.ZERO}
|
value={conversation.expireTimer || DurationInSeconds.ZERO}
|
||||||
onChange={setDisappearingMessages}
|
onChange={value =>
|
||||||
|
setDisappearingMessages(conversation.id, value)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { MediaGridItem } from '../media-gallery/MediaGridItem';
|
||||||
export type Props = {
|
export type Props = {
|
||||||
conversation: ConversationType;
|
conversation: ConversationType;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
loadRecentMediaItems: (limit: number) => void;
|
loadRecentMediaItems: (id: string, limit: number) => void;
|
||||||
showAllMedia: () => void;
|
showAllMedia: () => void;
|
||||||
showLightboxForMedia: (
|
showLightboxForMedia: (
|
||||||
selectedMediaItem: MediaItemType,
|
selectedMediaItem: MediaItemType,
|
||||||
|
@ -39,8 +39,8 @@ export function ConversationDetailsMediaList({
|
||||||
const mediaItemsLength = mediaItems.length;
|
const mediaItemsLength = mediaItems.length;
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
loadRecentMediaItems(MEDIA_ITEM_LIMIT);
|
loadRecentMediaItems(conversation.id, MEDIA_ITEM_LIMIT);
|
||||||
}, [loadRecentMediaItems, mediaItemsLength]);
|
}, [conversation.id, loadRecentMediaItems, mediaItemsLength]);
|
||||||
|
|
||||||
if (mediaItemsLength === 0) {
|
if (mediaItemsLength === 0) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -13,14 +13,19 @@ import { PanelSection } from './PanelSection';
|
||||||
import { Select } from '../../Select';
|
import { Select } from '../../Select';
|
||||||
import { useUniqueId } from '../../../hooks/useUniqueId';
|
import { useUniqueId } from '../../../hooks/useUniqueId';
|
||||||
|
|
||||||
export type PropsType = {
|
export type PropsDataType = {
|
||||||
conversation?: ConversationType;
|
conversation?: ConversationType;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
setAccessControlAttributesSetting: (value: number) => void;
|
|
||||||
setAccessControlMembersSetting: (value: number) => void;
|
|
||||||
setAnnouncementsOnly: (value: boolean) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type PropsActionType = {
|
||||||
|
setAccessControlAttributesSetting: (id: string, value: number) => void;
|
||||||
|
setAccessControlMembersSetting: (id: string, value: number) => void;
|
||||||
|
setAnnouncementsOnly: (id: string, value: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PropsType = PropsDataType & PropsActionType;
|
||||||
|
|
||||||
export function GroupV2Permissions({
|
export function GroupV2Permissions({
|
||||||
conversation,
|
conversation,
|
||||||
i18n,
|
i18n,
|
||||||
|
@ -37,14 +42,17 @@ export function GroupV2Permissions({
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateAccessControlAttributes = (value: string) => {
|
const updateAccessControlAttributes = (value: string) => {
|
||||||
setAccessControlAttributesSetting(Number(value));
|
setAccessControlAttributesSetting(conversation.id, Number(value));
|
||||||
};
|
};
|
||||||
const updateAccessControlMembers = (value: string) => {
|
const updateAccessControlMembers = (value: string) => {
|
||||||
setAccessControlMembersSetting(Number(value));
|
setAccessControlMembersSetting(conversation.id, Number(value));
|
||||||
};
|
};
|
||||||
const AccessControlEnum = Proto.AccessControl.AccessRequired;
|
const AccessControlEnum = Proto.AccessControl.AccessRequired;
|
||||||
const updateAnnouncementsOnly = (value: string) => {
|
const updateAnnouncementsOnly = (value: string) => {
|
||||||
setAnnouncementsOnly(Number(value) === AccessControlEnum.ADMINISTRATOR);
|
setAnnouncementsOnly(
|
||||||
|
conversation.id,
|
||||||
|
Number(value) === AccessControlEnum.ADMINISTRATOR
|
||||||
|
);
|
||||||
};
|
};
|
||||||
const accessControlOptions = getAccessControlOptions(i18n);
|
const accessControlOptions = getAccessControlOptions(i18n);
|
||||||
const announcementsOnlyValue = String(
|
const announcementsOnlyValue = String(
|
||||||
|
|
|
@ -48,7 +48,9 @@ const conversation: ConversationType = {
|
||||||
const OUR_UUID = UUID.generate().toString();
|
const OUR_UUID = UUID.generate().toString();
|
||||||
|
|
||||||
const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
approvePendingMembership: action('approvePendingMembership'),
|
approvePendingMembershipFromGroupV2: action(
|
||||||
|
'approvePendingMembershipFromGroupV2'
|
||||||
|
),
|
||||||
conversation,
|
conversation,
|
||||||
getPreferredBadge: () => undefined,
|
getPreferredBadge: () => undefined,
|
||||||
i18n,
|
i18n,
|
||||||
|
@ -70,7 +72,9 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
],
|
],
|
||||||
revokePendingMemberships: action('revokePendingMemberships'),
|
revokePendingMembershipsFromGroupV2: action(
|
||||||
|
'revokePendingMembershipsFromGroupV2'
|
||||||
|
),
|
||||||
theme: React.useContext(StorybookThemeContext),
|
theme: React.useContext(StorybookThemeContext),
|
||||||
...overrideProps,
|
...overrideProps,
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,18 +17,29 @@ import { ConversationDetailsIcon, IconType } from './ConversationDetailsIcon';
|
||||||
import { isAccessControlEnabled } from '../../../groups/util';
|
import { isAccessControlEnabled } from '../../../groups/util';
|
||||||
import { assertDev } from '../../../util/assert';
|
import { assertDev } from '../../../util/assert';
|
||||||
|
|
||||||
export type PropsType = {
|
export type PropsDataType = {
|
||||||
readonly conversation?: ConversationType;
|
readonly conversation?: ConversationType;
|
||||||
readonly getPreferredBadge: PreferredBadgeSelectorType;
|
readonly getPreferredBadge: PreferredBadgeSelectorType;
|
||||||
readonly i18n: LocalizerType;
|
readonly i18n: LocalizerType;
|
||||||
readonly ourUuid: UUIDStringType;
|
readonly ourUuid: UUIDStringType;
|
||||||
readonly pendingApprovalMemberships: ReadonlyArray<GroupV2RequestingMembership>;
|
readonly pendingApprovalMemberships: ReadonlyArray<GroupV2RequestingMembership>;
|
||||||
readonly pendingMemberships: ReadonlyArray<GroupV2PendingMembership>;
|
readonly pendingMemberships: ReadonlyArray<GroupV2PendingMembership>;
|
||||||
readonly approvePendingMembership: (conversationId: string) => void;
|
|
||||||
readonly revokePendingMemberships: (conversationIds: Array<string>) => void;
|
|
||||||
readonly theme: ThemeType;
|
readonly theme: ThemeType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type PropsActionType = {
|
||||||
|
readonly approvePendingMembershipFromGroupV2: (
|
||||||
|
conversationId: string,
|
||||||
|
memberId: string
|
||||||
|
) => void;
|
||||||
|
readonly revokePendingMembershipsFromGroupV2: (
|
||||||
|
conversationId: string,
|
||||||
|
memberIds: Array<string>
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PropsType = PropsDataType & PropsActionType;
|
||||||
|
|
||||||
export type GroupV2PendingMembership = {
|
export type GroupV2PendingMembership = {
|
||||||
metadata: {
|
metadata: {
|
||||||
addedByUserId?: UUIDStringType;
|
addedByUserId?: UUIDStringType;
|
||||||
|
@ -57,14 +68,14 @@ type StagedMembershipType = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export function PendingInvites({
|
export function PendingInvites({
|
||||||
approvePendingMembership,
|
approvePendingMembershipFromGroupV2,
|
||||||
conversation,
|
conversation,
|
||||||
getPreferredBadge,
|
getPreferredBadge,
|
||||||
i18n,
|
i18n,
|
||||||
ourUuid,
|
ourUuid,
|
||||||
pendingMemberships,
|
pendingMemberships,
|
||||||
pendingApprovalMemberships,
|
pendingApprovalMemberships,
|
||||||
revokePendingMemberships,
|
revokePendingMembershipsFromGroupV2,
|
||||||
theme,
|
theme,
|
||||||
}: PropsType): JSX.Element {
|
}: PropsType): JSX.Element {
|
||||||
if (!conversation || !ourUuid) {
|
if (!conversation || !ourUuid) {
|
||||||
|
@ -148,13 +159,17 @@ export function PendingInvites({
|
||||||
|
|
||||||
{stagedMemberships && stagedMemberships.length && (
|
{stagedMemberships && stagedMemberships.length && (
|
||||||
<MembershipActionConfirmation
|
<MembershipActionConfirmation
|
||||||
approvePendingMembership={approvePendingMembership}
|
approvePendingMembershipFromGroupV2={
|
||||||
|
approvePendingMembershipFromGroupV2
|
||||||
|
}
|
||||||
conversation={conversation}
|
conversation={conversation}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
members={conversation.sortedGroupMembers || []}
|
members={conversation.sortedGroupMembers || []}
|
||||||
onClose={() => setStagedMemberships(null)}
|
onClose={() => setStagedMemberships(null)}
|
||||||
ourUuid={ourUuid}
|
ourUuid={ourUuid}
|
||||||
revokePendingMemberships={revokePendingMemberships}
|
revokePendingMembershipsFromGroupV2={
|
||||||
|
revokePendingMembershipsFromGroupV2
|
||||||
|
}
|
||||||
stagedMemberships={stagedMemberships}
|
stagedMemberships={stagedMemberships}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -163,29 +178,36 @@ export function PendingInvites({
|
||||||
}
|
}
|
||||||
|
|
||||||
function MembershipActionConfirmation({
|
function MembershipActionConfirmation({
|
||||||
approvePendingMembership,
|
approvePendingMembershipFromGroupV2,
|
||||||
conversation,
|
conversation,
|
||||||
i18n,
|
i18n,
|
||||||
members,
|
members,
|
||||||
onClose,
|
onClose,
|
||||||
ourUuid,
|
ourUuid,
|
||||||
revokePendingMemberships,
|
revokePendingMembershipsFromGroupV2,
|
||||||
stagedMemberships,
|
stagedMemberships,
|
||||||
}: {
|
}: {
|
||||||
approvePendingMembership: (conversationId: string) => void;
|
approvePendingMembershipFromGroupV2: (
|
||||||
|
conversationId: string,
|
||||||
|
memberId: string
|
||||||
|
) => void;
|
||||||
conversation: ConversationType;
|
conversation: ConversationType;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
members: Array<ConversationType>;
|
members: Array<ConversationType>;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
ourUuid: string;
|
ourUuid: string;
|
||||||
revokePendingMemberships: (conversationIds: Array<string>) => void;
|
revokePendingMembershipsFromGroupV2: (
|
||||||
|
conversationId: string,
|
||||||
|
memberIds: Array<string>
|
||||||
|
) => void;
|
||||||
stagedMemberships: Array<StagedMembershipType>;
|
stagedMemberships: Array<StagedMembershipType>;
|
||||||
}) {
|
}) {
|
||||||
const revokeStagedMemberships = () => {
|
const revokeStagedMemberships = () => {
|
||||||
if (!stagedMemberships) {
|
if (!stagedMemberships) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
revokePendingMemberships(
|
revokePendingMembershipsFromGroupV2(
|
||||||
|
conversation.id,
|
||||||
stagedMemberships.map(({ membership }) => membership.member.id)
|
stagedMemberships.map(({ membership }) => membership.member.id)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -194,7 +216,10 @@ function MembershipActionConfirmation({
|
||||||
if (!stagedMemberships) {
|
if (!stagedMemberships) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
approvePendingMembership(stagedMemberships[0].membership.member.id);
|
approvePendingMembershipFromGroupV2(
|
||||||
|
conversation.id,
|
||||||
|
stagedMemberships[0].membership.member.id
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const membershipType = stagedMemberships[0].type;
|
const membershipType = stagedMemberships[0].type;
|
||||||
|
|
|
@ -150,6 +150,9 @@ import { getSendTarget } from '../util/getSendTarget';
|
||||||
import { getRecipients } from '../util/getRecipients';
|
import { getRecipients } from '../util/getRecipients';
|
||||||
import { validateConversation } from '../util/validateConversation';
|
import { validateConversation } from '../util/validateConversation';
|
||||||
import { isSignalConversation } from '../util/isSignalConversation';
|
import { isSignalConversation } from '../util/isSignalConversation';
|
||||||
|
import { isMemberRequestingToJoin } from '../util/isMemberRequestingToJoin';
|
||||||
|
import { removePendingMember } from '../util/removePendingMember';
|
||||||
|
import { isMemberPending } from '../util/isMemberPending';
|
||||||
|
|
||||||
/* eslint-disable more/no-then */
|
/* eslint-disable more/no-then */
|
||||||
window.Whisper = window.Whisper || {};
|
window.Whisper = window.Whisper || {};
|
||||||
|
@ -436,29 +439,11 @@ export class ConversationModel extends window.Backbone
|
||||||
}
|
}
|
||||||
|
|
||||||
private isMemberRequestingToJoin(uuid: UUID): boolean {
|
private isMemberRequestingToJoin(uuid: UUID): boolean {
|
||||||
if (!isGroupV2(this.attributes)) {
|
return isMemberRequestingToJoin(this.attributes, uuid);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const pendingAdminApprovalV2 = this.get('pendingAdminApprovalV2');
|
|
||||||
|
|
||||||
if (!pendingAdminApprovalV2 || !pendingAdminApprovalV2.length) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return pendingAdminApprovalV2.some(item => item.uuid === uuid.toString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isMemberPending(uuid: UUID): boolean {
|
isMemberPending(uuid: UUID): boolean {
|
||||||
if (!isGroupV2(this.attributes)) {
|
return isMemberPending(this.attributes, uuid);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const pendingMembersV2 = this.get('pendingMembersV2');
|
|
||||||
|
|
||||||
if (!pendingMembersV2 || !pendingMembersV2.length) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return pendingMembersV2.some(item => item.uuid === uuid.toString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private isMemberBanned(uuid: UUID): boolean {
|
private isMemberBanned(uuid: UUID): boolean {
|
||||||
|
@ -569,28 +554,6 @@ export class ConversationModel extends window.Backbone
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async approvePendingApprovalRequest(
|
|
||||||
uuid: UUID
|
|
||||||
): Promise<Proto.GroupChange.Actions | undefined> {
|
|
||||||
const idLog = this.idForLogging();
|
|
||||||
|
|
||||||
// This user's pending state may have changed in the time between the user's
|
|
||||||
// button press and when we get here. It's especially important to check here
|
|
||||||
// in conflict/retry cases.
|
|
||||||
if (!this.isMemberRequestingToJoin(uuid)) {
|
|
||||||
log.warn(
|
|
||||||
`approvePendingApprovalRequest/${idLog}: ${uuid} is not requesting ` +
|
|
||||||
'to join the group. Returning early.'
|
|
||||||
);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return window.Signal.Groups.buildPromotePendingAdminApprovalMemberChange({
|
|
||||||
group: this.attributes,
|
|
||||||
uuid,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async denyPendingApprovalRequest(
|
private async denyPendingApprovalRequest(
|
||||||
uuid: UUID
|
uuid: UUID
|
||||||
): Promise<Proto.GroupChange.Actions | undefined> {
|
): Promise<Proto.GroupChange.Actions | undefined> {
|
||||||
|
@ -712,32 +675,7 @@ export class ConversationModel extends window.Backbone
|
||||||
private async removePendingMember(
|
private async removePendingMember(
|
||||||
uuids: ReadonlyArray<UUID>
|
uuids: ReadonlyArray<UUID>
|
||||||
): Promise<Proto.GroupChange.Actions | undefined> {
|
): Promise<Proto.GroupChange.Actions | undefined> {
|
||||||
const idLog = this.idForLogging();
|
return removePendingMember(this.attributes, uuids);
|
||||||
|
|
||||||
const pendingUuids = uuids
|
|
||||||
.map(uuid => {
|
|
||||||
// This user's pending state may have changed in the time between the user's
|
|
||||||
// button press and when we get here. It's especially important to check here
|
|
||||||
// in conflict/retry cases.
|
|
||||||
if (!this.isMemberPending(uuid)) {
|
|
||||||
log.warn(
|
|
||||||
`removePendingMember/${idLog}: ${uuid} is not a pending member of group. Returning early.`
|
|
||||||
);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return uuid;
|
|
||||||
})
|
|
||||||
.filter(isNotNil);
|
|
||||||
|
|
||||||
if (!uuids.length) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return window.Signal.Groups.buildDeletePendingMemberChange({
|
|
||||||
group: this.attributes,
|
|
||||||
uuids: pendingUuids,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async removeMember(
|
private async removeMember(
|
||||||
|
@ -2631,85 +2569,6 @@ export class ConversationModel extends window.Backbone
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async approvePendingMembershipFromGroupV2(
|
|
||||||
conversationId: string
|
|
||||||
): Promise<void> {
|
|
||||||
const logId = this.idForLogging();
|
|
||||||
|
|
||||||
const pendingMember = window.ConversationController.get(conversationId);
|
|
||||||
if (!pendingMember) {
|
|
||||||
throw new Error(
|
|
||||||
`approvePendingMembershipFromGroupV2/${logId}: No conversation found for conversation ${conversationId}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const uuid = pendingMember.getCheckedUuid(
|
|
||||||
`approvePendingMembershipFromGroupV2/${logId}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isGroupV2(this.attributes) && this.isMemberRequestingToJoin(uuid)) {
|
|
||||||
await this.modifyGroupV2({
|
|
||||||
name: 'approvePendingApprovalRequest',
|
|
||||||
usingCredentialsFrom: [pendingMember],
|
|
||||||
createGroupChange: () => this.approvePendingApprovalRequest(uuid),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async revokePendingMembershipsFromGroupV2(
|
|
||||||
conversationIds: Array<string>
|
|
||||||
): Promise<void> {
|
|
||||||
if (!isGroupV2(this.attributes)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only pending memberships can be revoked for multiple members at once
|
|
||||||
if (conversationIds.length > 1) {
|
|
||||||
const uuids = conversationIds.map(id => {
|
|
||||||
const uuid = window.ConversationController.get(id)?.getUuid();
|
|
||||||
strictAssert(uuid, `UUID does not exist for ${id}`);
|
|
||||||
return uuid;
|
|
||||||
});
|
|
||||||
await this.modifyGroupV2({
|
|
||||||
name: 'removePendingMember',
|
|
||||||
usingCredentialsFrom: [],
|
|
||||||
createGroupChange: () => this.removePendingMember(uuids),
|
|
||||||
extraConversationsForSend: conversationIds,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [conversationId] = conversationIds;
|
|
||||||
|
|
||||||
const pendingMember = window.ConversationController.get(conversationId);
|
|
||||||
if (!pendingMember) {
|
|
||||||
const logId = this.idForLogging();
|
|
||||||
throw new Error(
|
|
||||||
`revokePendingMembershipsFromGroupV2/${logId}: No conversation found for conversation ${conversationId}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const uuid = pendingMember.getCheckedUuid(
|
|
||||||
'revokePendingMembershipsFromGroupV2'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (this.isMemberRequestingToJoin(uuid)) {
|
|
||||||
await this.modifyGroupV2({
|
|
||||||
name: 'denyPendingApprovalRequest',
|
|
||||||
usingCredentialsFrom: [],
|
|
||||||
createGroupChange: () => this.denyPendingApprovalRequest(uuid),
|
|
||||||
extraConversationsForSend: [conversationId],
|
|
||||||
});
|
|
||||||
} else if (this.isMemberPending(uuid)) {
|
|
||||||
await this.modifyGroupV2({
|
|
||||||
name: 'removePendingMember',
|
|
||||||
usingCredentialsFrom: [],
|
|
||||||
createGroupChange: () => this.removePendingMember([uuid]),
|
|
||||||
extraConversationsForSend: [conversationId],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async removeFromGroupV2(conversationId: string): Promise<void> {
|
async removeFromGroupV2(conversationId: string): Promise<void> {
|
||||||
if (!isGroupV2(this.attributes)) {
|
if (!isGroupV2(this.attributes)) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -12,6 +12,7 @@ import {
|
||||||
without,
|
without,
|
||||||
} from 'lodash';
|
} from 'lodash';
|
||||||
|
|
||||||
|
import type { AttachmentType } from '../../types/Attachment';
|
||||||
import type { StateType as RootStateType } from '../reducer';
|
import type { StateType as RootStateType } from '../reducer';
|
||||||
import * as groups from '../../groups';
|
import * as groups from '../../groups';
|
||||||
import * as log from '../../logging/log';
|
import * as log from '../../logging/log';
|
||||||
|
@ -90,12 +91,19 @@ import type { TimelineMessageLoadingState } from '../../util/timelineUtil';
|
||||||
import {
|
import {
|
||||||
isDirectConversation,
|
isDirectConversation,
|
||||||
isGroup,
|
isGroup,
|
||||||
|
isGroupV2,
|
||||||
} from '../../util/whatTypeOfConversation';
|
} from '../../util/whatTypeOfConversation';
|
||||||
import { missingCaseError } from '../../util/missingCaseError';
|
import { missingCaseError } from '../../util/missingCaseError';
|
||||||
import { viewedReceiptsJobQueue } from '../../jobs/viewedReceiptsJobQueue';
|
import { viewedReceiptsJobQueue } from '../../jobs/viewedReceiptsJobQueue';
|
||||||
import { viewSyncJobQueue } from '../../jobs/viewSyncJobQueue';
|
import { viewSyncJobQueue } from '../../jobs/viewSyncJobQueue';
|
||||||
import { ReadStatus } from '../../messages/MessageReadStatus';
|
import { ReadStatus } from '../../messages/MessageReadStatus';
|
||||||
import { isIncoming } from '../selectors/message';
|
import { isIncoming } from '../selectors/message';
|
||||||
|
import { sendDeleteForEveryoneMessage } from '../../util/sendDeleteForEveryoneMessage';
|
||||||
|
import type { ShowToastActionType } from './toast';
|
||||||
|
import { SHOW_TOAST, ToastType } from './toast';
|
||||||
|
import { isMemberRequestingToJoin } from '../../util/isMemberRequestingToJoin';
|
||||||
|
import { removePendingMember } from '../../util/removePendingMember';
|
||||||
|
import { denyPendingApprovalRequest } from '../../util/denyPendingApprovalRequest';
|
||||||
|
|
||||||
// State
|
// State
|
||||||
|
|
||||||
|
@ -833,6 +841,7 @@ export type ConversationActionType =
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
addMemberToGroup,
|
addMemberToGroup,
|
||||||
|
approvePendingMembershipFromGroupV2,
|
||||||
cancelConversationVerification,
|
cancelConversationVerification,
|
||||||
changeHasGroupLink,
|
changeHasGroupLink,
|
||||||
clearCancelledConversationVerification,
|
clearCancelledConversationVerification,
|
||||||
|
@ -854,9 +863,12 @@ export const actions = {
|
||||||
conversationUnloaded,
|
conversationUnloaded,
|
||||||
createGroup,
|
createGroup,
|
||||||
deleteAvatarFromDisk,
|
deleteAvatarFromDisk,
|
||||||
|
deleteMessageForEveryone,
|
||||||
|
destroyMessages,
|
||||||
discardMessages,
|
discardMessages,
|
||||||
doubleCheckMissingQuoteReference,
|
doubleCheckMissingQuoteReference,
|
||||||
generateNewGroupLink,
|
generateNewGroupLink,
|
||||||
|
loadRecentMediaItems,
|
||||||
messageChanged,
|
messageChanged,
|
||||||
messageDeleted,
|
messageDeleted,
|
||||||
messageExpanded,
|
messageExpanded,
|
||||||
|
@ -872,31 +884,35 @@ export const actions = {
|
||||||
resetAllChatColors,
|
resetAllChatColors,
|
||||||
reviewGroupMemberNameCollision,
|
reviewGroupMemberNameCollision,
|
||||||
reviewMessageRequestNameCollision,
|
reviewMessageRequestNameCollision,
|
||||||
|
revokePendingMembershipsFromGroupV2,
|
||||||
saveAvatarToDisk,
|
saveAvatarToDisk,
|
||||||
scrollToMessage,
|
scrollToMessage,
|
||||||
selectMessage,
|
selectMessage,
|
||||||
setAccessControlAddFromInviteLinkSetting,
|
setAccessControlAddFromInviteLinkSetting,
|
||||||
|
setAccessControlAttributesSetting,
|
||||||
|
setAccessControlMembersSetting,
|
||||||
|
setAnnouncementsOnly,
|
||||||
setComposeGroupAvatar,
|
setComposeGroupAvatar,
|
||||||
setComposeGroupExpireTimer,
|
setComposeGroupExpireTimer,
|
||||||
setComposeGroupName,
|
setComposeGroupName,
|
||||||
setComposeSearchTerm,
|
setComposeSearchTerm,
|
||||||
|
setDisappearingMessages,
|
||||||
setIsFetchingUUID,
|
setIsFetchingUUID,
|
||||||
setIsNearBottom,
|
setIsNearBottom,
|
||||||
setMessageLoadingState,
|
setMessageLoadingState,
|
||||||
setPreJoinConversation,
|
setPreJoinConversation,
|
||||||
setRecentMediaItems,
|
|
||||||
setSelectedConversationHeaderTitle,
|
setSelectedConversationHeaderTitle,
|
||||||
setSelectedConversationPanelDepth,
|
setSelectedConversationPanelDepth,
|
||||||
setVoiceNotePlaybackRate,
|
setVoiceNotePlaybackRate,
|
||||||
showArchivedConversations,
|
showArchivedConversations,
|
||||||
showChooseGroupMembers,
|
showChooseGroupMembers,
|
||||||
showInbox,
|
|
||||||
showConversation,
|
showConversation,
|
||||||
|
showInbox,
|
||||||
startComposing,
|
startComposing,
|
||||||
startSettingGroupMetadata,
|
startSettingGroupMetadata,
|
||||||
toggleAdmin,
|
toggleAdmin,
|
||||||
toggleConversationInChooseMembers,
|
|
||||||
toggleComposeEditingAvatar,
|
toggleComposeEditingAvatar,
|
||||||
|
toggleConversationInChooseMembers,
|
||||||
toggleGroupsForStorySend,
|
toggleGroupsForStorySend,
|
||||||
toggleHideStories,
|
toggleHideStories,
|
||||||
updateConversationModelSharedGroups,
|
updateConversationModelSharedGroups,
|
||||||
|
@ -1003,6 +1019,125 @@ function changeHasGroupLink(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setAnnouncementsOnly(
|
||||||
|
conversationId: string,
|
||||||
|
value: boolean
|
||||||
|
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
|
||||||
|
return async dispatch => {
|
||||||
|
const conversation = window.ConversationController.get(conversationId);
|
||||||
|
if (!conversation) {
|
||||||
|
throw new Error('No conversation found');
|
||||||
|
}
|
||||||
|
|
||||||
|
await longRunningTaskWrapper({
|
||||||
|
name: 'updateAnnouncementsOnly',
|
||||||
|
idForLogging: conversation.idForLogging(),
|
||||||
|
task: async () => conversation.updateAnnouncementsOnly(value),
|
||||||
|
});
|
||||||
|
dispatch({
|
||||||
|
type: 'NOOP',
|
||||||
|
payload: null,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAccessControlMembersSetting(
|
||||||
|
conversationId: string,
|
||||||
|
value: number
|
||||||
|
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
|
||||||
|
return async dispatch => {
|
||||||
|
const conversation = window.ConversationController.get(conversationId);
|
||||||
|
if (!conversation) {
|
||||||
|
throw new Error('No conversation found');
|
||||||
|
}
|
||||||
|
|
||||||
|
await longRunningTaskWrapper({
|
||||||
|
name: 'updateAccessControlMembers',
|
||||||
|
idForLogging: conversation.idForLogging(),
|
||||||
|
task: async () => conversation.updateAccessControlMembers(value),
|
||||||
|
});
|
||||||
|
dispatch({
|
||||||
|
type: 'NOOP',
|
||||||
|
payload: null,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAccessControlAttributesSetting(
|
||||||
|
conversationId: string,
|
||||||
|
value: number
|
||||||
|
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
|
||||||
|
return async dispatch => {
|
||||||
|
const conversation = window.ConversationController.get(conversationId);
|
||||||
|
if (!conversation) {
|
||||||
|
throw new Error('No conversation found');
|
||||||
|
}
|
||||||
|
|
||||||
|
await longRunningTaskWrapper({
|
||||||
|
name: 'updateAccessControlAttributes',
|
||||||
|
idForLogging: conversation.idForLogging(),
|
||||||
|
task: async () => conversation.updateAccessControlAttributes(value),
|
||||||
|
});
|
||||||
|
dispatch({
|
||||||
|
type: 'NOOP',
|
||||||
|
payload: null,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDisappearingMessages(
|
||||||
|
conversationId: string,
|
||||||
|
seconds: DurationInSeconds
|
||||||
|
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
|
||||||
|
return async dispatch => {
|
||||||
|
const conversation = window.ConversationController.get(conversationId);
|
||||||
|
if (!conversation) {
|
||||||
|
throw new Error('No conversation found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const valueToSet = seconds > 0 ? seconds : undefined;
|
||||||
|
|
||||||
|
await longRunningTaskWrapper({
|
||||||
|
name: 'updateExpirationTimer',
|
||||||
|
idForLogging: conversation.idForLogging(),
|
||||||
|
task: async () =>
|
||||||
|
conversation.updateExpirationTimer(valueToSet, {
|
||||||
|
reason: 'setDisappearingMessages',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
dispatch({
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
await longRunningTaskWrapper({
|
||||||
|
name: 'destroymessages',
|
||||||
|
idForLogging: conversation.idForLogging(),
|
||||||
|
task: async () => {
|
||||||
|
conversation.trigger('unload', 'delete messages');
|
||||||
|
await conversation.destroyMessages();
|
||||||
|
conversation.updateLastMessage();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: 'NOOP',
|
||||||
|
payload: null,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function generateNewGroupLink(
|
function generateNewGroupLink(
|
||||||
conversationId: string
|
conversationId: string
|
||||||
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
|
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
|
||||||
|
@ -1845,15 +1980,248 @@ function setSelectedConversationPanelDepth(
|
||||||
payload: { panelDepth },
|
payload: { panelDepth },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function setRecentMediaItems(
|
|
||||||
id: string,
|
function deleteMessageForEveryone(
|
||||||
recentMediaItems: Array<MediaItemType>
|
messageId: string
|
||||||
): SetRecentMediaItemsActionType {
|
): ThunkAction<
|
||||||
return {
|
void,
|
||||||
type: 'SET_RECENT_MEDIA_ITEMS',
|
RootStateType,
|
||||||
payload: { id, recentMediaItems },
|
unknown,
|
||||||
|
NoopActionType | ShowToastActionType
|
||||||
|
> {
|
||||||
|
return async dispatch => {
|
||||||
|
const message = window.MessageController.getById(messageId);
|
||||||
|
if (!message) {
|
||||||
|
throw new Error(
|
||||||
|
`deleteMessageForEveryone: Message ${messageId} missing!`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const conversation = message.getConversation();
|
||||||
|
if (!conversation) {
|
||||||
|
throw new Error('deleteMessageForEveryone: no conversation');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sendDeleteForEveryoneMessage(conversation.attributes, {
|
||||||
|
id: message.id,
|
||||||
|
timestamp: message.get('sent_at'),
|
||||||
|
});
|
||||||
|
dispatch({
|
||||||
|
type: 'NOOP',
|
||||||
|
payload: null,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
log.error(
|
||||||
|
'Error sending delete-for-everyone',
|
||||||
|
Errors.toLogFormat(error),
|
||||||
|
messageId
|
||||||
|
);
|
||||||
|
dispatch({
|
||||||
|
type: SHOW_TOAST,
|
||||||
|
payload: {
|
||||||
|
toastType: ToastType.DeleteForEveryoneFailed,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function approvePendingMembershipFromGroupV2(
|
||||||
|
conversationId: string,
|
||||||
|
memberId: string
|
||||||
|
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
|
||||||
|
return async dispatch => {
|
||||||
|
const conversation = window.ConversationController.get(conversationId);
|
||||||
|
if (!conversation) {
|
||||||
|
throw new Error(
|
||||||
|
`approvePendingMembershipFromGroupV2: No conversation found for conversation ${conversationId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const logId = conversation.idForLogging();
|
||||||
|
|
||||||
|
const pendingMember = window.ConversationController.get(memberId);
|
||||||
|
if (!pendingMember) {
|
||||||
|
throw new Error(
|
||||||
|
`approvePendingMembershipFromGroupV2/${logId}: No member found for conversation ${conversationId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const uuid = pendingMember.getCheckedUuid(
|
||||||
|
`approvePendingMembershipFromGroupV2/${logId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
isGroupV2(conversation.attributes) &&
|
||||||
|
isMemberRequestingToJoin(conversation.attributes, uuid)
|
||||||
|
) {
|
||||||
|
await window.Signal.Groups.modifyGroupV2({
|
||||||
|
conversation,
|
||||||
|
usingCredentialsFrom: [pendingMember],
|
||||||
|
createGroupChange: async () => {
|
||||||
|
// This user's pending state may have changed in the time between the user's
|
||||||
|
// button press and when we get here. It's especially important to check here
|
||||||
|
// in conflict/retry cases.
|
||||||
|
if (!isMemberRequestingToJoin(conversation.attributes, uuid)) {
|
||||||
|
log.warn(
|
||||||
|
`approvePendingMembershipFromGroupV2/${logId}: ${uuid} is not requesting ` +
|
||||||
|
'to join the group. Returning early.'
|
||||||
|
);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return window.Signal.Groups.buildPromotePendingAdminApprovalMemberChange(
|
||||||
|
{
|
||||||
|
group: conversation.attributes,
|
||||||
|
uuid,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
name: 'approvePendingMembershipFromGroupV2',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: 'NOOP',
|
||||||
|
payload: null,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function revokePendingMembershipsFromGroupV2(
|
||||||
|
conversationId: string,
|
||||||
|
memberIds: Array<string>
|
||||||
|
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
|
||||||
|
return async dispatch => {
|
||||||
|
const conversation = window.ConversationController.get(conversationId);
|
||||||
|
if (!conversation) {
|
||||||
|
throw new Error(
|
||||||
|
`approvePendingMembershipFromGroupV2: No conversation found for conversation ${conversationId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isGroupV2(conversation.attributes)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only pending memberships can be revoked for multiple members at once
|
||||||
|
if (memberIds.length > 1) {
|
||||||
|
const uuids = memberIds.map(id => {
|
||||||
|
const uuid = window.ConversationController.get(id)?.getUuid();
|
||||||
|
strictAssert(uuid, `UUID does not exist for ${id}`);
|
||||||
|
return uuid;
|
||||||
|
});
|
||||||
|
await conversation.modifyGroupV2({
|
||||||
|
name: 'removePendingMember',
|
||||||
|
usingCredentialsFrom: [],
|
||||||
|
createGroupChange: () =>
|
||||||
|
removePendingMember(conversation.attributes, uuids),
|
||||||
|
extraConversationsForSend: memberIds,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [memberId] = memberIds;
|
||||||
|
|
||||||
|
const pendingMember = window.ConversationController.get(memberId);
|
||||||
|
if (!pendingMember) {
|
||||||
|
const logId = conversation.idForLogging();
|
||||||
|
throw new Error(
|
||||||
|
`revokePendingMembershipsFromGroupV2/${logId}: No conversation found for conversation ${memberId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const uuid = pendingMember.getCheckedUuid(
|
||||||
|
'revokePendingMembershipsFromGroupV2'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isMemberRequestingToJoin(conversation.attributes, uuid)) {
|
||||||
|
await conversation.modifyGroupV2({
|
||||||
|
name: 'denyPendingApprovalRequest',
|
||||||
|
usingCredentialsFrom: [],
|
||||||
|
createGroupChange: () =>
|
||||||
|
denyPendingApprovalRequest(conversation.attributes, uuid),
|
||||||
|
extraConversationsForSend: [memberId],
|
||||||
|
});
|
||||||
|
} else if (conversation.isMemberPending(uuid)) {
|
||||||
|
await conversation.modifyGroupV2({
|
||||||
|
name: 'removePendingMember',
|
||||||
|
usingCredentialsFrom: [],
|
||||||
|
createGroupChange: () =>
|
||||||
|
removePendingMember(conversation.attributes, [uuid]),
|
||||||
|
extraConversationsForSend: [memberId],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: 'NOOP',
|
||||||
|
payload: null,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadRecentMediaItems(
|
||||||
|
conversationId: string,
|
||||||
|
limit: number
|
||||||
|
): ThunkAction<void, RootStateType, unknown, SetRecentMediaItemsActionType> {
|
||||||
|
return async dispatch => {
|
||||||
|
const { getAbsoluteAttachmentPath } = window.Signal.Migrations;
|
||||||
|
|
||||||
|
const messages: Array<MessageAttributesType> =
|
||||||
|
await window.Signal.Data.getMessagesWithVisualMediaAttachments(
|
||||||
|
conversationId,
|
||||||
|
{
|
||||||
|
limit,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cache these messages in memory to ensure Lightbox can find them
|
||||||
|
messages.forEach(message => {
|
||||||
|
window.MessageController.register(message.id, message);
|
||||||
|
});
|
||||||
|
|
||||||
|
const recentMediaItems = messages
|
||||||
|
.filter(message => message.attachments !== undefined)
|
||||||
|
.reduce(
|
||||||
|
(acc, message) => [
|
||||||
|
...acc,
|
||||||
|
...(message.attachments || []).map(
|
||||||
|
(attachment: AttachmentType, index: number): MediaItemType => {
|
||||||
|
const { thumbnail } = attachment;
|
||||||
|
|
||||||
|
return {
|
||||||
|
objectURL: getAbsoluteAttachmentPath(attachment.path || ''),
|
||||||
|
thumbnailObjectUrl: thumbnail?.path
|
||||||
|
? getAbsoluteAttachmentPath(thumbnail.path)
|
||||||
|
: '',
|
||||||
|
contentType: attachment.contentType,
|
||||||
|
index,
|
||||||
|
attachment,
|
||||||
|
message: {
|
||||||
|
attachments: message.attachments || [],
|
||||||
|
conversationId:
|
||||||
|
window.ConversationController.get(message.sourceUuid)?.id ||
|
||||||
|
message.conversationId,
|
||||||
|
id: message.id,
|
||||||
|
received_at: message.received_at,
|
||||||
|
received_at_ms: Number(message.received_at_ms),
|
||||||
|
sent_at: message.sent_at,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[] as Array<MediaItemType>
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: 'SET_RECENT_MEDIA_ITEMS',
|
||||||
|
payload: { id: conversationId, recentMediaItems },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function clearInvitedUuidsForNewlyCreatedGroup(): ClearInvitedUuidsForNewlyCreatedGroupActionType {
|
function clearInvitedUuidsForNewlyCreatedGroup(): ClearInvitedUuidsForNewlyCreatedGroupActionType {
|
||||||
return { type: 'CLEAR_INVITED_UUIDS_FOR_NEWLY_CREATED_GROUP' };
|
return { type: 'CLEAR_INVITED_UUIDS_FOR_NEWLY_CREATED_GROUP' };
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ export enum ToastType {
|
||||||
FailedToDeleteUsername = 'FailedToDeleteUsername',
|
FailedToDeleteUsername = 'FailedToDeleteUsername',
|
||||||
CopiedUsername = 'CopiedUsername',
|
CopiedUsername = 'CopiedUsername',
|
||||||
CopiedUsernameLink = 'CopiedUsernameLink',
|
CopiedUsernameLink = 'CopiedUsernameLink',
|
||||||
|
DeleteForEveryoneFailed = 'DeleteForEveryoneFailed',
|
||||||
}
|
}
|
||||||
|
|
||||||
// State
|
// State
|
||||||
|
@ -32,13 +33,13 @@ export type ToastStateType = {
|
||||||
// Actions
|
// Actions
|
||||||
|
|
||||||
const HIDE_TOAST = 'toast/HIDE_TOAST';
|
const HIDE_TOAST = 'toast/HIDE_TOAST';
|
||||||
const SHOW_TOAST = 'toast/SHOW_TOAST';
|
export const SHOW_TOAST = 'toast/SHOW_TOAST';
|
||||||
|
|
||||||
type HideToastActionType = {
|
type HideToastActionType = {
|
||||||
type: typeof HIDE_TOAST;
|
type: typeof HIDE_TOAST;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ShowToastActionType = {
|
export type ShowToastActionType = {
|
||||||
type: typeof SHOW_TOAST;
|
type: typeof SHOW_TOAST;
|
||||||
payload: {
|
payload: {
|
||||||
toastType: ToastType;
|
toastType: ToastType;
|
||||||
|
|
|
@ -24,7 +24,6 @@ import {
|
||||||
getPreferredBadgeSelector,
|
getPreferredBadgeSelector,
|
||||||
} from '../selectors/badges';
|
} from '../selectors/badges';
|
||||||
import { assertDev } from '../../util/assert';
|
import { assertDev } from '../../util/assert';
|
||||||
import type { DurationInSeconds } from '../../util/durations';
|
|
||||||
import { SignalService as Proto } from '../../protobuf';
|
import { SignalService as Proto } from '../../protobuf';
|
||||||
import { getConversationColorAttributes } from '../../util/getConversationColorAttributes';
|
import { getConversationColorAttributes } from '../../util/getConversationColorAttributes';
|
||||||
import type { SmartChooseGroupMembersModalPropsType } from './ChooseGroupMembersModal';
|
import type { SmartChooseGroupMembersModalPropsType } from './ChooseGroupMembersModal';
|
||||||
|
@ -39,8 +38,6 @@ import {
|
||||||
export type SmartConversationDetailsProps = {
|
export type SmartConversationDetailsProps = {
|
||||||
addMembers: (conversationIds: ReadonlyArray<string>) => Promise<void>;
|
addMembers: (conversationIds: ReadonlyArray<string>) => Promise<void>;
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
loadRecentMediaItems: (limit: number) => void;
|
|
||||||
setDisappearingMessages: (seconds: DurationInSeconds) => void;
|
|
||||||
showAllMedia: () => void;
|
showAllMedia: () => void;
|
||||||
showChatColorEditor: () => void;
|
showChatColorEditor: () => void;
|
||||||
showGroupLinkManagement: () => void;
|
showGroupLinkManagement: () => void;
|
||||||
|
|
|
@ -25,20 +25,17 @@ import { mapDispatchToProps } from '../actions';
|
||||||
import { missingCaseError } from '../../util/missingCaseError';
|
import { missingCaseError } from '../../util/missingCaseError';
|
||||||
import { strictAssert } from '../../util/assert';
|
import { strictAssert } from '../../util/assert';
|
||||||
import { isSignalConversation } from '../../util/isSignalConversation';
|
import { isSignalConversation } from '../../util/isSignalConversation';
|
||||||
import type { DurationInSeconds } from '../../util/durations';
|
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
onArchive: () => void;
|
onArchive: () => void;
|
||||||
onDeleteMessages: () => void;
|
|
||||||
onGoBack: () => void;
|
onGoBack: () => void;
|
||||||
onMarkUnread: () => void;
|
onMarkUnread: () => void;
|
||||||
onMoveToInbox: () => void;
|
onMoveToInbox: () => void;
|
||||||
onOutgoingAudioCallInConversation: () => void;
|
onOutgoingAudioCallInConversation: () => void;
|
||||||
onOutgoingVideoCallInConversation: () => void;
|
onOutgoingVideoCallInConversation: () => void;
|
||||||
onSearchInConversation: () => void;
|
onSearchInConversation: () => void;
|
||||||
onSetDisappearingMessages: (seconds: DurationInSeconds) => void;
|
|
||||||
onSetMuteNotifications: (seconds: number) => void;
|
onSetMuteNotifications: (seconds: number) => void;
|
||||||
onSetPin: (value: boolean) => void;
|
onSetPin: (value: boolean) => void;
|
||||||
onShowAllMedia: () => void;
|
onShowAllMedia: () => void;
|
||||||
|
|
|
@ -4,22 +4,20 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import type { StateType } from '../reducer';
|
import type { StateType } from '../reducer';
|
||||||
import type { PropsType } from '../../components/conversation/conversation-details/GroupV2Permissions';
|
import type { PropsDataType } from '../../components/conversation/conversation-details/GroupV2Permissions';
|
||||||
|
import { mapDispatchToProps } from '../actions';
|
||||||
import { GroupV2Permissions } from '../../components/conversation/conversation-details/GroupV2Permissions';
|
import { GroupV2Permissions } from '../../components/conversation/conversation-details/GroupV2Permissions';
|
||||||
import { getConversationSelector } from '../selectors/conversations';
|
import { getConversationSelector } from '../selectors/conversations';
|
||||||
import { getIntl } from '../selectors/user';
|
import { getIntl } from '../selectors/user';
|
||||||
|
|
||||||
export type SmartGroupV2PermissionsProps = {
|
export type SmartGroupV2PermissionsProps = {
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
setAccessControlAttributesSetting: (value: number) => void;
|
|
||||||
setAccessControlMembersSetting: (value: number) => void;
|
|
||||||
setAnnouncementsOnly: (value: boolean) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = (
|
const mapStateToProps = (
|
||||||
state: StateType,
|
state: StateType,
|
||||||
props: SmartGroupV2PermissionsProps
|
props: SmartGroupV2PermissionsProps
|
||||||
): PropsType => {
|
): PropsDataType => {
|
||||||
const conversation = getConversationSelector(state)(props.conversationId);
|
const conversation = getConversationSelector(state)(props.conversationId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -29,6 +27,6 @@ const mapStateToProps = (
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const smart = connect(mapStateToProps);
|
const smart = connect(mapStateToProps, mapDispatchToProps);
|
||||||
|
|
||||||
export const SmartGroupV2Permissions = smart(GroupV2Permissions);
|
export const SmartGroupV2Permissions = smart(GroupV2Permissions);
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { mapDispatchToProps } from '../actions';
|
import { mapDispatchToProps } from '../actions';
|
||||||
import type { PropsType } from '../../components/conversation/conversation-details/PendingInvites';
|
import type { PropsDataType } from '../../components/conversation/conversation-details/PendingInvites';
|
||||||
import { PendingInvites } from '../../components/conversation/conversation-details/PendingInvites';
|
import { PendingInvites } from '../../components/conversation/conversation-details/PendingInvites';
|
||||||
import type { StateType } from '../reducer';
|
import type { StateType } from '../reducer';
|
||||||
|
|
||||||
|
@ -20,14 +20,12 @@ import type { UUIDStringType } from '../../types/UUID';
|
||||||
export type SmartPendingInvitesProps = {
|
export type SmartPendingInvitesProps = {
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
ourUuid: UUIDStringType;
|
ourUuid: UUIDStringType;
|
||||||
readonly approvePendingMembership: (conversationid: string) => void;
|
|
||||||
readonly revokePendingMemberships: (membershipIds: Array<string>) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = (
|
const mapStateToProps = (
|
||||||
state: StateType,
|
state: StateType,
|
||||||
props: SmartPendingInvitesProps
|
props: SmartPendingInvitesProps
|
||||||
): PropsType => {
|
): PropsDataType => {
|
||||||
const conversationSelector = getConversationByIdSelector(state);
|
const conversationSelector = getConversationByIdSelector(state);
|
||||||
const conversationByUuidSelector = getConversationByUuidSelector(state);
|
const conversationByUuidSelector = getConversationByUuidSelector(state);
|
||||||
|
|
||||||
|
|
|
@ -66,7 +66,6 @@ export type TimelinePropsType = ExternalProps &
|
||||||
| 'contactSupport'
|
| 'contactSupport'
|
||||||
| 'blockGroupLinkRequests'
|
| 'blockGroupLinkRequests'
|
||||||
| 'deleteMessage'
|
| 'deleteMessage'
|
||||||
| 'deleteMessageForEveryone'
|
|
||||||
| 'displayTapToViewMessage'
|
| 'displayTapToViewMessage'
|
||||||
| 'downloadAttachment'
|
| 'downloadAttachment'
|
||||||
| 'downloadNewVersion'
|
| 'downloadNewVersion'
|
||||||
|
|
36
ts/util/denyPendingApprovalRequest.ts
Normal file
36
ts/util/denyPendingApprovalRequest.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import type { ConversationAttributesType } from '../model-types.d';
|
||||||
|
import type { SignalService as Proto } from '../protobuf';
|
||||||
|
import type { UUID } from '../types/UUID';
|
||||||
|
import * as log from '../logging/log';
|
||||||
|
import { UUIDKind } from '../types/UUID';
|
||||||
|
import { getConversationIdForLogging } from './idForLogging';
|
||||||
|
import { isMemberRequestingToJoin } from './isMemberRequestingToJoin';
|
||||||
|
|
||||||
|
export async function denyPendingApprovalRequest(
|
||||||
|
conversationAttributes: ConversationAttributesType,
|
||||||
|
uuid: UUID
|
||||||
|
): Promise<Proto.GroupChange.Actions | undefined> {
|
||||||
|
const idLog = getConversationIdForLogging(conversationAttributes);
|
||||||
|
|
||||||
|
// This user's pending state may have changed in the time between the user's
|
||||||
|
// button press and when we get here. It's especially important to check here
|
||||||
|
// in conflict/retry cases.
|
||||||
|
if (!isMemberRequestingToJoin(conversationAttributes, uuid)) {
|
||||||
|
log.warn(
|
||||||
|
`denyPendingApprovalRequest/${idLog}: ${uuid} is not requesting ` +
|
||||||
|
'to join the group. Returning early.'
|
||||||
|
);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ourUuid = window.textsecure.storage.user.getCheckedUuid(UUIDKind.ACI);
|
||||||
|
|
||||||
|
return window.Signal.Groups.buildDeletePendingAdminApprovalMemberChange({
|
||||||
|
group: conversationAttributes,
|
||||||
|
ourUuid,
|
||||||
|
uuid,
|
||||||
|
});
|
||||||
|
}
|
25
ts/util/isMemberPending.ts
Normal file
25
ts/util/isMemberPending.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import type { UUID } from '../types/UUID';
|
||||||
|
import type { ConversationAttributesType } from '../model-types.d';
|
||||||
|
import { isGroupV2 } from './whatTypeOfConversation';
|
||||||
|
|
||||||
|
export function isMemberPending(
|
||||||
|
conversationAttrs: Pick<
|
||||||
|
ConversationAttributesType,
|
||||||
|
'groupId' | 'groupVersion' | 'pendingMembersV2'
|
||||||
|
>,
|
||||||
|
uuid: UUID
|
||||||
|
): boolean {
|
||||||
|
if (!isGroupV2(conversationAttrs)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const { pendingMembersV2 } = conversationAttrs;
|
||||||
|
|
||||||
|
if (!pendingMembersV2 || !pendingMembersV2.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pendingMembersV2.some(item => item.uuid === uuid.toString());
|
||||||
|
}
|
25
ts/util/isMemberRequestingToJoin.ts
Normal file
25
ts/util/isMemberRequestingToJoin.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import type { UUID } from '../types/UUID';
|
||||||
|
import type { ConversationAttributesType } from '../model-types.d';
|
||||||
|
import { isGroupV2 } from './whatTypeOfConversation';
|
||||||
|
|
||||||
|
export function isMemberRequestingToJoin(
|
||||||
|
conversationAttrs: Pick<
|
||||||
|
ConversationAttributesType,
|
||||||
|
'groupId' | 'groupVersion' | 'pendingAdminApprovalV2'
|
||||||
|
>,
|
||||||
|
uuid: UUID
|
||||||
|
): boolean {
|
||||||
|
if (!isGroupV2(conversationAttrs)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const { pendingAdminApprovalV2 } = conversationAttrs;
|
||||||
|
|
||||||
|
if (!pendingAdminApprovalV2 || !pendingAdminApprovalV2.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pendingAdminApprovalV2.some(item => item.uuid === uuid.toString());
|
||||||
|
}
|
42
ts/util/removePendingMember.ts
Normal file
42
ts/util/removePendingMember.ts
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import type { ConversationAttributesType } from '../model-types.d';
|
||||||
|
import type { SignalService as Proto } from '../protobuf';
|
||||||
|
import type { UUID } from '../types/UUID';
|
||||||
|
import * as log from '../logging/log';
|
||||||
|
import { getConversationIdForLogging } from './idForLogging';
|
||||||
|
import { isMemberPending } from './isMemberPending';
|
||||||
|
import { isNotNil } from './isNotNil';
|
||||||
|
|
||||||
|
export async function removePendingMember(
|
||||||
|
conversationAttributes: ConversationAttributesType,
|
||||||
|
uuids: ReadonlyArray<UUID>
|
||||||
|
): Promise<Proto.GroupChange.Actions | undefined> {
|
||||||
|
const idLog = getConversationIdForLogging(conversationAttributes);
|
||||||
|
|
||||||
|
const pendingUuids = uuids
|
||||||
|
.map(uuid => {
|
||||||
|
// This user's pending state may have changed in the time between the user's
|
||||||
|
// button press and when we get here. It's especially important to check here
|
||||||
|
// in conflict/retry cases.
|
||||||
|
if (!isMemberPending(conversationAttributes, uuid)) {
|
||||||
|
log.warn(
|
||||||
|
`removePendingMember/${idLog}: ${uuid} is not a pending member of group. Returning early.`
|
||||||
|
);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return uuid;
|
||||||
|
})
|
||||||
|
.filter(isNotNil);
|
||||||
|
|
||||||
|
if (!uuids.length) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return window.Signal.Groups.buildDeletePendingMemberChange({
|
||||||
|
group: conversationAttributes,
|
||||||
|
uuids: pendingUuids,
|
||||||
|
});
|
||||||
|
}
|
|
@ -27,7 +27,6 @@ import type {
|
||||||
ToastInternalError,
|
ToastInternalError,
|
||||||
ToastPropsType as ToastInternalErrorPropsType,
|
ToastPropsType as ToastInternalErrorPropsType,
|
||||||
} from '../components/ToastInternalError';
|
} from '../components/ToastInternalError';
|
||||||
import type { ToastDeleteForEveryoneFailed } from '../components/ToastDeleteForEveryoneFailed';
|
|
||||||
import type { ToastExpired } from '../components/ToastExpired';
|
import type { ToastExpired } from '../components/ToastExpired';
|
||||||
import type {
|
import type {
|
||||||
ToastFileSaved,
|
ToastFileSaved,
|
||||||
|
@ -79,7 +78,6 @@ export function showToast(
|
||||||
Toast: typeof ToastInternalError,
|
Toast: typeof ToastInternalError,
|
||||||
props: ToastInternalErrorPropsType
|
props: ToastInternalErrorPropsType
|
||||||
): void;
|
): void;
|
||||||
export function showToast(Toast: typeof ToastDeleteForEveryoneFailed): void;
|
|
||||||
export function showToast(Toast: typeof ToastExpired): void;
|
export function showToast(Toast: typeof ToastExpired): void;
|
||||||
export function showToast(
|
export function showToast(
|
||||||
Toast: typeof ToastFileSaved,
|
Toast: typeof ToastFileSaved,
|
||||||
|
|
19
ts/util/startConversation.ts
Normal file
19
ts/util/startConversation.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import type { UUIDStringType } from '../types/UUID';
|
||||||
|
import { strictAssert } from './assert';
|
||||||
|
|
||||||
|
export function startConversation(e164: string, uuid: UUIDStringType): void {
|
||||||
|
const conversation = window.ConversationController.lookupOrCreate({
|
||||||
|
e164,
|
||||||
|
uuid,
|
||||||
|
reason: 'util/startConversation',
|
||||||
|
});
|
||||||
|
strictAssert(
|
||||||
|
conversation,
|
||||||
|
`startConversation failed given ${e164}/${uuid} combination`
|
||||||
|
);
|
||||||
|
|
||||||
|
window.Whisper.events.trigger('showConversation', conversation.id);
|
||||||
|
}
|
|
@ -36,7 +36,6 @@ import {
|
||||||
isGroupV1,
|
isGroupV1,
|
||||||
} from '../util/whatTypeOfConversation';
|
} from '../util/whatTypeOfConversation';
|
||||||
import { findAndFormatContact } from '../util/findAndFormatContact';
|
import { findAndFormatContact } from '../util/findAndFormatContact';
|
||||||
import type { DurationInSeconds } from '../util/durations';
|
|
||||||
import { getPreferredBadgeSelector } from '../state/selectors/badges';
|
import { getPreferredBadgeSelector } from '../state/selectors/badges';
|
||||||
import {
|
import {
|
||||||
canReply,
|
canReply,
|
||||||
|
@ -67,7 +66,6 @@ import { ToastConversationArchived } from '../components/ToastConversationArchiv
|
||||||
import { ToastConversationMarkedUnread } from '../components/ToastConversationMarkedUnread';
|
import { ToastConversationMarkedUnread } from '../components/ToastConversationMarkedUnread';
|
||||||
import { ToastConversationUnarchived } from '../components/ToastConversationUnarchived';
|
import { ToastConversationUnarchived } from '../components/ToastConversationUnarchived';
|
||||||
import { ToastDangerousFileType } from '../components/ToastDangerousFileType';
|
import { ToastDangerousFileType } from '../components/ToastDangerousFileType';
|
||||||
import { ToastDeleteForEveryoneFailed } from '../components/ToastDeleteForEveryoneFailed';
|
|
||||||
import { ToastExpired } from '../components/ToastExpired';
|
import { ToastExpired } from '../components/ToastExpired';
|
||||||
import { ToastFileSize } from '../components/ToastFileSize';
|
import { ToastFileSize } from '../components/ToastFileSize';
|
||||||
import { ToastInvalidConversation } from '../components/ToastInvalidConversation';
|
import { ToastInvalidConversation } from '../components/ToastInvalidConversation';
|
||||||
|
@ -107,13 +105,13 @@ import {
|
||||||
import { LinkPreviewSourceType } from '../types/LinkPreview';
|
import { LinkPreviewSourceType } from '../types/LinkPreview';
|
||||||
import { closeLightbox, showLightbox } from '../util/showLightbox';
|
import { closeLightbox, showLightbox } from '../util/showLightbox';
|
||||||
import { saveAttachment } from '../util/saveAttachment';
|
import { saveAttachment } from '../util/saveAttachment';
|
||||||
import { sendDeleteForEveryoneMessage } from '../util/sendDeleteForEveryoneMessage';
|
|
||||||
import { SECOND } from '../util/durations';
|
import { SECOND } from '../util/durations';
|
||||||
import { blockSendUntilConversationsAreVerified } from '../util/blockSendUntilConversationsAreVerified';
|
import { blockSendUntilConversationsAreVerified } from '../util/blockSendUntilConversationsAreVerified';
|
||||||
import { SafetyNumberChangeSource } from '../components/SafetyNumberChangeDialog';
|
import { SafetyNumberChangeSource } from '../components/SafetyNumberChangeDialog';
|
||||||
import { getOwn } from '../util/getOwn';
|
import { getOwn } from '../util/getOwn';
|
||||||
import { CallMode } from '../types/Calling';
|
import { CallMode } from '../types/Calling';
|
||||||
import { isAnybodyElseInGroupCall } from '../state/ducks/calling';
|
import { isAnybodyElseInGroupCall } from '../state/ducks/calling';
|
||||||
|
import { startConversation } from '../util/startConversation';
|
||||||
|
|
||||||
type AttachmentOptions = {
|
type AttachmentOptions = {
|
||||||
messageId: string;
|
messageId: string;
|
||||||
|
@ -136,7 +134,6 @@ const { getMessagesBySentAt } = window.Signal.Data;
|
||||||
|
|
||||||
type MessageActionsType = {
|
type MessageActionsType = {
|
||||||
deleteMessage: (messageId: string) => unknown;
|
deleteMessage: (messageId: string) => unknown;
|
||||||
deleteMessageForEveryone: (messageId: string) => unknown;
|
|
||||||
displayTapToViewMessage: (messageId: string) => unknown;
|
displayTapToViewMessage: (messageId: string) => unknown;
|
||||||
downloadAttachment: (options: {
|
downloadAttachment: (options: {
|
||||||
attachment: AttachmentType;
|
attachment: AttachmentType;
|
||||||
|
@ -337,9 +334,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
const conversationHeaderProps = {
|
const conversationHeaderProps = {
|
||||||
id: this.model.id,
|
id: this.model.id,
|
||||||
|
|
||||||
onSetDisappearingMessages: (seconds: DurationInSeconds) =>
|
|
||||||
this.setDisappearingMessages(seconds),
|
|
||||||
onDeleteMessages: () => this.destroyMessages(),
|
|
||||||
onSearchInConversation: () => {
|
onSearchInConversation: () => {
|
||||||
const { searchInConversation } = window.reduxActions.search;
|
const { searchInConversation } = window.reduxActions.search;
|
||||||
searchInConversation(this.model.id);
|
searchInConversation(this.model.id);
|
||||||
|
@ -737,9 +731,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
const deleteMessage = (messageId: string) => {
|
const deleteMessage = (messageId: string) => {
|
||||||
this.deleteMessage(messageId);
|
this.deleteMessage(messageId);
|
||||||
};
|
};
|
||||||
const deleteMessageForEveryone = (messageId: string) => {
|
|
||||||
this.deleteMessageForEveryone(messageId);
|
|
||||||
};
|
|
||||||
const showMessageDetail = (messageId: string) => {
|
const showMessageDetail = (messageId: string) => {
|
||||||
this.showMessageDetail(messageId);
|
this.showMessageDetail(messageId);
|
||||||
};
|
};
|
||||||
|
@ -826,11 +817,9 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
};
|
};
|
||||||
|
|
||||||
const showForwardMessageModal = this.showForwardMessageModal.bind(this);
|
const showForwardMessageModal = this.showForwardMessageModal.bind(this);
|
||||||
const startConversation = this.startConversation.bind(this);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
deleteMessage,
|
deleteMessage,
|
||||||
deleteMessageForEveryone,
|
|
||||||
displayTapToViewMessage,
|
displayTapToViewMessage,
|
||||||
downloadAttachment,
|
downloadAttachment,
|
||||||
downloadNewVersion,
|
downloadNewVersion,
|
||||||
|
@ -1661,38 +1650,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteMessageForEveryone(messageId: string): void {
|
|
||||||
const message = window.MessageController.getById(messageId);
|
|
||||||
if (!message) {
|
|
||||||
throw new Error(
|
|
||||||
`deleteMessageForEveryone: Message ${messageId} missing!`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
window.showConfirmationDialog({
|
|
||||||
dialogName: 'deleteMessageForEveryone',
|
|
||||||
confirmStyle: 'negative',
|
|
||||||
message: window.i18n('deleteForEveryoneWarning'),
|
|
||||||
okText: window.i18n('delete'),
|
|
||||||
resolve: async () => {
|
|
||||||
try {
|
|
||||||
await sendDeleteForEveryoneMessage(this.model.attributes, {
|
|
||||||
id: message.id,
|
|
||||||
timestamp: message.get('sent_at'),
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
log.error(
|
|
||||||
'Error sending delete-for-everyone',
|
|
||||||
Errors.toLogFormat(error),
|
|
||||||
messageId
|
|
||||||
);
|
|
||||||
showToast(ToastDeleteForEveryoneFailed);
|
|
||||||
}
|
|
||||||
this.resetPanel();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
showStickerPackPreview(packId: string, packKey: string): void {
|
showStickerPackPreview(packId: string, packKey: string): void {
|
||||||
Stickers.downloadEphemeralPack(packId, packKey);
|
Stickers.downloadEphemeralPack(packId, packKey);
|
||||||
|
|
||||||
|
@ -1873,11 +1830,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
window.reduxStore,
|
window.reduxStore,
|
||||||
{
|
{
|
||||||
conversationId: this.model.id,
|
conversationId: this.model.id,
|
||||||
setAccessControlAttributesSetting:
|
|
||||||
this.setAccessControlAttributesSetting.bind(this),
|
|
||||||
setAccessControlMembersSetting:
|
|
||||||
this.setAccessControlMembersSetting.bind(this),
|
|
||||||
setAnnouncementsOnly: this.setAnnouncementsOnly.bind(this),
|
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
@ -1893,12 +1845,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
JSX: window.Signal.State.Roots.createPendingInvites(window.reduxStore, {
|
JSX: window.Signal.State.Roots.createPendingInvites(window.reduxStore, {
|
||||||
conversationId: this.model.id,
|
conversationId: this.model.id,
|
||||||
ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(),
|
ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(),
|
||||||
approvePendingMembership: (conversationId: string) => {
|
|
||||||
this.model.approvePendingMembershipFromGroupV2(conversationId);
|
|
||||||
},
|
|
||||||
revokePendingMemberships: conversationIds => {
|
|
||||||
this.model.revokePendingMembershipsFromGroupV2(conversationIds);
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const headerTitle = window.i18n(
|
const headerTitle = window.i18n(
|
||||||
|
@ -1971,8 +1917,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
const props = {
|
const props = {
|
||||||
addMembers: this.model.addMembersV2.bind(this.model),
|
addMembers: this.model.addMembersV2.bind(this.model),
|
||||||
conversationId: this.model.get('id'),
|
conversationId: this.model.get('id'),
|
||||||
loadRecentMediaItems: this.loadRecentMediaItems.bind(this),
|
|
||||||
setDisappearingMessages: this.setDisappearingMessages.bind(this),
|
|
||||||
showAllMedia: this.showAllMedia.bind(this),
|
showAllMedia: this.showAllMedia.bind(this),
|
||||||
showContactModal: this.showContactModal.bind(this),
|
showContactModal: this.showContactModal.bind(this),
|
||||||
showChatColorEditor: this.showChatColorEditor.bind(this),
|
showChatColorEditor: this.showChatColorEditor.bind(this),
|
||||||
|
@ -2092,10 +2036,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
hasSignalAccount={Boolean(signalAccount)}
|
hasSignalAccount={Boolean(signalAccount)}
|
||||||
onSendMessage={() => {
|
onSendMessage={() => {
|
||||||
if (signalAccount) {
|
if (signalAccount) {
|
||||||
this.startConversation(
|
startConversation(signalAccount.phoneNumber, signalAccount.uuid);
|
||||||
signalAccount.phoneNumber,
|
|
||||||
signalAccount.uuid
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -2108,20 +2049,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
this.addPanel({ view });
|
this.addPanel({ view });
|
||||||
}
|
}
|
||||||
|
|
||||||
startConversation(e164: string, uuid: UUIDStringType): void {
|
|
||||||
const conversation = window.ConversationController.lookupOrCreate({
|
|
||||||
e164,
|
|
||||||
uuid,
|
|
||||||
reason: 'conversation_view.startConversation',
|
|
||||||
});
|
|
||||||
strictAssert(
|
|
||||||
conversation,
|
|
||||||
`startConversation failed given ${e164}/${uuid} combination`
|
|
||||||
);
|
|
||||||
|
|
||||||
this.openConversation(conversation.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
async openConversation(
|
async openConversation(
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
messageId?: string
|
messageId?: string
|
||||||
|
@ -2201,124 +2128,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadRecentMediaItems(limit: number): Promise<void> {
|
|
||||||
const { model }: { model: ConversationModel } = this;
|
|
||||||
|
|
||||||
const messages: Array<MessageAttributesType> =
|
|
||||||
await window.Signal.Data.getMessagesWithVisualMediaAttachments(model.id, {
|
|
||||||
limit,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cache these messages in memory to ensure Lightbox can find them
|
|
||||||
messages.forEach(message => {
|
|
||||||
window.MessageController.register(message.id, message);
|
|
||||||
});
|
|
||||||
|
|
||||||
const loadedRecentMediaItems = messages
|
|
||||||
.filter(message => message.attachments !== undefined)
|
|
||||||
.reduce(
|
|
||||||
(acc, message) => [
|
|
||||||
...acc,
|
|
||||||
...(message.attachments || []).map(
|
|
||||||
(attachment: AttachmentType, index: number): MediaItemType => {
|
|
||||||
const { thumbnail } = attachment;
|
|
||||||
|
|
||||||
return {
|
|
||||||
objectURL: getAbsoluteAttachmentPath(attachment.path || ''),
|
|
||||||
thumbnailObjectUrl: thumbnail?.path
|
|
||||||
? getAbsoluteAttachmentPath(thumbnail.path)
|
|
||||||
: '',
|
|
||||||
contentType: attachment.contentType,
|
|
||||||
index,
|
|
||||||
attachment,
|
|
||||||
message: {
|
|
||||||
attachments: message.attachments || [],
|
|
||||||
conversationId:
|
|
||||||
window.ConversationController.get(message.sourceUuid)?.id ||
|
|
||||||
message.conversationId,
|
|
||||||
id: message.id,
|
|
||||||
received_at: message.received_at,
|
|
||||||
received_at_ms: Number(message.received_at_ms),
|
|
||||||
sent_at: message.sent_at,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
),
|
|
||||||
],
|
|
||||||
[] as Array<MediaItemType>
|
|
||||||
);
|
|
||||||
|
|
||||||
window.reduxActions.conversations.setRecentMediaItems(
|
|
||||||
model.id,
|
|
||||||
loadedRecentMediaItems
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async setDisappearingMessages(seconds: DurationInSeconds): Promise<void> {
|
|
||||||
const { model }: { model: ConversationModel } = this;
|
|
||||||
|
|
||||||
const valueToSet = seconds > 0 ? seconds : undefined;
|
|
||||||
|
|
||||||
await this.longRunningTaskWrapper({
|
|
||||||
name: 'updateExpirationTimer',
|
|
||||||
task: async () =>
|
|
||||||
model.updateExpirationTimer(valueToSet, {
|
|
||||||
reason: 'setDisappearingMessages',
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async setAccessControlAttributesSetting(value: number): Promise<void> {
|
|
||||||
const { model }: { model: ConversationModel } = this;
|
|
||||||
|
|
||||||
await this.longRunningTaskWrapper({
|
|
||||||
name: 'updateAccessControlAttributes',
|
|
||||||
task: async () => model.updateAccessControlAttributes(value),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async setAccessControlMembersSetting(value: number): Promise<void> {
|
|
||||||
const { model }: { model: ConversationModel } = this;
|
|
||||||
|
|
||||||
await this.longRunningTaskWrapper({
|
|
||||||
name: 'updateAccessControlMembers',
|
|
||||||
task: async () => model.updateAccessControlMembers(value),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async setAnnouncementsOnly(value: boolean): Promise<void> {
|
|
||||||
const { model }: { model: ConversationModel } = this;
|
|
||||||
|
|
||||||
await this.longRunningTaskWrapper({
|
|
||||||
name: 'updateAnnouncementsOnly',
|
|
||||||
task: async () => model.updateAnnouncementsOnly(value),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async destroyMessages(): Promise<void> {
|
|
||||||
const { model }: { model: ConversationModel } = this;
|
|
||||||
|
|
||||||
window.showConfirmationDialog({
|
|
||||||
dialogName: 'destroyMessages',
|
|
||||||
confirmStyle: 'negative',
|
|
||||||
message: window.i18n('deleteConversationConfirmation'),
|
|
||||||
okText: window.i18n('delete'),
|
|
||||||
resolve: () => {
|
|
||||||
this.longRunningTaskWrapper({
|
|
||||||
name: 'destroymessages',
|
|
||||||
task: async () => {
|
|
||||||
model.trigger('unload', 'delete messages');
|
|
||||||
await model.destroyMessages();
|
|
||||||
model.updateLastMessage();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
reject: () => {
|
|
||||||
log.info('destroyMessages: User canceled delete');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async isCallSafe(): Promise<boolean> {
|
async isCallSafe(): Promise<boolean> {
|
||||||
const recipientsByConversation = {
|
const recipientsByConversation = {
|
||||||
[this.model.id]: {
|
[this.model.id]: {
|
||||||
|
|
Loading…
Add table
Reference in a new issue