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);
|
||||
}
|
||||
|
|
|
@ -38,8 +38,8 @@ const commonProps = {
|
|||
i18n,
|
||||
|
||||
onShowConversationDetails: action('onShowConversationDetails'),
|
||||
onSetDisappearingMessages: action('onSetDisappearingMessages'),
|
||||
onDeleteMessages: action('onDeleteMessages'),
|
||||
setDisappearingMessages: action('setDisappearingMessages'),
|
||||
destroyMessages: action('destroyMessages'),
|
||||
onSearchInConversation: action('onSearchInConversation'),
|
||||
onSetMuteNotifications: action('onSetMuteNotifications'),
|
||||
onOutgoingAudioCallInConversation: action(
|
||||
|
|
|
@ -28,6 +28,7 @@ import * as expirationTimer from '../../util/expirationTimer';
|
|||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import { isInSystemContacts } from '../../util/isInSystemContacts';
|
||||
import { isConversationMuted } from '../../util/isConversationMuted';
|
||||
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||
import { DurationInSeconds } from '../../util/durations';
|
||||
import {
|
||||
useStartCallShortcuts,
|
||||
|
@ -80,8 +81,7 @@ export type PropsDataType = {
|
|||
|
||||
export type PropsActionsType = {
|
||||
onSetMuteNotifications: (seconds: number) => void;
|
||||
onSetDisappearingMessages: (seconds: DurationInSeconds) => void;
|
||||
onDeleteMessages: () => void;
|
||||
destroyMessages: (conversationId: string) => void;
|
||||
onSearchInConversation: () => void;
|
||||
onOutgoingAudioCallInConversation: () => void;
|
||||
onOutgoingVideoCallInConversation: () => void;
|
||||
|
@ -95,6 +95,10 @@ export type PropsActionsType = {
|
|||
onArchive: () => void;
|
||||
onMarkUnread: () => void;
|
||||
onMoveToInbox: () => void;
|
||||
setDisappearingMessages: (
|
||||
conversationId: string,
|
||||
seconds: DurationInSeconds
|
||||
) => void;
|
||||
viewUserStories: ViewUserStoriesActionCreatorType;
|
||||
};
|
||||
|
||||
|
@ -112,6 +116,7 @@ enum ModalState {
|
|||
}
|
||||
|
||||
type StateType = {
|
||||
hasDeleteMessagesConfirmation: boolean;
|
||||
isNarrow: boolean;
|
||||
modalState: ModalState;
|
||||
};
|
||||
|
@ -130,7 +135,11 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
|||
public constructor(props: PropsType) {
|
||||
super(props);
|
||||
|
||||
this.state = { isNarrow: false, modalState: ModalState.NothingOpen };
|
||||
this.state = {
|
||||
hasDeleteMessagesConfirmation: false,
|
||||
isNarrow: false,
|
||||
modalState: ModalState.NothingOpen,
|
||||
};
|
||||
|
||||
this.menuTriggerRef = React.createRef();
|
||||
this.headerRef = React.createRef();
|
||||
|
@ -329,6 +338,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
|||
expireTimer,
|
||||
groupVersion,
|
||||
i18n,
|
||||
id,
|
||||
isArchived,
|
||||
isMissingMandatoryProfileSharing,
|
||||
isPinned,
|
||||
|
@ -337,15 +347,14 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
|||
markedUnread,
|
||||
muteExpiresAt,
|
||||
onArchive,
|
||||
onDeleteMessages,
|
||||
onMarkUnread,
|
||||
onMoveToInbox,
|
||||
onSetDisappearingMessages,
|
||||
onSetMuteNotifications,
|
||||
onSetPin,
|
||||
onShowAllMedia,
|
||||
onShowConversationDetails,
|
||||
onShowGroupMembers,
|
||||
setDisappearingMessages,
|
||||
type,
|
||||
} = this.props;
|
||||
|
||||
|
@ -425,7 +434,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
|||
modalState: ModalState.CustomDisappearingTimeout,
|
||||
});
|
||||
} 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={onDeleteMessages}>{i18n('deleteMessages')}</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => this.setState({ hasDeleteMessagesConfirmation: true })}
|
||||
>
|
||||
{i18n('deleteMessages')}
|
||||
</MenuItem>
|
||||
{isPinned ? (
|
||||
<MenuItem onClick={() => onSetPin(false)}>
|
||||
{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 {
|
||||
const { conversationTitle, groupVersion, onShowConversationDetails, type } =
|
||||
this.props;
|
||||
|
@ -579,8 +623,8 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
|||
isSignalConversation,
|
||||
onOutgoingAudioCallInConversation,
|
||||
onOutgoingVideoCallInConversation,
|
||||
onSetDisappearingMessages,
|
||||
outgoingCallButtonStyle,
|
||||
setDisappearingMessages,
|
||||
showBackButton,
|
||||
} = this.props;
|
||||
const { isNarrow, modalState } = this.state;
|
||||
|
@ -596,7 +640,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
|||
initialValue={expireTimer}
|
||||
onSubmit={value => {
|
||||
this.setState({ modalState: ModalState.NothingOpen });
|
||||
onSetDisappearingMessages(value);
|
||||
setDisappearingMessages(id, value);
|
||||
}}
|
||||
onClose={() => this.setState({ modalState: ModalState.NothingOpen })}
|
||||
/>
|
||||
|
@ -608,6 +652,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
|||
return (
|
||||
<>
|
||||
{modalNode}
|
||||
{this.renderConfirmationDialog()}
|
||||
<Measure
|
||||
bounds
|
||||
onResize={({ bounds }) => {
|
||||
|
|
|
@ -326,7 +326,7 @@ export class MessageDetail extends React.Component<Props> {
|
|||
displayLimit={Number.MAX_SAFE_INTEGER}
|
||||
displayTapToViewMessage={displayTapToViewMessage}
|
||||
downloadAttachment={() =>
|
||||
log.warn('MessageDetail: deleteMessageForEveryone called!')
|
||||
log.warn('MessageDetail: downloadAttachment called!')
|
||||
}
|
||||
doubleCheckMissingQuoteReference={doubleCheckMissingQuoteReference}
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
|
@ -355,7 +355,7 @@ export class MessageDetail extends React.Component<Props> {
|
|||
showExpiredOutgoingTapToViewToast
|
||||
}
|
||||
showMessageDetail={() => {
|
||||
log.warn('MessageDetail: deleteMessageForEveryone called!');
|
||||
log.warn('MessageDetail: showMessageDetail called!');
|
||||
}}
|
||||
showVisualAttachment={showVisualAttachment}
|
||||
startConversation={startConversation}
|
||||
|
|
|
@ -26,6 +26,7 @@ import type {
|
|||
} from './Message';
|
||||
import { doesMessageBodyOverflow } from './MessageBodyReadMore';
|
||||
import type { Props as ReactionPickerProps } from './ReactionPicker';
|
||||
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||
import { useToggleReactionPicker } from '../../hooks/useKeyboardShortcuts';
|
||||
|
||||
export type PropsData = {
|
||||
|
@ -237,6 +238,8 @@ export function TimelineMessage(props: Props): JSX.Element {
|
|||
|
||||
const handleReact = canReact ? () => toggleReactionPicker() : undefined;
|
||||
|
||||
const [hasDOEConfirmation, setHasDOEConfirmation] = useState(false);
|
||||
|
||||
const toggleReactionPickerKeyboard = useToggleReactionPicker(
|
||||
handleReact || noop
|
||||
);
|
||||
|
@ -253,6 +256,22 @@ export function TimelineMessage(props: Props): JSX.Element {
|
|||
|
||||
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
|
||||
{...props}
|
||||
renderingContext="conversation/TimelineItem"
|
||||
|
@ -319,7 +338,7 @@ export function TimelineMessage(props: Props): JSX.Element {
|
|||
onForward={canForward ? () => showForwardMessageModal(id) : undefined}
|
||||
onDeleteForMe={() => deleteMessage(id)}
|
||||
onDeleteForEveryone={
|
||||
canDeleteForEveryone ? () => deleteMessageForEveryone(id) : undefined
|
||||
canDeleteForEveryone ? () => setHasDOEConfirmation(true) : undefined
|
||||
}
|
||||
onMoreInfo={() => showMessageDetail(id)}
|
||||
/>
|
||||
|
|
|
@ -73,14 +73,12 @@ export type StateProps = {
|
|||
i18n: LocalizerType;
|
||||
isAdmin: boolean;
|
||||
isGroup: boolean;
|
||||
loadRecentMediaItems: (limit: number) => void;
|
||||
groupsInCommon: Array<ConversationType>;
|
||||
maxGroupSize: number;
|
||||
maxRecommendedGroupSize: number;
|
||||
memberships: Array<GroupV2Membership>;
|
||||
pendingApprovalMemberships: ReadonlyArray<GroupV2RequestingMembership>;
|
||||
pendingMemberships: ReadonlyArray<GroupV2PendingMembership>;
|
||||
setDisappearingMessages: (seconds: DurationInSeconds) => void;
|
||||
showAllMedia: () => void;
|
||||
showChatColorEditor: () => void;
|
||||
showGroupLinkManagement: () => void;
|
||||
|
@ -116,13 +114,15 @@ export type StateProps = {
|
|||
|
||||
type ActionProps = {
|
||||
deleteAvatarFromDisk: DeleteAvatarFromDiskActionType;
|
||||
loadRecentMediaItems: (id: string, limit: number) => void;
|
||||
replaceAvatar: ReplaceAvatarActionType;
|
||||
saveAvatarToDisk: SaveAvatarToDiskActionType;
|
||||
searchInConversation: (id: string) => unknown;
|
||||
setDisappearingMessages: (id: string, seconds: DurationInSeconds) => void;
|
||||
showContactModal: (contactId: string, conversationId?: string) => void;
|
||||
showConversation: ShowConversationType;
|
||||
toggleSafetyNumberModal: (conversationId: string) => unknown;
|
||||
searchInConversation: (id: string) => unknown;
|
||||
toggleAddUserToAnotherGroupModal: (contactId?: string) => void;
|
||||
toggleSafetyNumberModal: (conversationId: string) => unknown;
|
||||
};
|
||||
|
||||
export type Props = StateProps & ActionProps;
|
||||
|
@ -412,7 +412,9 @@ export function ConversationDetails({
|
|||
<DisappearingTimerSelect
|
||||
i18n={i18n}
|
||||
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 = {
|
||||
conversation: ConversationType;
|
||||
i18n: LocalizerType;
|
||||
loadRecentMediaItems: (limit: number) => void;
|
||||
loadRecentMediaItems: (id: string, limit: number) => void;
|
||||
showAllMedia: () => void;
|
||||
showLightboxForMedia: (
|
||||
selectedMediaItem: MediaItemType,
|
||||
|
@ -39,8 +39,8 @@ export function ConversationDetailsMediaList({
|
|||
const mediaItemsLength = mediaItems.length;
|
||||
|
||||
React.useEffect(() => {
|
||||
loadRecentMediaItems(MEDIA_ITEM_LIMIT);
|
||||
}, [loadRecentMediaItems, mediaItemsLength]);
|
||||
loadRecentMediaItems(conversation.id, MEDIA_ITEM_LIMIT);
|
||||
}, [conversation.id, loadRecentMediaItems, mediaItemsLength]);
|
||||
|
||||
if (mediaItemsLength === 0) {
|
||||
return null;
|
||||
|
|
|
@ -13,14 +13,19 @@ import { PanelSection } from './PanelSection';
|
|||
import { Select } from '../../Select';
|
||||
import { useUniqueId } from '../../../hooks/useUniqueId';
|
||||
|
||||
export type PropsType = {
|
||||
export type PropsDataType = {
|
||||
conversation?: ConversationType;
|
||||
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({
|
||||
conversation,
|
||||
i18n,
|
||||
|
@ -37,14 +42,17 @@ export function GroupV2Permissions({
|
|||
}
|
||||
|
||||
const updateAccessControlAttributes = (value: string) => {
|
||||
setAccessControlAttributesSetting(Number(value));
|
||||
setAccessControlAttributesSetting(conversation.id, Number(value));
|
||||
};
|
||||
const updateAccessControlMembers = (value: string) => {
|
||||
setAccessControlMembersSetting(Number(value));
|
||||
setAccessControlMembersSetting(conversation.id, Number(value));
|
||||
};
|
||||
const AccessControlEnum = Proto.AccessControl.AccessRequired;
|
||||
const updateAnnouncementsOnly = (value: string) => {
|
||||
setAnnouncementsOnly(Number(value) === AccessControlEnum.ADMINISTRATOR);
|
||||
setAnnouncementsOnly(
|
||||
conversation.id,
|
||||
Number(value) === AccessControlEnum.ADMINISTRATOR
|
||||
);
|
||||
};
|
||||
const accessControlOptions = getAccessControlOptions(i18n);
|
||||
const announcementsOnlyValue = String(
|
||||
|
|
|
@ -48,7 +48,9 @@ const conversation: ConversationType = {
|
|||
const OUR_UUID = UUID.generate().toString();
|
||||
|
||||
const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
approvePendingMembership: action('approvePendingMembership'),
|
||||
approvePendingMembershipFromGroupV2: action(
|
||||
'approvePendingMembershipFromGroupV2'
|
||||
),
|
||||
conversation,
|
||||
getPreferredBadge: () => undefined,
|
||||
i18n,
|
||||
|
@ -70,7 +72,9 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
|||
},
|
||||
})),
|
||||
],
|
||||
revokePendingMemberships: action('revokePendingMemberships'),
|
||||
revokePendingMembershipsFromGroupV2: action(
|
||||
'revokePendingMembershipsFromGroupV2'
|
||||
),
|
||||
theme: React.useContext(StorybookThemeContext),
|
||||
...overrideProps,
|
||||
});
|
||||
|
|
|
@ -17,18 +17,29 @@ import { ConversationDetailsIcon, IconType } from './ConversationDetailsIcon';
|
|||
import { isAccessControlEnabled } from '../../../groups/util';
|
||||
import { assertDev } from '../../../util/assert';
|
||||
|
||||
export type PropsType = {
|
||||
export type PropsDataType = {
|
||||
readonly conversation?: ConversationType;
|
||||
readonly getPreferredBadge: PreferredBadgeSelectorType;
|
||||
readonly i18n: LocalizerType;
|
||||
readonly ourUuid: UUIDStringType;
|
||||
readonly pendingApprovalMemberships: ReadonlyArray<GroupV2RequestingMembership>;
|
||||
readonly pendingMemberships: ReadonlyArray<GroupV2PendingMembership>;
|
||||
readonly approvePendingMembership: (conversationId: string) => void;
|
||||
readonly revokePendingMemberships: (conversationIds: Array<string>) => void;
|
||||
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 = {
|
||||
metadata: {
|
||||
addedByUserId?: UUIDStringType;
|
||||
|
@ -57,14 +68,14 @@ type StagedMembershipType = {
|
|||
};
|
||||
|
||||
export function PendingInvites({
|
||||
approvePendingMembership,
|
||||
approvePendingMembershipFromGroupV2,
|
||||
conversation,
|
||||
getPreferredBadge,
|
||||
i18n,
|
||||
ourUuid,
|
||||
pendingMemberships,
|
||||
pendingApprovalMemberships,
|
||||
revokePendingMemberships,
|
||||
revokePendingMembershipsFromGroupV2,
|
||||
theme,
|
||||
}: PropsType): JSX.Element {
|
||||
if (!conversation || !ourUuid) {
|
||||
|
@ -148,13 +159,17 @@ export function PendingInvites({
|
|||
|
||||
{stagedMemberships && stagedMemberships.length && (
|
||||
<MembershipActionConfirmation
|
||||
approvePendingMembership={approvePendingMembership}
|
||||
approvePendingMembershipFromGroupV2={
|
||||
approvePendingMembershipFromGroupV2
|
||||
}
|
||||
conversation={conversation}
|
||||
i18n={i18n}
|
||||
members={conversation.sortedGroupMembers || []}
|
||||
onClose={() => setStagedMemberships(null)}
|
||||
ourUuid={ourUuid}
|
||||
revokePendingMemberships={revokePendingMemberships}
|
||||
revokePendingMembershipsFromGroupV2={
|
||||
revokePendingMembershipsFromGroupV2
|
||||
}
|
||||
stagedMemberships={stagedMemberships}
|
||||
/>
|
||||
)}
|
||||
|
@ -163,29 +178,36 @@ export function PendingInvites({
|
|||
}
|
||||
|
||||
function MembershipActionConfirmation({
|
||||
approvePendingMembership,
|
||||
approvePendingMembershipFromGroupV2,
|
||||
conversation,
|
||||
i18n,
|
||||
members,
|
||||
onClose,
|
||||
ourUuid,
|
||||
revokePendingMemberships,
|
||||
revokePendingMembershipsFromGroupV2,
|
||||
stagedMemberships,
|
||||
}: {
|
||||
approvePendingMembership: (conversationId: string) => void;
|
||||
approvePendingMembershipFromGroupV2: (
|
||||
conversationId: string,
|
||||
memberId: string
|
||||
) => void;
|
||||
conversation: ConversationType;
|
||||
i18n: LocalizerType;
|
||||
members: Array<ConversationType>;
|
||||
onClose: () => void;
|
||||
ourUuid: string;
|
||||
revokePendingMemberships: (conversationIds: Array<string>) => void;
|
||||
revokePendingMembershipsFromGroupV2: (
|
||||
conversationId: string,
|
||||
memberIds: Array<string>
|
||||
) => void;
|
||||
stagedMemberships: Array<StagedMembershipType>;
|
||||
}) {
|
||||
const revokeStagedMemberships = () => {
|
||||
if (!stagedMemberships) {
|
||||
return;
|
||||
}
|
||||
revokePendingMemberships(
|
||||
revokePendingMembershipsFromGroupV2(
|
||||
conversation.id,
|
||||
stagedMemberships.map(({ membership }) => membership.member.id)
|
||||
);
|
||||
};
|
||||
|
@ -194,7 +216,10 @@ function MembershipActionConfirmation({
|
|||
if (!stagedMemberships) {
|
||||
return;
|
||||
}
|
||||
approvePendingMembership(stagedMemberships[0].membership.member.id);
|
||||
approvePendingMembershipFromGroupV2(
|
||||
conversation.id,
|
||||
stagedMemberships[0].membership.member.id
|
||||
);
|
||||
};
|
||||
|
||||
const membershipType = stagedMemberships[0].type;
|
||||
|
|
|
@ -150,6 +150,9 @@ import { getSendTarget } from '../util/getSendTarget';
|
|||
import { getRecipients } from '../util/getRecipients';
|
||||
import { validateConversation } from '../util/validateConversation';
|
||||
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 */
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
@ -436,29 +439,11 @@ export class ConversationModel extends window.Backbone
|
|||
}
|
||||
|
||||
private isMemberRequestingToJoin(uuid: UUID): boolean {
|
||||
if (!isGroupV2(this.attributes)) {
|
||||
return false;
|
||||
}
|
||||
const pendingAdminApprovalV2 = this.get('pendingAdminApprovalV2');
|
||||
|
||||
if (!pendingAdminApprovalV2 || !pendingAdminApprovalV2.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return pendingAdminApprovalV2.some(item => item.uuid === uuid.toString());
|
||||
return isMemberRequestingToJoin(this.attributes, uuid);
|
||||
}
|
||||
|
||||
isMemberPending(uuid: UUID): boolean {
|
||||
if (!isGroupV2(this.attributes)) {
|
||||
return false;
|
||||
}
|
||||
const pendingMembersV2 = this.get('pendingMembersV2');
|
||||
|
||||
if (!pendingMembersV2 || !pendingMembersV2.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return pendingMembersV2.some(item => item.uuid === uuid.toString());
|
||||
return isMemberPending(this.attributes, uuid);
|
||||
}
|
||||
|
||||
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(
|
||||
uuid: UUID
|
||||
): Promise<Proto.GroupChange.Actions | undefined> {
|
||||
|
@ -712,32 +675,7 @@ export class ConversationModel extends window.Backbone
|
|||
private async removePendingMember(
|
||||
uuids: ReadonlyArray<UUID>
|
||||
): Promise<Proto.GroupChange.Actions | undefined> {
|
||||
const idLog = this.idForLogging();
|
||||
|
||||
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,
|
||||
});
|
||||
return removePendingMember(this.attributes, uuids);
|
||||
}
|
||||
|
||||
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> {
|
||||
if (!isGroupV2(this.attributes)) {
|
||||
return;
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
without,
|
||||
} from 'lodash';
|
||||
|
||||
import type { AttachmentType } from '../../types/Attachment';
|
||||
import type { StateType as RootStateType } from '../reducer';
|
||||
import * as groups from '../../groups';
|
||||
import * as log from '../../logging/log';
|
||||
|
@ -90,12 +91,19 @@ import type { TimelineMessageLoadingState } from '../../util/timelineUtil';
|
|||
import {
|
||||
isDirectConversation,
|
||||
isGroup,
|
||||
isGroupV2,
|
||||
} from '../../util/whatTypeOfConversation';
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import { viewedReceiptsJobQueue } from '../../jobs/viewedReceiptsJobQueue';
|
||||
import { viewSyncJobQueue } from '../../jobs/viewSyncJobQueue';
|
||||
import { ReadStatus } from '../../messages/MessageReadStatus';
|
||||
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
|
||||
|
||||
|
@ -833,6 +841,7 @@ export type ConversationActionType =
|
|||
|
||||
export const actions = {
|
||||
addMemberToGroup,
|
||||
approvePendingMembershipFromGroupV2,
|
||||
cancelConversationVerification,
|
||||
changeHasGroupLink,
|
||||
clearCancelledConversationVerification,
|
||||
|
@ -854,9 +863,12 @@ export const actions = {
|
|||
conversationUnloaded,
|
||||
createGroup,
|
||||
deleteAvatarFromDisk,
|
||||
deleteMessageForEveryone,
|
||||
destroyMessages,
|
||||
discardMessages,
|
||||
doubleCheckMissingQuoteReference,
|
||||
generateNewGroupLink,
|
||||
loadRecentMediaItems,
|
||||
messageChanged,
|
||||
messageDeleted,
|
||||
messageExpanded,
|
||||
|
@ -872,31 +884,35 @@ export const actions = {
|
|||
resetAllChatColors,
|
||||
reviewGroupMemberNameCollision,
|
||||
reviewMessageRequestNameCollision,
|
||||
revokePendingMembershipsFromGroupV2,
|
||||
saveAvatarToDisk,
|
||||
scrollToMessage,
|
||||
selectMessage,
|
||||
setAccessControlAddFromInviteLinkSetting,
|
||||
setAccessControlAttributesSetting,
|
||||
setAccessControlMembersSetting,
|
||||
setAnnouncementsOnly,
|
||||
setComposeGroupAvatar,
|
||||
setComposeGroupExpireTimer,
|
||||
setComposeGroupName,
|
||||
setComposeSearchTerm,
|
||||
setDisappearingMessages,
|
||||
setIsFetchingUUID,
|
||||
setIsNearBottom,
|
||||
setMessageLoadingState,
|
||||
setPreJoinConversation,
|
||||
setRecentMediaItems,
|
||||
setSelectedConversationHeaderTitle,
|
||||
setSelectedConversationPanelDepth,
|
||||
setVoiceNotePlaybackRate,
|
||||
showArchivedConversations,
|
||||
showChooseGroupMembers,
|
||||
showInbox,
|
||||
showConversation,
|
||||
showInbox,
|
||||
startComposing,
|
||||
startSettingGroupMetadata,
|
||||
toggleAdmin,
|
||||
toggleConversationInChooseMembers,
|
||||
toggleComposeEditingAvatar,
|
||||
toggleConversationInChooseMembers,
|
||||
toggleGroupsForStorySend,
|
||||
toggleHideStories,
|
||||
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(
|
||||
conversationId: string
|
||||
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
|
||||
|
@ -1845,15 +1980,248 @@ function setSelectedConversationPanelDepth(
|
|||
payload: { panelDepth },
|
||||
};
|
||||
}
|
||||
function setRecentMediaItems(
|
||||
id: string,
|
||||
recentMediaItems: Array<MediaItemType>
|
||||
): SetRecentMediaItemsActionType {
|
||||
return {
|
||||
type: 'SET_RECENT_MEDIA_ITEMS',
|
||||
payload: { id, recentMediaItems },
|
||||
|
||||
function deleteMessageForEveryone(
|
||||
messageId: string
|
||||
): ThunkAction<
|
||||
void,
|
||||
RootStateType,
|
||||
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 {
|
||||
return { type: 'CLEAR_INVITED_UUIDS_FOR_NEWLY_CREATED_GROUP' };
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ export enum ToastType {
|
|||
FailedToDeleteUsername = 'FailedToDeleteUsername',
|
||||
CopiedUsername = 'CopiedUsername',
|
||||
CopiedUsernameLink = 'CopiedUsernameLink',
|
||||
DeleteForEveryoneFailed = 'DeleteForEveryoneFailed',
|
||||
}
|
||||
|
||||
// State
|
||||
|
@ -32,13 +33,13 @@ export type ToastStateType = {
|
|||
// Actions
|
||||
|
||||
const HIDE_TOAST = 'toast/HIDE_TOAST';
|
||||
const SHOW_TOAST = 'toast/SHOW_TOAST';
|
||||
export const SHOW_TOAST = 'toast/SHOW_TOAST';
|
||||
|
||||
type HideToastActionType = {
|
||||
type: typeof HIDE_TOAST;
|
||||
};
|
||||
|
||||
type ShowToastActionType = {
|
||||
export type ShowToastActionType = {
|
||||
type: typeof SHOW_TOAST;
|
||||
payload: {
|
||||
toastType: ToastType;
|
||||
|
|
|
@ -24,7 +24,6 @@ import {
|
|||
getPreferredBadgeSelector,
|
||||
} from '../selectors/badges';
|
||||
import { assertDev } from '../../util/assert';
|
||||
import type { DurationInSeconds } from '../../util/durations';
|
||||
import { SignalService as Proto } from '../../protobuf';
|
||||
import { getConversationColorAttributes } from '../../util/getConversationColorAttributes';
|
||||
import type { SmartChooseGroupMembersModalPropsType } from './ChooseGroupMembersModal';
|
||||
|
@ -39,8 +38,6 @@ import {
|
|||
export type SmartConversationDetailsProps = {
|
||||
addMembers: (conversationIds: ReadonlyArray<string>) => Promise<void>;
|
||||
conversationId: string;
|
||||
loadRecentMediaItems: (limit: number) => void;
|
||||
setDisappearingMessages: (seconds: DurationInSeconds) => void;
|
||||
showAllMedia: () => void;
|
||||
showChatColorEditor: () => void;
|
||||
showGroupLinkManagement: () => void;
|
||||
|
|
|
@ -25,20 +25,17 @@ import { mapDispatchToProps } from '../actions';
|
|||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import { strictAssert } from '../../util/assert';
|
||||
import { isSignalConversation } from '../../util/isSignalConversation';
|
||||
import type { DurationInSeconds } from '../../util/durations';
|
||||
|
||||
export type OwnProps = {
|
||||
id: string;
|
||||
|
||||
onArchive: () => void;
|
||||
onDeleteMessages: () => void;
|
||||
onGoBack: () => void;
|
||||
onMarkUnread: () => void;
|
||||
onMoveToInbox: () => void;
|
||||
onOutgoingAudioCallInConversation: () => void;
|
||||
onOutgoingVideoCallInConversation: () => void;
|
||||
onSearchInConversation: () => void;
|
||||
onSetDisappearingMessages: (seconds: DurationInSeconds) => void;
|
||||
onSetMuteNotifications: (seconds: number) => void;
|
||||
onSetPin: (value: boolean) => void;
|
||||
onShowAllMedia: () => void;
|
||||
|
|
|
@ -4,22 +4,20 @@
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
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 { getConversationSelector } from '../selectors/conversations';
|
||||
import { getIntl } from '../selectors/user';
|
||||
|
||||
export type SmartGroupV2PermissionsProps = {
|
||||
conversationId: string;
|
||||
setAccessControlAttributesSetting: (value: number) => void;
|
||||
setAccessControlMembersSetting: (value: number) => void;
|
||||
setAnnouncementsOnly: (value: boolean) => void;
|
||||
};
|
||||
|
||||
const mapStateToProps = (
|
||||
state: StateType,
|
||||
props: SmartGroupV2PermissionsProps
|
||||
): PropsType => {
|
||||
): PropsDataType => {
|
||||
const conversation = getConversationSelector(state)(props.conversationId);
|
||||
|
||||
return {
|
||||
|
@ -29,6 +27,6 @@ const mapStateToProps = (
|
|||
};
|
||||
};
|
||||
|
||||
const smart = connect(mapStateToProps);
|
||||
const smart = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
export const SmartGroupV2Permissions = smart(GroupV2Permissions);
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import { connect } from 'react-redux';
|
||||
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 type { StateType } from '../reducer';
|
||||
|
||||
|
@ -20,14 +20,12 @@ import type { UUIDStringType } from '../../types/UUID';
|
|||
export type SmartPendingInvitesProps = {
|
||||
conversationId: string;
|
||||
ourUuid: UUIDStringType;
|
||||
readonly approvePendingMembership: (conversationid: string) => void;
|
||||
readonly revokePendingMemberships: (membershipIds: Array<string>) => void;
|
||||
};
|
||||
|
||||
const mapStateToProps = (
|
||||
state: StateType,
|
||||
props: SmartPendingInvitesProps
|
||||
): PropsType => {
|
||||
): PropsDataType => {
|
||||
const conversationSelector = getConversationByIdSelector(state);
|
||||
const conversationByUuidSelector = getConversationByUuidSelector(state);
|
||||
|
||||
|
|
|
@ -66,7 +66,6 @@ export type TimelinePropsType = ExternalProps &
|
|||
| 'contactSupport'
|
||||
| 'blockGroupLinkRequests'
|
||||
| 'deleteMessage'
|
||||
| 'deleteMessageForEveryone'
|
||||
| 'displayTapToViewMessage'
|
||||
| 'downloadAttachment'
|
||||
| '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,
|
||||
ToastPropsType as ToastInternalErrorPropsType,
|
||||
} from '../components/ToastInternalError';
|
||||
import type { ToastDeleteForEveryoneFailed } from '../components/ToastDeleteForEveryoneFailed';
|
||||
import type { ToastExpired } from '../components/ToastExpired';
|
||||
import type {
|
||||
ToastFileSaved,
|
||||
|
@ -79,7 +78,6 @@ export function showToast(
|
|||
Toast: typeof ToastInternalError,
|
||||
props: ToastInternalErrorPropsType
|
||||
): void;
|
||||
export function showToast(Toast: typeof ToastDeleteForEveryoneFailed): void;
|
||||
export function showToast(Toast: typeof ToastExpired): void;
|
||||
export function showToast(
|
||||
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,
|
||||
} from '../util/whatTypeOfConversation';
|
||||
import { findAndFormatContact } from '../util/findAndFormatContact';
|
||||
import type { DurationInSeconds } from '../util/durations';
|
||||
import { getPreferredBadgeSelector } from '../state/selectors/badges';
|
||||
import {
|
||||
canReply,
|
||||
|
@ -67,7 +66,6 @@ import { ToastConversationArchived } from '../components/ToastConversationArchiv
|
|||
import { ToastConversationMarkedUnread } from '../components/ToastConversationMarkedUnread';
|
||||
import { ToastConversationUnarchived } from '../components/ToastConversationUnarchived';
|
||||
import { ToastDangerousFileType } from '../components/ToastDangerousFileType';
|
||||
import { ToastDeleteForEveryoneFailed } from '../components/ToastDeleteForEveryoneFailed';
|
||||
import { ToastExpired } from '../components/ToastExpired';
|
||||
import { ToastFileSize } from '../components/ToastFileSize';
|
||||
import { ToastInvalidConversation } from '../components/ToastInvalidConversation';
|
||||
|
@ -107,13 +105,13 @@ import {
|
|||
import { LinkPreviewSourceType } from '../types/LinkPreview';
|
||||
import { closeLightbox, showLightbox } from '../util/showLightbox';
|
||||
import { saveAttachment } from '../util/saveAttachment';
|
||||
import { sendDeleteForEveryoneMessage } from '../util/sendDeleteForEveryoneMessage';
|
||||
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';
|
||||
|
||||
type AttachmentOptions = {
|
||||
messageId: string;
|
||||
|
@ -136,7 +134,6 @@ const { getMessagesBySentAt } = window.Signal.Data;
|
|||
|
||||
type MessageActionsType = {
|
||||
deleteMessage: (messageId: string) => unknown;
|
||||
deleteMessageForEveryone: (messageId: string) => unknown;
|
||||
displayTapToViewMessage: (messageId: string) => unknown;
|
||||
downloadAttachment: (options: {
|
||||
attachment: AttachmentType;
|
||||
|
@ -337,9 +334,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
const conversationHeaderProps = {
|
||||
id: this.model.id,
|
||||
|
||||
onSetDisappearingMessages: (seconds: DurationInSeconds) =>
|
||||
this.setDisappearingMessages(seconds),
|
||||
onDeleteMessages: () => this.destroyMessages(),
|
||||
onSearchInConversation: () => {
|
||||
const { searchInConversation } = window.reduxActions.search;
|
||||
searchInConversation(this.model.id);
|
||||
|
@ -737,9 +731,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
const deleteMessage = (messageId: string) => {
|
||||
this.deleteMessage(messageId);
|
||||
};
|
||||
const deleteMessageForEveryone = (messageId: string) => {
|
||||
this.deleteMessageForEveryone(messageId);
|
||||
};
|
||||
const showMessageDetail = (messageId: string) => {
|
||||
this.showMessageDetail(messageId);
|
||||
};
|
||||
|
@ -826,11 +817,9 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
};
|
||||
|
||||
const showForwardMessageModal = this.showForwardMessageModal.bind(this);
|
||||
const startConversation = this.startConversation.bind(this);
|
||||
|
||||
return {
|
||||
deleteMessage,
|
||||
deleteMessageForEveryone,
|
||||
displayTapToViewMessage,
|
||||
downloadAttachment,
|
||||
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 {
|
||||
Stickers.downloadEphemeralPack(packId, packKey);
|
||||
|
||||
|
@ -1873,11 +1830,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
window.reduxStore,
|
||||
{
|
||||
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, {
|
||||
conversationId: this.model.id,
|
||||
ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(),
|
||||
approvePendingMembership: (conversationId: string) => {
|
||||
this.model.approvePendingMembershipFromGroupV2(conversationId);
|
||||
},
|
||||
revokePendingMemberships: conversationIds => {
|
||||
this.model.revokePendingMembershipsFromGroupV2(conversationIds);
|
||||
},
|
||||
}),
|
||||
});
|
||||
const headerTitle = window.i18n(
|
||||
|
@ -1971,8 +1917,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
const props = {
|
||||
addMembers: this.model.addMembersV2.bind(this.model),
|
||||
conversationId: this.model.get('id'),
|
||||
loadRecentMediaItems: this.loadRecentMediaItems.bind(this),
|
||||
setDisappearingMessages: this.setDisappearingMessages.bind(this),
|
||||
showAllMedia: this.showAllMedia.bind(this),
|
||||
showContactModal: this.showContactModal.bind(this),
|
||||
showChatColorEditor: this.showChatColorEditor.bind(this),
|
||||
|
@ -2092,10 +2036,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
hasSignalAccount={Boolean(signalAccount)}
|
||||
onSendMessage={() => {
|
||||
if (signalAccount) {
|
||||
this.startConversation(
|
||||
signalAccount.phoneNumber,
|
||||
signalAccount.uuid
|
||||
);
|
||||
startConversation(signalAccount.phoneNumber, signalAccount.uuid);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
@ -2108,20 +2049,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
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(
|
||||
conversationId: 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> {
|
||||
const recipientsByConversation = {
|
||||
[this.model.id]: {
|
||||
|
|
Loading…
Add table
Reference in a new issue