Introduce new 'Block request' button in timeline
This commit is contained in:
parent
536dd0c7b0
commit
703bb8a3a3
22 changed files with 1088 additions and 157 deletions
|
@ -5116,6 +5116,30 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"GroupV2--admin-approval-bounce--one": {
|
||||||
|
"message": "$joinerName$ requested and cancelled their request to join via the group link",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"joinerName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Alice"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--admin-approval-bounce": {
|
||||||
|
"message": "$joinerName$ requested and cancelled $numberOfRequests$ requests to join via the group link",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"joinerName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Alice"
|
||||||
|
},
|
||||||
|
"numberOfRequests": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
"GroupV2--group-link-add--disabled--you": {
|
"GroupV2--group-link-add--disabled--you": {
|
||||||
"message": "You turned on the group link with admin approval disabled.",
|
"message": "You turned on the group link with admin approval disabled.",
|
||||||
|
@ -5796,6 +5820,30 @@
|
||||||
"message": "Details about people invited to this group aren’t shown until they join. Invitees will only see messages after they join the group.",
|
"message": "Details about people invited to this group aren’t shown until they join. Invitees will only see messages after they join the group.",
|
||||||
"description": "Information shown below the invite list"
|
"description": "Information shown below the invite list"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"PendingRequests--block--button": {
|
||||||
|
"message": "Block request",
|
||||||
|
"description": "Shown in timeline if users cancel their request to join a group via a group link"
|
||||||
|
},
|
||||||
|
"PendingRequests--block--title": {
|
||||||
|
"message": "Block request?",
|
||||||
|
"description": "Title of dialog to block a user from requesting to join via the link again"
|
||||||
|
},
|
||||||
|
"PendingRequests--block--contents": {
|
||||||
|
"message": "$name$ will not be able to join or request to join this group via the group link. They can still be added to the group manually.",
|
||||||
|
"description": "Details of dialog to block a user from requesting to join via the link again",
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Annoying Person"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"PendingRequests--block--confirm": {
|
||||||
|
"message": "Block Request",
|
||||||
|
"description": "Confirmation button of dialog to block a user from requesting to join via the link again"
|
||||||
|
},
|
||||||
|
|
||||||
"AvatarInput--no-photo-label--group": {
|
"AvatarInput--no-photo-label--group": {
|
||||||
"message": "Add a group photo",
|
"message": "Add a group photo",
|
||||||
"description": "The label for the avatar uploader when no group photo is selected"
|
"description": "The label for the avatar uploader when no group photo is selected"
|
||||||
|
|
|
@ -70,6 +70,11 @@ export class Intl extends React.Component<Props> {
|
||||||
public override render() {
|
public override render() {
|
||||||
const { components, id, i18n, renderText } = this.props;
|
const { components, id, i18n, renderText } = this.props;
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
log.error('Error: Intl id prop not provided');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const text = i18n(id);
|
const text = i18n(id);
|
||||||
const results: Array<
|
const results: Array<
|
||||||
string | JSX.Element | Array<string | JSX.Element> | null
|
string | JSX.Element | Array<string | JSX.Element> | null
|
||||||
|
|
|
@ -3,10 +3,12 @@
|
||||||
|
|
||||||
/* eslint-disable-next-line max-classes-per-file */
|
/* eslint-disable-next-line max-classes-per-file */
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
import { storiesOf } from '@storybook/react';
|
import { storiesOf } from '@storybook/react';
|
||||||
|
|
||||||
import { setupI18n } from '../../util/setupI18n';
|
import { setupI18n } from '../../util/setupI18n';
|
||||||
import { UUID } from '../../types/UUID';
|
import { UUID } from '../../types/UUID';
|
||||||
|
import type { UUIDStringType } from '../../types/UUID';
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
import type { GroupV2ChangeType } from '../../groups';
|
import type { GroupV2ChangeType } from '../../groups';
|
||||||
import { SignalService as Proto } from '../../protobuf';
|
import { SignalService as Proto } from '../../protobuf';
|
||||||
|
@ -34,9 +36,29 @@ const renderContact: SmartContactRendererType<FullJSXType> = (
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderChange = (change: GroupV2ChangeType, groupName?: string) => (
|
const renderChange = (
|
||||||
|
change: GroupV2ChangeType,
|
||||||
|
{
|
||||||
|
groupBannedMemberships,
|
||||||
|
groupMemberships,
|
||||||
|
groupName,
|
||||||
|
areWeAdmin = true,
|
||||||
|
}: {
|
||||||
|
groupMemberships?: Array<{
|
||||||
|
uuid: UUIDStringType;
|
||||||
|
isAdmin: boolean;
|
||||||
|
}>;
|
||||||
|
groupBannedMemberships?: Array<UUIDStringType>;
|
||||||
|
groupName?: string;
|
||||||
|
areWeAdmin?: boolean;
|
||||||
|
} = {}
|
||||||
|
) => (
|
||||||
<GroupV2Change
|
<GroupV2Change
|
||||||
|
areWeAdmin={areWeAdmin ?? true}
|
||||||
|
blockGroupLinkRequests={action('blockGroupLinkRequests')}
|
||||||
change={change}
|
change={change}
|
||||||
|
groupBannedMemberships={groupBannedMemberships}
|
||||||
|
groupMemberships={groupMemberships}
|
||||||
groupName={groupName}
|
groupName={groupName}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
ourUuid={OUR_ID}
|
ourUuid={OUR_ID}
|
||||||
|
@ -1176,15 +1198,22 @@ storiesOf('Components/Conversation/GroupV2Change', module)
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})}
|
})}
|
||||||
{renderChange({
|
Should show button:
|
||||||
from: CONTACT_A,
|
{renderChange(
|
||||||
details: [
|
{
|
||||||
{
|
from: CONTACT_A,
|
||||||
type: 'admin-approval-remove-one',
|
details: [
|
||||||
uuid: CONTACT_A,
|
{
|
||||||
},
|
type: 'admin-approval-remove-one',
|
||||||
],
|
uuid: CONTACT_A,
|
||||||
})}
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
groupMemberships: [{ uuid: CONTACT_C, isAdmin: false }],
|
||||||
|
groupBannedMemberships: [CONTACT_B],
|
||||||
|
}
|
||||||
|
)}
|
||||||
{renderChange({
|
{renderChange({
|
||||||
from: ADMIN_A,
|
from: ADMIN_A,
|
||||||
details: [
|
details: [
|
||||||
|
@ -1194,14 +1223,62 @@ storiesOf('Components/Conversation/GroupV2Change', module)
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})}
|
})}
|
||||||
{renderChange({
|
Should show button:
|
||||||
details: [
|
{renderChange(
|
||||||
{
|
{
|
||||||
type: 'admin-approval-remove-one',
|
details: [
|
||||||
uuid: CONTACT_A,
|
{
|
||||||
},
|
type: 'admin-approval-remove-one',
|
||||||
],
|
uuid: CONTACT_A,
|
||||||
})}
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
groupMemberships: [{ uuid: CONTACT_C, isAdmin: false }],
|
||||||
|
groupBannedMemberships: [CONTACT_B],
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
Would show button, but we're not admin:
|
||||||
|
{renderChange(
|
||||||
|
{
|
||||||
|
from: CONTACT_A,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'admin-approval-remove-one',
|
||||||
|
uuid: CONTACT_A,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{ areWeAdmin: false, groupName: 'Group 1' }
|
||||||
|
)}
|
||||||
|
Would show button, but user is a group member:
|
||||||
|
{renderChange(
|
||||||
|
{
|
||||||
|
from: CONTACT_A,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'admin-approval-remove-one',
|
||||||
|
uuid: CONTACT_A,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ groupMemberships: [{ uuid: CONTACT_A, isAdmin: false }] }
|
||||||
|
)}
|
||||||
|
Would show button, but user is already banned:
|
||||||
|
{renderChange(
|
||||||
|
{
|
||||||
|
from: CONTACT_A,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'admin-approval-remove-one',
|
||||||
|
uuid: CONTACT_A,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{ groupBannedMemberships: [CONTACT_A] }
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
@ -1367,7 +1444,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
'We do hikes 🌲'
|
{ groupName: 'We do hikes 🌲' }
|
||||||
)}
|
)}
|
||||||
{renderChange(
|
{renderChange(
|
||||||
{
|
{
|
||||||
|
@ -1380,7 +1457,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
'We do hikes 🌲'
|
{ groupName: 'We do hikes 🌲' }
|
||||||
)}
|
)}
|
||||||
{renderChange(
|
{renderChange(
|
||||||
{
|
{
|
||||||
|
@ -1392,7 +1469,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
'We do hikes 🌲'
|
{ groupName: 'We do hikes 🌲' }
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
// Copyright 2020-2022 Signal Messenger, LLC
|
// Copyright 2020-2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import type { ReactElement } from 'react';
|
import type { ReactElement, ReactNode } from 'react';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { get } from 'lodash';
|
import { get } from 'lodash';
|
||||||
|
|
||||||
|
import * as log from '../../logging/log';
|
||||||
import type { ReplacementValuesType } from '../../types/I18N';
|
import type { ReplacementValuesType } from '../../types/I18N';
|
||||||
import type { FullJSXType } from '../Intl';
|
import type { FullJSXType } from '../Intl';
|
||||||
import { Intl } from '../Intl';
|
import { Intl } from '../Intl';
|
||||||
|
@ -19,19 +20,32 @@ import type { GroupV2ChangeType, GroupV2ChangeDetailType } from '../../groups';
|
||||||
import type { SmartContactRendererType } from '../../groupChange';
|
import type { SmartContactRendererType } from '../../groupChange';
|
||||||
import { renderChange } from '../../groupChange';
|
import { renderChange } from '../../groupChange';
|
||||||
import { Modal } from '../Modal';
|
import { Modal } from '../Modal';
|
||||||
|
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||||
|
|
||||||
export type PropsDataType = {
|
export type PropsDataType = {
|
||||||
|
areWeAdmin: boolean;
|
||||||
|
groupMemberships?: Array<{
|
||||||
|
uuid: UUIDStringType;
|
||||||
|
isAdmin: boolean;
|
||||||
|
}>;
|
||||||
|
groupBannedMemberships?: Array<UUIDStringType>;
|
||||||
groupName?: string;
|
groupName?: string;
|
||||||
ourUuid?: UUIDStringType;
|
ourUuid?: UUIDStringType;
|
||||||
change: GroupV2ChangeType;
|
change: GroupV2ChangeType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PropsActionsType = {
|
||||||
|
blockGroupLinkRequests: (uuid: UUIDStringType) => unknown;
|
||||||
|
};
|
||||||
|
|
||||||
export type PropsHousekeepingType = {
|
export type PropsHousekeepingType = {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
renderContact: SmartContactRendererType<FullJSXType>;
|
renderContact: SmartContactRendererType<FullJSXType>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PropsType = PropsDataType & PropsHousekeepingType;
|
export type PropsType = PropsDataType &
|
||||||
|
PropsActionsType &
|
||||||
|
PropsHousekeepingType;
|
||||||
|
|
||||||
function renderStringToIntl(
|
function renderStringToIntl(
|
||||||
id: string,
|
id: string,
|
||||||
|
@ -41,6 +55,12 @@ function renderStringToIntl(
|
||||||
return <Intl id={id} i18n={i18n} components={components} />;
|
return <Intl id={id} i18n={i18n} components={components} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ModalState {
|
||||||
|
None = 'None',
|
||||||
|
ViewingGroupDescription = 'ViewingGroupDescription',
|
||||||
|
ConfirmingblockGroupLinkRequests = 'ConfirmingblockGroupLinkRequests',
|
||||||
|
}
|
||||||
|
|
||||||
type GroupIconType =
|
type GroupIconType =
|
||||||
| 'group'
|
| 'group'
|
||||||
| 'group-access'
|
| 'group-access'
|
||||||
|
@ -58,6 +78,7 @@ const changeToIconMap = new Map<string, GroupIconType>([
|
||||||
['access-members', 'group-access'],
|
['access-members', 'group-access'],
|
||||||
['admin-approval-add-one', 'group-add'],
|
['admin-approval-add-one', 'group-add'],
|
||||||
['admin-approval-remove-one', 'group-decline'],
|
['admin-approval-remove-one', 'group-decline'],
|
||||||
|
['admin-approval-bounce', 'group-decline'],
|
||||||
['announcements-only', 'group-access'],
|
['announcements-only', 'group-access'],
|
||||||
['avatar', 'group-avatar'],
|
['avatar', 'group-avatar'],
|
||||||
['description', 'group-edit'],
|
['description', 'group-edit'],
|
||||||
|
@ -79,6 +100,7 @@ const changeToIconMap = new Map<string, GroupIconType>([
|
||||||
|
|
||||||
function getIcon(
|
function getIcon(
|
||||||
detail: GroupV2ChangeDetailType,
|
detail: GroupV2ChangeDetailType,
|
||||||
|
isLastText = true,
|
||||||
fromId?: UUIDStringType
|
fromId?: UUIDStringType
|
||||||
): GroupIconType {
|
): GroupIconType {
|
||||||
const changeType = detail.type;
|
const changeType = detail.type;
|
||||||
|
@ -92,52 +114,170 @@ function getIcon(
|
||||||
possibleIcon = 'group-approved';
|
possibleIcon = 'group-approved';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Use default icon for "... requested to join via group link" added to
|
||||||
|
// bounce notification.
|
||||||
|
if (changeType === 'admin-approval-bounce' && isLastText) {
|
||||||
|
possibleIcon = undefined;
|
||||||
|
}
|
||||||
return possibleIcon || 'group';
|
return possibleIcon || 'group';
|
||||||
}
|
}
|
||||||
|
|
||||||
function GroupV2Detail({
|
function GroupV2Detail({
|
||||||
|
areWeAdmin,
|
||||||
|
blockGroupLinkRequests,
|
||||||
detail,
|
detail,
|
||||||
i18n,
|
isLastText,
|
||||||
fromId,
|
fromId,
|
||||||
onButtonClick,
|
groupMemberships,
|
||||||
|
groupBannedMemberships,
|
||||||
|
groupName,
|
||||||
|
i18n,
|
||||||
|
ourUuid,
|
||||||
|
renderContact,
|
||||||
text,
|
text,
|
||||||
}: {
|
}: {
|
||||||
|
areWeAdmin: boolean;
|
||||||
|
blockGroupLinkRequests: (uuid: UUIDStringType) => unknown;
|
||||||
detail: GroupV2ChangeDetailType;
|
detail: GroupV2ChangeDetailType;
|
||||||
|
isLastText: boolean;
|
||||||
|
groupMemberships?: Array<{
|
||||||
|
uuid: UUIDStringType;
|
||||||
|
isAdmin: boolean;
|
||||||
|
}>;
|
||||||
|
groupBannedMemberships?: Array<UUIDStringType>;
|
||||||
|
groupName?: string;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
fromId?: UUIDStringType;
|
fromId?: UUIDStringType;
|
||||||
onButtonClick: (x: string) => unknown;
|
ourUuid?: UUIDStringType;
|
||||||
|
renderContact: SmartContactRendererType<FullJSXType>;
|
||||||
text: FullJSXType;
|
text: FullJSXType;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const icon = getIcon(detail, fromId);
|
const icon = getIcon(detail, isLastText, fromId);
|
||||||
|
let buttonNode: ReactNode;
|
||||||
|
|
||||||
const newGroupDescription =
|
const [modalState, setModalState] = useState<ModalState>(ModalState.None);
|
||||||
detail.type === 'description' && get(detail, 'description');
|
let modalNode: ReactNode;
|
||||||
|
|
||||||
|
switch (modalState) {
|
||||||
|
case ModalState.None:
|
||||||
|
modalNode = undefined;
|
||||||
|
break;
|
||||||
|
case ModalState.ViewingGroupDescription:
|
||||||
|
if (detail.type !== 'description' || !detail.description) {
|
||||||
|
log.warn(
|
||||||
|
'GroupV2Detail: ViewingGroupDescription but missing description or wrong change type'
|
||||||
|
);
|
||||||
|
modalNode = undefined;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
modalNode = (
|
||||||
|
<Modal
|
||||||
|
hasXButton
|
||||||
|
i18n={i18n}
|
||||||
|
title={groupName}
|
||||||
|
onClose={() => setModalState(ModalState.None)}
|
||||||
|
>
|
||||||
|
<GroupDescriptionText text={detail.description} />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case ModalState.ConfirmingblockGroupLinkRequests:
|
||||||
|
if (
|
||||||
|
!isLastText ||
|
||||||
|
detail.type !== 'admin-approval-bounce' ||
|
||||||
|
!detail.uuid
|
||||||
|
) {
|
||||||
|
log.warn(
|
||||||
|
'GroupV2Detail: ConfirmingblockGroupLinkRequests but missing uuid or wrong change type'
|
||||||
|
);
|
||||||
|
modalNode = undefined;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
modalNode = (
|
||||||
|
<ConfirmationDialog
|
||||||
|
title={i18n('PendingRequests--block--title')}
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
action: () => blockGroupLinkRequests(detail.uuid),
|
||||||
|
text: i18n('PendingRequests--block--confirm'),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
i18n={i18n}
|
||||||
|
onClose={() => setModalState(ModalState.None)}
|
||||||
|
>
|
||||||
|
<Intl
|
||||||
|
id="PendingRequests--block--contents"
|
||||||
|
i18n={i18n}
|
||||||
|
components={{
|
||||||
|
name: renderContact(detail.uuid),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ConfirmationDialog>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default: {
|
||||||
|
const state: never = modalState;
|
||||||
|
log.warn(`GroupV2Detail: unexpected modal state ${state}`);
|
||||||
|
modalNode = undefined;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (detail.type === 'description' && detail.description) {
|
||||||
|
buttonNode = (
|
||||||
|
<Button
|
||||||
|
onClick={() => setModalState(ModalState.ViewingGroupDescription)}
|
||||||
|
size={ButtonSize.Small}
|
||||||
|
variant={ButtonVariant.SystemMessage}
|
||||||
|
>
|
||||||
|
{i18n('view')}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
|
isLastText &&
|
||||||
|
detail.type === 'admin-approval-bounce' &&
|
||||||
|
areWeAdmin &&
|
||||||
|
detail.uuid &&
|
||||||
|
detail.uuid !== ourUuid &&
|
||||||
|
(!fromId || fromId === detail.uuid) &&
|
||||||
|
!groupMemberships?.some(item => item.uuid === detail.uuid) &&
|
||||||
|
!groupBannedMemberships?.some(uuid => uuid === detail.uuid)
|
||||||
|
) {
|
||||||
|
buttonNode = (
|
||||||
|
<Button
|
||||||
|
onClick={() =>
|
||||||
|
setModalState(ModalState.ConfirmingblockGroupLinkRequests)
|
||||||
|
}
|
||||||
|
size={ButtonSize.Small}
|
||||||
|
variant={ButtonVariant.SystemMessage}
|
||||||
|
>
|
||||||
|
{i18n('PendingRequests--block--button')}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SystemMessage
|
<>
|
||||||
icon={icon}
|
<SystemMessage icon={icon} contents={text} button={buttonNode} />
|
||||||
contents={text}
|
{modalNode}
|
||||||
button={
|
</>
|
||||||
newGroupDescription ? (
|
|
||||||
<Button
|
|
||||||
onClick={() => onButtonClick(newGroupDescription)}
|
|
||||||
size={ButtonSize.Small}
|
|
||||||
variant={ButtonVariant.SystemMessage}
|
|
||||||
>
|
|
||||||
{i18n('view')}
|
|
||||||
</Button>
|
|
||||||
) : undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GroupV2Change(props: PropsType): ReactElement {
|
export function GroupV2Change(props: PropsType): ReactElement {
|
||||||
const { change, groupName, i18n, ourUuid, renderContact } = props;
|
const {
|
||||||
|
areWeAdmin,
|
||||||
const [groupDescription, setGroupDescription] = useState<
|
blockGroupLinkRequests,
|
||||||
string | undefined
|
change,
|
||||||
>();
|
groupBannedMemberships,
|
||||||
|
groupMemberships,
|
||||||
|
groupName,
|
||||||
|
i18n,
|
||||||
|
ourUuid,
|
||||||
|
renderContact,
|
||||||
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -146,30 +286,27 @@ export function GroupV2Change(props: PropsType): ReactElement {
|
||||||
ourUuid,
|
ourUuid,
|
||||||
renderContact,
|
renderContact,
|
||||||
renderString: renderStringToIntl,
|
renderString: renderStringToIntl,
|
||||||
}).map((text: FullJSXType, index: number) => (
|
}).map(({ detail, isLastText, text }, index) => {
|
||||||
<GroupV2Detail
|
return (
|
||||||
detail={change.details[index]}
|
<GroupV2Detail
|
||||||
fromId={change.from}
|
areWeAdmin={areWeAdmin}
|
||||||
i18n={i18n}
|
blockGroupLinkRequests={blockGroupLinkRequests}
|
||||||
// Difficult to find a unique key for this type
|
detail={detail}
|
||||||
// eslint-disable-next-line react/no-array-index-key
|
isLastText={isLastText}
|
||||||
key={index}
|
fromId={change.from}
|
||||||
onButtonClick={nextGroupDescription =>
|
groupBannedMemberships={groupBannedMemberships}
|
||||||
setGroupDescription(nextGroupDescription)
|
groupMemberships={groupMemberships}
|
||||||
}
|
groupName={groupName}
|
||||||
text={text}
|
i18n={i18n}
|
||||||
/>
|
// Difficult to find a unique key for this type
|
||||||
))}
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
{groupDescription ? (
|
key={index}
|
||||||
<Modal
|
ourUuid={ourUuid}
|
||||||
hasXButton
|
renderContact={renderContact}
|
||||||
i18n={i18n}
|
text={text}
|
||||||
title={groupName}
|
/>
|
||||||
onClose={() => setGroupDescription(undefined)}
|
);
|
||||||
>
|
})}
|
||||||
<GroupDescriptionText text={groupDescription} />
|
|
||||||
</Modal>
|
|
||||||
) : null}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -337,6 +337,7 @@ const actions = () => ({
|
||||||
acknowledgeGroupMemberNameCollisions: action(
|
acknowledgeGroupMemberNameCollisions: action(
|
||||||
'acknowledgeGroupMemberNameCollisions'
|
'acknowledgeGroupMemberNameCollisions'
|
||||||
),
|
),
|
||||||
|
blockGroupLinkRequests: action('blockGroupLinkRequests'),
|
||||||
checkForAccount: action('checkForAccount'),
|
checkForAccount: action('checkForAccount'),
|
||||||
clearInvitedUuidsForNewlyCreatedGroup: action(
|
clearInvitedUuidsForNewlyCreatedGroup: action(
|
||||||
'clearInvitedUuidsForNewlyCreatedGroup'
|
'clearInvitedUuidsForNewlyCreatedGroup'
|
||||||
|
|
|
@ -21,6 +21,7 @@ import { WidthBreakpoint } from '../_util';
|
||||||
import type { PropsActions as MessageActionsType } from './Message';
|
import type { PropsActions as MessageActionsType } from './Message';
|
||||||
import type { PropsActions as UnsupportedMessageActionsType } from './UnsupportedMessage';
|
import type { PropsActions as UnsupportedMessageActionsType } from './UnsupportedMessage';
|
||||||
import type { PropsActionsType as ChatSessionRefreshedNotificationActionsType } from './ChatSessionRefreshedNotification';
|
import type { PropsActionsType as ChatSessionRefreshedNotificationActionsType } from './ChatSessionRefreshedNotification';
|
||||||
|
import type { PropsActionsType as GroupV2ChangeActionsType } from './GroupV2Change';
|
||||||
import { ErrorBoundary } from './ErrorBoundary';
|
import { ErrorBoundary } from './ErrorBoundary';
|
||||||
import type { PropsActions as SafetyNumberActionsType } from './SafetyNumberNotification';
|
import type { PropsActions as SafetyNumberActionsType } from './SafetyNumberNotification';
|
||||||
import { Intl } from '../Intl';
|
import { Intl } from '../Intl';
|
||||||
|
@ -167,6 +168,7 @@ export type PropsActionsType = {
|
||||||
} & MessageActionsType &
|
} & MessageActionsType &
|
||||||
SafetyNumberActionsType &
|
SafetyNumberActionsType &
|
||||||
UnsupportedMessageActionsType &
|
UnsupportedMessageActionsType &
|
||||||
|
GroupV2ChangeActionsType &
|
||||||
ChatSessionRefreshedNotificationActionsType;
|
ChatSessionRefreshedNotificationActionsType;
|
||||||
|
|
||||||
export type PropsType = PropsDataType &
|
export type PropsType = PropsDataType &
|
||||||
|
@ -199,6 +201,7 @@ const getActions = createSelector(
|
||||||
(props: PropsType): PropsActionsType => {
|
(props: PropsType): PropsActionsType => {
|
||||||
const unsafe = pick(props, [
|
const unsafe = pick(props, [
|
||||||
'acknowledgeGroupMemberNameCollisions',
|
'acknowledgeGroupMemberNameCollisions',
|
||||||
|
'blockGroupLinkRequests',
|
||||||
'clearInvitedUuidsForNewlyCreatedGroup',
|
'clearInvitedUuidsForNewlyCreatedGroup',
|
||||||
'closeContactSpoofingReview',
|
'closeContactSpoofingReview',
|
||||||
'setIsNearBottom',
|
'setIsNearBottom',
|
||||||
|
|
|
@ -65,6 +65,7 @@ const getDefaultProps = () => ({
|
||||||
replyToMessage: action('replyToMessage'),
|
replyToMessage: action('replyToMessage'),
|
||||||
retryDeleteForEveryone: action('retryDeleteForEveryone'),
|
retryDeleteForEveryone: action('retryDeleteForEveryone'),
|
||||||
retrySend: action('retrySend'),
|
retrySend: action('retrySend'),
|
||||||
|
blockGroupLinkRequests: action('blockGroupLinkRequests'),
|
||||||
deleteMessage: action('deleteMessage'),
|
deleteMessage: action('deleteMessage'),
|
||||||
deleteMessageForEveryone: action('deleteMessageForEveryone'),
|
deleteMessageForEveryone: action('deleteMessageForEveryone'),
|
||||||
kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
|
kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
|
||||||
|
|
|
@ -45,7 +45,10 @@ import type { PropsData as VerificationNotificationProps } from './VerificationN
|
||||||
import { VerificationNotification } from './VerificationNotification';
|
import { VerificationNotification } from './VerificationNotification';
|
||||||
import type { PropsData as GroupNotificationProps } from './GroupNotification';
|
import type { PropsData as GroupNotificationProps } from './GroupNotification';
|
||||||
import { GroupNotification } from './GroupNotification';
|
import { GroupNotification } from './GroupNotification';
|
||||||
import type { PropsDataType as GroupV2ChangeProps } from './GroupV2Change';
|
import type {
|
||||||
|
PropsDataType as GroupV2ChangeProps,
|
||||||
|
PropsActionsType as GroupV2ChangeActionsType,
|
||||||
|
} from './GroupV2Change';
|
||||||
import { GroupV2Change } from './GroupV2Change';
|
import { GroupV2Change } from './GroupV2Change';
|
||||||
import type { PropsDataType as GroupV1MigrationProps } from './GroupV1Migration';
|
import type { PropsDataType as GroupV1MigrationProps } from './GroupV1Migration';
|
||||||
import { GroupV1Migration } from './GroupV1Migration';
|
import { GroupV1Migration } from './GroupV1Migration';
|
||||||
|
@ -161,6 +164,7 @@ type PropsLocalType = {
|
||||||
type PropsActionsType = MessageActionsType &
|
type PropsActionsType = MessageActionsType &
|
||||||
CallingNotificationActionsType &
|
CallingNotificationActionsType &
|
||||||
DeliveryIssueActionProps &
|
DeliveryIssueActionProps &
|
||||||
|
GroupV2ChangeActionsType &
|
||||||
PropsChatSessionRefreshedActionsType &
|
PropsChatSessionRefreshedActionsType &
|
||||||
UnsupportedMessageActionsType &
|
UnsupportedMessageActionsType &
|
||||||
SafetyNumberActionsType;
|
SafetyNumberActionsType;
|
||||||
|
@ -190,7 +194,6 @@ export class TimelineItem extends React.PureComponent<PropsType> {
|
||||||
theme,
|
theme,
|
||||||
nextItem,
|
nextItem,
|
||||||
previousItem,
|
previousItem,
|
||||||
renderContact,
|
|
||||||
renderUniversalTimerNotification,
|
renderUniversalTimerNotification,
|
||||||
returnToActiveCall,
|
returnToActiveCall,
|
||||||
selectMessage,
|
selectMessage,
|
||||||
|
@ -294,11 +297,7 @@ export class TimelineItem extends React.PureComponent<PropsType> {
|
||||||
);
|
);
|
||||||
} else if (item.type === 'groupV2Change') {
|
} else if (item.type === 'groupV2Change') {
|
||||||
notification = (
|
notification = (
|
||||||
<GroupV2Change
|
<GroupV2Change {...this.props} {...item.data} i18n={i18n} />
|
||||||
renderContact={renderContact}
|
|
||||||
{...item.data}
|
|
||||||
i18n={i18n}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
} else if (item.type === 'groupV1Migration') {
|
} else if (item.type === 'groupV1Migration') {
|
||||||
notification = (
|
notification = (
|
||||||
|
|
|
@ -28,24 +28,44 @@ export type RenderOptionsType<T> = {
|
||||||
const AccessControlEnum = Proto.AccessControl.AccessRequired;
|
const AccessControlEnum = Proto.AccessControl.AccessRequired;
|
||||||
const RoleEnum = Proto.Member.Role;
|
const RoleEnum = Proto.Member.Role;
|
||||||
|
|
||||||
|
export type RenderChangeResultType<T> = ReadonlyArray<
|
||||||
|
Readonly<{
|
||||||
|
detail: GroupV2ChangeDetailType;
|
||||||
|
text: T | string;
|
||||||
|
|
||||||
|
// Used to differentiate between the multiple texts produced by
|
||||||
|
// 'admin-approval-bounce'
|
||||||
|
isLastText: boolean;
|
||||||
|
}>
|
||||||
|
>;
|
||||||
|
|
||||||
export function renderChange<T>(
|
export function renderChange<T>(
|
||||||
change: GroupV2ChangeType,
|
change: GroupV2ChangeType,
|
||||||
options: RenderOptionsType<T>
|
options: RenderOptionsType<T>
|
||||||
): Array<T | string> {
|
): RenderChangeResultType<T> {
|
||||||
const { details, from } = change;
|
const { details, from } = change;
|
||||||
|
|
||||||
return details.map((detail: GroupV2ChangeDetailType) =>
|
return details.flatMap((detail: GroupV2ChangeDetailType) => {
|
||||||
renderChangeDetail<T>(detail, {
|
const texts = renderChangeDetail<T>(detail, {
|
||||||
...options,
|
...options,
|
||||||
from,
|
from,
|
||||||
})
|
});
|
||||||
);
|
|
||||||
|
if (!Array.isArray(texts)) {
|
||||||
|
return { detail, isLastText: true, text: texts };
|
||||||
|
}
|
||||||
|
|
||||||
|
return texts.map((text, index) => {
|
||||||
|
const isLastText = index === texts.length - 1;
|
||||||
|
return { detail, isLastText, text };
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderChangeDetail<T>(
|
export function renderChangeDetail<T>(
|
||||||
detail: GroupV2ChangeDetailType,
|
detail: GroupV2ChangeDetailType,
|
||||||
options: RenderOptionsType<T>
|
options: RenderOptionsType<T>
|
||||||
): T | string {
|
): T | string | ReadonlyArray<T | string> {
|
||||||
const { from, i18n, ourUuid, renderContact, renderString } = options;
|
const { from, i18n, ourUuid, renderContact, renderString } = options;
|
||||||
const fromYou = Boolean(from && ourUuid && from === ourUuid);
|
const fromYou = Boolean(from && ourUuid && from === ourUuid);
|
||||||
|
|
||||||
|
@ -768,6 +788,38 @@ export function renderChangeDetail<T>(
|
||||||
[renderContact(uuid)]
|
[renderContact(uuid)]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (detail.type === 'admin-approval-bounce') {
|
||||||
|
const { uuid, times, isApprovalPending } = detail;
|
||||||
|
|
||||||
|
let firstMessage: T | string;
|
||||||
|
if (times === 1) {
|
||||||
|
firstMessage = renderString('GroupV2--admin-approval-bounce--one', i18n, {
|
||||||
|
joinerName: renderContact(uuid),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
firstMessage = renderString('GroupV2--admin-approval-bounce', i18n, {
|
||||||
|
joinerName: renderContact(uuid),
|
||||||
|
numberOfRequests: String(times),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isApprovalPending) {
|
||||||
|
return firstMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
const secondMessage = renderChangeDetail(
|
||||||
|
{
|
||||||
|
type: 'admin-approval-add-one',
|
||||||
|
uuid,
|
||||||
|
},
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
firstMessage,
|
||||||
|
...(Array.isArray(secondMessage) ? secondMessage : [secondMessage]),
|
||||||
|
];
|
||||||
|
}
|
||||||
if (detail.type === 'group-link-add') {
|
if (detail.type === 'group-link-add') {
|
||||||
const { privilege } = detail;
|
const { privilege } = detail;
|
||||||
|
|
||||||
|
|
364
ts/groups.ts
364
ts/groups.ts
|
@ -186,6 +186,12 @@ type GroupV2AdminApprovalRemoveOneChangeType = {
|
||||||
uuid: UUIDStringType;
|
uuid: UUIDStringType;
|
||||||
inviter?: UUIDStringType;
|
inviter?: UUIDStringType;
|
||||||
};
|
};
|
||||||
|
type GroupV2AdminApprovalBounceChangeType = {
|
||||||
|
type: 'admin-approval-bounce';
|
||||||
|
times: number;
|
||||||
|
isApprovalPending: boolean;
|
||||||
|
uuid: UUIDStringType;
|
||||||
|
};
|
||||||
export type GroupV2DescriptionChangeType = {
|
export type GroupV2DescriptionChangeType = {
|
||||||
type: 'description';
|
type: 'description';
|
||||||
removed?: boolean;
|
removed?: boolean;
|
||||||
|
@ -200,6 +206,7 @@ export type GroupV2ChangeDetailType =
|
||||||
| GroupV2AccessMembersChangeType
|
| GroupV2AccessMembersChangeType
|
||||||
| GroupV2AdminApprovalAddOneChangeType
|
| GroupV2AdminApprovalAddOneChangeType
|
||||||
| GroupV2AdminApprovalRemoveOneChangeType
|
| GroupV2AdminApprovalRemoveOneChangeType
|
||||||
|
| GroupV2AdminApprovalBounceChangeType
|
||||||
| GroupV2AnnouncementsOnlyChangeType
|
| GroupV2AnnouncementsOnlyChangeType
|
||||||
| GroupV2AvatarChangeType
|
| GroupV2AvatarChangeType
|
||||||
| GroupV2DescriptionChangeType
|
| GroupV2DescriptionChangeType
|
||||||
|
@ -249,7 +256,7 @@ type MemberType = {
|
||||||
};
|
};
|
||||||
type UpdatesResultType = {
|
type UpdatesResultType = {
|
||||||
// The array of new messages to be added into the message timeline
|
// The array of new messages to be added into the message timeline
|
||||||
groupChangeMessages: Array<MessageAttributesType>;
|
groupChangeMessages: Array<GroupChangeMessageType>;
|
||||||
// The set of members in the group, and we largely just pull profile keys for each,
|
// The set of members in the group, and we largely just pull profile keys for each,
|
||||||
// because the group membership is updated in newAttributes
|
// because the group membership is updated in newAttributes
|
||||||
members: Array<MemberType>;
|
members: Array<MemberType>;
|
||||||
|
@ -263,6 +270,33 @@ type UploadedAvatarType = {
|
||||||
key: string;
|
key: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type BasicMessageType = Pick<MessageAttributesType, 'id' | 'schemaVersion'>;
|
||||||
|
|
||||||
|
type GroupV2ChangeMessageType = {
|
||||||
|
type: 'group-v2-change';
|
||||||
|
} & Pick<MessageAttributesType, 'groupV2Change' | 'sourceUuid'>;
|
||||||
|
|
||||||
|
type GroupV1MigrationMessageType = {
|
||||||
|
type: 'group-v1-migration';
|
||||||
|
} & Pick<
|
||||||
|
MessageAttributesType,
|
||||||
|
'invitedGV2Members' | 'droppedGV2MemberIds' | 'groupMigration'
|
||||||
|
>;
|
||||||
|
|
||||||
|
type TimerNotificationMessageType = {
|
||||||
|
type: 'timer-notification';
|
||||||
|
} & Pick<
|
||||||
|
MessageAttributesType,
|
||||||
|
'sourceUuid' | 'flags' | 'expirationTimerUpdate'
|
||||||
|
>;
|
||||||
|
|
||||||
|
type GroupChangeMessageType = BasicMessageType &
|
||||||
|
(
|
||||||
|
| GroupV2ChangeMessageType
|
||||||
|
| GroupV1MigrationMessageType
|
||||||
|
| TimerNotificationMessageType
|
||||||
|
);
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
|
|
||||||
export const MASTER_KEY_LENGTH = 32;
|
export const MASTER_KEY_LENGTH = 32;
|
||||||
|
@ -277,6 +311,14 @@ const SUPPORTED_CHANGE_EPOCH = 4;
|
||||||
export const LINK_VERSION_ERROR = 'LINK_VERSION_ERROR';
|
export const LINK_VERSION_ERROR = 'LINK_VERSION_ERROR';
|
||||||
const GROUP_INVITE_LINK_PASSWORD_LENGTH = 16;
|
const GROUP_INVITE_LINK_PASSWORD_LENGTH = 16;
|
||||||
|
|
||||||
|
function generateBasicMessage(): BasicMessageType {
|
||||||
|
return {
|
||||||
|
id: getGuid(),
|
||||||
|
schemaVersion: MAX_MESSAGE_SCHEMA,
|
||||||
|
// this is missing most properties to fulfill this type
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Group Links
|
// Group Links
|
||||||
|
|
||||||
export function generateGroupInviteLinkPassword(): Uint8Array {
|
export function generateGroupInviteLinkPassword(): Uint8Array {
|
||||||
|
@ -1138,6 +1180,47 @@ export function buildDeleteMemberChange({
|
||||||
return actions;
|
return actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildAddBannedMemberChange({
|
||||||
|
uuid,
|
||||||
|
group,
|
||||||
|
}: {
|
||||||
|
uuid: UUIDStringType;
|
||||||
|
group: ConversationAttributesType;
|
||||||
|
}): Proto.GroupChange.Actions {
|
||||||
|
const actions = new Proto.GroupChange.Actions();
|
||||||
|
|
||||||
|
if (!group.secretParams) {
|
||||||
|
throw new Error(
|
||||||
|
'buildAddBannedMemberChange: group was missing secretParams!'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const clientZkGroupCipher = getClientZkGroupCipher(group.secretParams);
|
||||||
|
const uuidCipherTextBuffer = encryptUuid(clientZkGroupCipher, uuid);
|
||||||
|
|
||||||
|
const addMemberBannedAction =
|
||||||
|
new Proto.GroupChange.Actions.AddMemberBannedAction();
|
||||||
|
|
||||||
|
addMemberBannedAction.added = new Proto.MemberBanned();
|
||||||
|
addMemberBannedAction.added.userId = uuidCipherTextBuffer;
|
||||||
|
|
||||||
|
actions.addMembersBanned = [addMemberBannedAction];
|
||||||
|
|
||||||
|
if (group.pendingAdminApprovalV2?.some(item => item.uuid === uuid)) {
|
||||||
|
const deleteMemberPendingAdminApprovalAction =
|
||||||
|
new Proto.GroupChange.Actions.DeleteMemberPendingAdminApprovalAction();
|
||||||
|
|
||||||
|
deleteMemberPendingAdminApprovalAction.deletedUserId = uuidCipherTextBuffer;
|
||||||
|
|
||||||
|
actions.deleteMemberPendingAdminApprovals = [
|
||||||
|
deleteMemberPendingAdminApprovalAction,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.version = (group.revision || 0) + 1;
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
export function buildModifyMemberRoleChange({
|
export function buildModifyMemberRoleChange({
|
||||||
uuid,
|
uuid,
|
||||||
group,
|
group,
|
||||||
|
@ -1692,13 +1775,14 @@ export async function createGroupV2({
|
||||||
conversationId: conversation.id,
|
conversationId: conversation.id,
|
||||||
received_at: window.Signal.Util.incrementMessageCounter(),
|
received_at: window.Signal.Util.incrementMessageCounter(),
|
||||||
received_at_ms: timestamp,
|
received_at_ms: timestamp,
|
||||||
|
timestamp,
|
||||||
sent_at: timestamp,
|
sent_at: timestamp,
|
||||||
groupV2Change: {
|
groupV2Change: {
|
||||||
from: ourUuid,
|
from: ourUuid,
|
||||||
details: [{ type: 'create' }],
|
details: [{ type: 'create' }],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
await window.Signal.Data.saveMessages([createdTheGroupMessage], {
|
await dataInterface.saveMessages([createdTheGroupMessage], {
|
||||||
forceSave: true,
|
forceSave: true,
|
||||||
ourUuid,
|
ourUuid,
|
||||||
});
|
});
|
||||||
|
@ -2127,7 +2211,7 @@ export async function initiateMigrationToGroupV2(
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupChangeMessages: Array<MessageAttributesType> = [];
|
const groupChangeMessages: Array<GroupChangeMessageType> = [];
|
||||||
groupChangeMessages.push({
|
groupChangeMessages.push({
|
||||||
...generateBasicMessage(),
|
...generateBasicMessage(),
|
||||||
type: 'group-v1-migration',
|
type: 'group-v1-migration',
|
||||||
|
@ -2210,7 +2294,7 @@ export async function waitThenRespondToGroupV2Migration(
|
||||||
export function buildMigrationBubble(
|
export function buildMigrationBubble(
|
||||||
previousGroupV1MembersIds: Array<string>,
|
previousGroupV1MembersIds: Array<string>,
|
||||||
newAttributes: ConversationAttributesType
|
newAttributes: ConversationAttributesType
|
||||||
): MessageAttributesType {
|
): GroupChangeMessageType {
|
||||||
const ourUuid = window.storage.user.getCheckedUuid().toString();
|
const ourUuid = window.storage.user.getCheckedUuid().toString();
|
||||||
const ourConversationId =
|
const ourConversationId =
|
||||||
window.ConversationController.getOurConversationId();
|
window.ConversationController.getOurConversationId();
|
||||||
|
@ -2249,7 +2333,7 @@ export function buildMigrationBubble(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getBasicMigrationBubble(): MessageAttributesType {
|
export function getBasicMigrationBubble(): GroupChangeMessageType {
|
||||||
return {
|
return {
|
||||||
...generateBasicMessage(),
|
...generateBasicMessage(),
|
||||||
type: 'group-v1-migration',
|
type: 'group-v1-migration',
|
||||||
|
@ -2322,7 +2406,7 @@ export async function joinGroupV2ViaLinkAndMigrate({
|
||||||
derivedGroupV2Id: undefined,
|
derivedGroupV2Id: undefined,
|
||||||
members: undefined,
|
members: undefined,
|
||||||
};
|
};
|
||||||
const groupChangeMessages: Array<MessageAttributesType> = [
|
const groupChangeMessages: Array<GroupChangeMessageType> = [
|
||||||
{
|
{
|
||||||
...generateBasicMessage(),
|
...generateBasicMessage(),
|
||||||
type: 'group-v1-migration',
|
type: 'group-v1-migration',
|
||||||
|
@ -2536,7 +2620,7 @@ export async function respondToGroupV2Migration({
|
||||||
});
|
});
|
||||||
|
|
||||||
// Generate notifications into the timeline
|
// Generate notifications into the timeline
|
||||||
const groupChangeMessages: Array<MessageAttributesType> = [];
|
const groupChangeMessages: Array<GroupChangeMessageType> = [];
|
||||||
|
|
||||||
groupChangeMessages.push(
|
groupChangeMessages.push(
|
||||||
buildMigrationBubble(previousGroupV1MembersIds, newAttributes)
|
buildMigrationBubble(previousGroupV1MembersIds, newAttributes)
|
||||||
|
@ -2749,6 +2833,7 @@ async function updateGroup(
|
||||||
|
|
||||||
// Save all synthetic messages describing group changes
|
// Save all synthetic messages describing group changes
|
||||||
let syntheticSentAt = initialSentAt - (groupChangeMessages.length + 1);
|
let syntheticSentAt = initialSentAt - (groupChangeMessages.length + 1);
|
||||||
|
const timestamp = Date.now();
|
||||||
const changeMessagesToSave = groupChangeMessages.map(changeMessage => {
|
const changeMessagesToSave = groupChangeMessages.map(changeMessage => {
|
||||||
// We do this to preserve the order of the timeline. We only update sentAt to ensure
|
// We do this to preserve the order of the timeline. We only update sentAt to ensure
|
||||||
// that we don't stomp on messages received around the same time as the message
|
// that we don't stomp on messages received around the same time as the message
|
||||||
|
@ -2761,6 +2846,7 @@ async function updateGroup(
|
||||||
received_at: finalReceivedAt,
|
received_at: finalReceivedAt,
|
||||||
received_at_ms: syntheticSentAt,
|
received_at_ms: syntheticSentAt,
|
||||||
sent_at: syntheticSentAt,
|
sent_at: syntheticSentAt,
|
||||||
|
timestamp,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2801,15 +2887,7 @@ async function updateGroup(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changeMessagesToSave.length > 0) {
|
if (changeMessagesToSave.length > 0) {
|
||||||
await window.Signal.Data.saveMessages(changeMessagesToSave, {
|
await appendChangeMessages(conversation, changeMessagesToSave);
|
||||||
forceSave: true,
|
|
||||||
ourUuid: ourUuid.toString(),
|
|
||||||
});
|
|
||||||
changeMessagesToSave.forEach(changeMessage => {
|
|
||||||
const model = new window.Whisper.Message(changeMessage);
|
|
||||||
window.MessageController.register(model.id, model);
|
|
||||||
conversation.trigger('newmessage', model);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We update group membership last to ensure that all notifications are in place before
|
// We update group membership last to ensure that all notifications are in place before
|
||||||
|
@ -2827,7 +2905,210 @@ async function updateGroup(
|
||||||
conversation.trigger('idUpdated', conversation, 'groupId', previousId);
|
conversation.trigger('idUpdated', conversation, 'groupId', previousId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// No need for convo.updateLastMessage(), 'newmessage' handler does that
|
// Save these most recent updates to conversation
|
||||||
|
await updateConversation(conversation.attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exported for testing
|
||||||
|
export function _mergeGroupChangeMessages(
|
||||||
|
first: MessageAttributesType | undefined,
|
||||||
|
second: MessageAttributesType
|
||||||
|
): MessageAttributesType | undefined {
|
||||||
|
if (!first) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (first.type !== 'group-v2-change' || second.type !== first.type) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { groupV2Change: firstChange } = first;
|
||||||
|
const { groupV2Change: secondChange } = second;
|
||||||
|
if (!firstChange || !secondChange) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstChange.details.length !== 1 && secondChange.details.length !== 1) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [firstDetail] = firstChange.details;
|
||||||
|
const [secondDetail] = secondChange.details;
|
||||||
|
let isApprovalPending: boolean;
|
||||||
|
if (secondDetail.type === 'admin-approval-add-one') {
|
||||||
|
isApprovalPending = true;
|
||||||
|
} else if (secondDetail.type === 'admin-approval-remove-one') {
|
||||||
|
isApprovalPending = false;
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { uuid } = secondDetail;
|
||||||
|
strictAssert(uuid, 'admin approval message should have uuid');
|
||||||
|
|
||||||
|
let updatedDetail;
|
||||||
|
// Member was previously added and is now removed
|
||||||
|
if (
|
||||||
|
!isApprovalPending &&
|
||||||
|
firstDetail.type === 'admin-approval-add-one' &&
|
||||||
|
firstDetail.uuid === uuid
|
||||||
|
) {
|
||||||
|
updatedDetail = {
|
||||||
|
type: 'admin-approval-bounce' as const,
|
||||||
|
uuid,
|
||||||
|
times: 1,
|
||||||
|
isApprovalPending,
|
||||||
|
};
|
||||||
|
|
||||||
|
// There is an existing bounce event - merge this one into it.
|
||||||
|
} else if (
|
||||||
|
firstDetail.type === 'admin-approval-bounce' &&
|
||||||
|
firstDetail.uuid === uuid &&
|
||||||
|
firstDetail.isApprovalPending === !isApprovalPending
|
||||||
|
) {
|
||||||
|
updatedDetail = {
|
||||||
|
type: 'admin-approval-bounce' as const,
|
||||||
|
uuid,
|
||||||
|
times: firstDetail.times + (isApprovalPending ? 0 : 1),
|
||||||
|
isApprovalPending,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...first,
|
||||||
|
groupV2Change: {
|
||||||
|
...first.groupV2Change,
|
||||||
|
details: [updatedDetail],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exported for testing
|
||||||
|
export function _isGroupChangeMessageBounceable(
|
||||||
|
message: MessageAttributesType
|
||||||
|
): boolean {
|
||||||
|
if (message.type !== 'group-v2-change') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { groupV2Change } = message;
|
||||||
|
if (!groupV2Change) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groupV2Change.details.length !== 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [first] = groupV2Change.details;
|
||||||
|
if (
|
||||||
|
first.type === 'admin-approval-add-one' ||
|
||||||
|
first.type === 'admin-approval-bounce'
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function appendChangeMessages(
|
||||||
|
conversation: ConversationModel,
|
||||||
|
messages: ReadonlyArray<MessageAttributesType>
|
||||||
|
): Promise<void> {
|
||||||
|
const logId = conversation.idForLogging();
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
`appendChangeMessages/${logId}: processing ${messages.length} messages`
|
||||||
|
);
|
||||||
|
|
||||||
|
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
|
||||||
|
|
||||||
|
let lastMessage = await dataInterface.getLastConversationMessage({
|
||||||
|
conversationId: conversation.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (lastMessage && !_isGroupChangeMessageBounceable(lastMessage)) {
|
||||||
|
lastMessage = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergedMessages = [];
|
||||||
|
let previousMessage = lastMessage;
|
||||||
|
for (const message of messages) {
|
||||||
|
const merged = _mergeGroupChangeMessages(previousMessage, message);
|
||||||
|
if (!merged) {
|
||||||
|
if (previousMessage && previousMessage !== lastMessage) {
|
||||||
|
mergedMessages.push(previousMessage);
|
||||||
|
}
|
||||||
|
previousMessage = message;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
previousMessage = merged;
|
||||||
|
log.info(
|
||||||
|
`appendChangeMessages/${logId}: merged ${message.id} into ${merged.id}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previousMessage && previousMessage !== lastMessage) {
|
||||||
|
mergedMessages.push(previousMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update existing message
|
||||||
|
if (lastMessage && mergedMessages[0]?.id === lastMessage?.id) {
|
||||||
|
const [first, ...rest] = mergedMessages;
|
||||||
|
strictAssert(first !== undefined, 'First message must be there');
|
||||||
|
|
||||||
|
log.info(`appendChangeMessages/${logId}: updating ${first.id}`);
|
||||||
|
await dataInterface.saveMessage(first, {
|
||||||
|
ourUuid: ourUuid.toString(),
|
||||||
|
|
||||||
|
// We don't use forceSave here because this is an update of existing
|
||||||
|
// message.
|
||||||
|
});
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
`appendChangeMessages/${logId}: saving ${rest.length} new messages`
|
||||||
|
);
|
||||||
|
await dataInterface.saveMessages(rest, {
|
||||||
|
ourUuid: ourUuid.toString(),
|
||||||
|
forceSave: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
log.info(
|
||||||
|
`appendChangeMessages/${logId}: saving ${mergedMessages.length} new messages`
|
||||||
|
);
|
||||||
|
await dataInterface.saveMessages(mergedMessages, {
|
||||||
|
ourUuid: ourUuid.toString(),
|
||||||
|
forceSave: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let newMessages = 0;
|
||||||
|
for (const changeMessage of mergedMessages) {
|
||||||
|
const existing = window.MessageController.getById(changeMessage.id);
|
||||||
|
|
||||||
|
// Update existing message
|
||||||
|
if (existing) {
|
||||||
|
strictAssert(
|
||||||
|
changeMessage.id === lastMessage?.id,
|
||||||
|
'Should only update group change that was already in the database'
|
||||||
|
);
|
||||||
|
existing.set(changeMessage);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const model = new window.Whisper.Message(changeMessage);
|
||||||
|
window.MessageController.register(model.id, model);
|
||||||
|
conversation.trigger('newmessage', model);
|
||||||
|
newMessages += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We updated the message, but didn't add new ones - refresh left pane
|
||||||
|
if (!newMessages && mergedMessages.length > 0) {
|
||||||
|
await conversation.updateLastMessage();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetGroupUpdatesType = Readonly<{
|
type GetGroupUpdatesType = Readonly<{
|
||||||
|
@ -2915,7 +3196,10 @@ async function getGroupUpdates({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isNumber(newRevision) && window.GV2_ENABLE_CHANGE_PROCESSING) {
|
if (
|
||||||
|
(!isFirstFetch || isNumber(newRevision)) &&
|
||||||
|
window.GV2_ENABLE_CHANGE_PROCESSING
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
const result = await updateGroupViaLogs({
|
const result = await updateGroupViaLogs({
|
||||||
group,
|
group,
|
||||||
|
@ -3063,7 +3347,7 @@ async function updateGroupViaLogs({
|
||||||
newRevision,
|
newRevision,
|
||||||
}: {
|
}: {
|
||||||
group: ConversationAttributesType;
|
group: ConversationAttributesType;
|
||||||
newRevision: number;
|
newRevision: number | undefined;
|
||||||
serverPublicParamsBase64: string;
|
serverPublicParamsBase64: string;
|
||||||
}): Promise<UpdatesResultType> {
|
}): Promise<UpdatesResultType> {
|
||||||
const logId = idForLogging(group.groupId);
|
const logId = idForLogging(group.groupId);
|
||||||
|
@ -3081,7 +3365,9 @@ async function updateGroupViaLogs({
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
log.info(
|
log.info(
|
||||||
`updateGroupViaLogs/${logId}: Getting group delta from ${group.revision} to ${newRevision} for group groupv2(${group.groupId})...`
|
`updateGroupViaLogs/${logId}: Getting group delta from ` +
|
||||||
|
`${group.revision ?? '?'} to ${newRevision ?? '?'} for group ` +
|
||||||
|
`groupv2(${group.groupId})...`
|
||||||
);
|
);
|
||||||
const result = await getGroupDelta(deltaOptions);
|
const result = await getGroupDelta(deltaOptions);
|
||||||
|
|
||||||
|
@ -3101,14 +3387,6 @@ async function updateGroupViaLogs({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateBasicMessage() {
|
|
||||||
return {
|
|
||||||
id: getGuid(),
|
|
||||||
schemaVersion: MAX_MESSAGE_SCHEMA,
|
|
||||||
// this is missing most properties to fulfill this type
|
|
||||||
} as MessageAttributesType;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function generateLeftGroupChanges(
|
async function generateLeftGroupChanges(
|
||||||
group: ConversationAttributesType
|
group: ConversationAttributesType
|
||||||
): Promise<UpdatesResultType> {
|
): Promise<UpdatesResultType> {
|
||||||
|
@ -3148,7 +3426,7 @@ async function generateLeftGroupChanges(
|
||||||
const isNewlyRemoved =
|
const isNewlyRemoved =
|
||||||
existingMembers.length > (newAttributes.membersV2 || []).length;
|
existingMembers.length > (newAttributes.membersV2 || []).length;
|
||||||
|
|
||||||
const youWereRemovedMessage: MessageAttributesType = {
|
const youWereRemovedMessage: GroupChangeMessageType = {
|
||||||
...generateBasicMessage(),
|
...generateBasicMessage(),
|
||||||
type: 'group-v2-change',
|
type: 'group-v2-change',
|
||||||
groupV2Change: {
|
groupV2Change: {
|
||||||
|
@ -3202,7 +3480,7 @@ async function getGroupDelta({
|
||||||
authCredentialBase64,
|
authCredentialBase64,
|
||||||
}: {
|
}: {
|
||||||
group: ConversationAttributesType;
|
group: ConversationAttributesType;
|
||||||
newRevision: number;
|
newRevision: number | undefined;
|
||||||
serverPublicParamsBase64: string;
|
serverPublicParamsBase64: string;
|
||||||
authCredentialBase64: string;
|
authCredentialBase64: string;
|
||||||
}): Promise<UpdatesResultType> {
|
}): Promise<UpdatesResultType> {
|
||||||
|
@ -3225,6 +3503,7 @@ async function getGroupDelta({
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentRevision = group.revision;
|
const currentRevision = group.revision;
|
||||||
|
let latestRevision = newRevision;
|
||||||
const isFirstFetch = !isNumber(currentRevision);
|
const isFirstFetch = !isNumber(currentRevision);
|
||||||
let revisionToFetch = isNumber(currentRevision)
|
let revisionToFetch = isNumber(currentRevision)
|
||||||
? currentRevision + 1
|
? currentRevision + 1
|
||||||
|
@ -3247,14 +3526,22 @@ async function getGroupDelta({
|
||||||
if (response.end) {
|
if (response.end) {
|
||||||
revisionToFetch = response.end + 1;
|
revisionToFetch = response.end + 1;
|
||||||
}
|
}
|
||||||
} while (response.end && response.end < newRevision);
|
|
||||||
|
if (latestRevision === undefined) {
|
||||||
|
latestRevision = response.currentRevision ?? response.end;
|
||||||
|
}
|
||||||
|
} while (
|
||||||
|
response.end &&
|
||||||
|
latestRevision !== undefined &&
|
||||||
|
response.end < latestRevision
|
||||||
|
);
|
||||||
|
|
||||||
// Would be nice to cache the unused groupChanges here, to reduce server roundtrips
|
// Would be nice to cache the unused groupChanges here, to reduce server roundtrips
|
||||||
|
|
||||||
return integrateGroupChanges({
|
return integrateGroupChanges({
|
||||||
changes,
|
changes,
|
||||||
group,
|
group,
|
||||||
newRevision,
|
newRevision: latestRevision,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3264,12 +3551,12 @@ async function integrateGroupChanges({
|
||||||
changes,
|
changes,
|
||||||
}: {
|
}: {
|
||||||
group: ConversationAttributesType;
|
group: ConversationAttributesType;
|
||||||
newRevision: number;
|
newRevision: number | undefined;
|
||||||
changes: Array<Proto.IGroupChanges>;
|
changes: Array<Proto.IGroupChanges>;
|
||||||
}): Promise<UpdatesResultType> {
|
}): Promise<UpdatesResultType> {
|
||||||
const logId = idForLogging(group.groupId);
|
const logId = idForLogging(group.groupId);
|
||||||
let attributes = group;
|
let attributes = group;
|
||||||
const finalMessages: Array<Array<MessageAttributesType>> = [];
|
const finalMessages: Array<Array<GroupChangeMessageType>> = [];
|
||||||
const finalMembers: Array<Array<MemberType>> = [];
|
const finalMembers: Array<Array<MemberType>> = [];
|
||||||
|
|
||||||
const imax = changes.length;
|
const imax = changes.length;
|
||||||
|
@ -3361,7 +3648,7 @@ async function integrateGroupChange({
|
||||||
group: ConversationAttributesType;
|
group: ConversationAttributesType;
|
||||||
groupChange?: Proto.IGroupChange;
|
groupChange?: Proto.IGroupChange;
|
||||||
groupState?: Proto.IGroup;
|
groupState?: Proto.IGroup;
|
||||||
newRevision: number;
|
newRevision: number | undefined;
|
||||||
}): Promise<UpdatesResultType> {
|
}): Promise<UpdatesResultType> {
|
||||||
const logId = idForLogging(group.groupId);
|
const logId = idForLogging(group.groupId);
|
||||||
if (!group.secretParams) {
|
if (!group.secretParams) {
|
||||||
|
@ -3396,6 +3683,7 @@ async function integrateGroupChange({
|
||||||
|
|
||||||
if (
|
if (
|
||||||
groupChangeActions.version &&
|
groupChangeActions.version &&
|
||||||
|
newRevision !== undefined &&
|
||||||
groupChangeActions.version > newRevision
|
groupChangeActions.version > newRevision
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
|
@ -3571,7 +3859,7 @@ function extractDiffs({
|
||||||
dropInitialJoinMessage?: boolean;
|
dropInitialJoinMessage?: boolean;
|
||||||
old: ConversationAttributesType;
|
old: ConversationAttributesType;
|
||||||
sourceUuid?: UUIDStringType;
|
sourceUuid?: UUIDStringType;
|
||||||
}): Array<MessageAttributesType> {
|
}): Array<GroupChangeMessageType> {
|
||||||
const logId = idForLogging(old.groupId);
|
const logId = idForLogging(old.groupId);
|
||||||
const details: Array<GroupV2ChangeDetailType> = [];
|
const details: Array<GroupV2ChangeDetailType> = [];
|
||||||
const ourUuid = window.storage.user.getCheckedUuid().toString();
|
const ourUuid = window.storage.user.getCheckedUuid().toString();
|
||||||
|
@ -3870,8 +4158,8 @@ function extractDiffs({
|
||||||
|
|
||||||
// final processing
|
// final processing
|
||||||
|
|
||||||
let message: MessageAttributesType | undefined;
|
let message: GroupChangeMessageType | undefined;
|
||||||
let timerNotification: MessageAttributesType | undefined;
|
let timerNotification: GroupChangeMessageType | undefined;
|
||||||
|
|
||||||
const firstUpdate = !isNumber(old.revision);
|
const firstUpdate = !isNumber(old.revision);
|
||||||
|
|
||||||
|
|
|
@ -421,7 +421,21 @@ export class ConversationModel extends window.Backbone
|
||||||
}
|
}
|
||||||
|
|
||||||
const uuid = UUID.checkedLookup(id).toString();
|
const uuid = UUID.checkedLookup(id).toString();
|
||||||
return window._.any(pendingMembersV2, item => item.uuid === uuid);
|
return pendingMembersV2.some(item => item.uuid === uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
isMemberBanned(id: string): boolean {
|
||||||
|
if (!isGroupV2(this.attributes)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const bannedMembersV2 = this.get('bannedMembersV2');
|
||||||
|
|
||||||
|
if (!bannedMembersV2 || !bannedMembersV2.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uuid = UUID.checkedLookup(id).toString();
|
||||||
|
return bannedMembersV2.some(item => item === uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
isMemberAwaitingApproval(id: string): boolean {
|
isMemberAwaitingApproval(id: string): boolean {
|
||||||
|
@ -1865,6 +1879,7 @@ export class ConversationModel extends window.Backbone
|
||||||
messageCount: this.get('messageCount') || 0,
|
messageCount: this.get('messageCount') || 0,
|
||||||
pendingMemberships: this.getPendingMemberships(),
|
pendingMemberships: this.getPendingMemberships(),
|
||||||
pendingApprovalMemberships: this.getPendingApprovalMemberships(),
|
pendingApprovalMemberships: this.getPendingApprovalMemberships(),
|
||||||
|
bannedMemberships: this.getBannedMemberships(),
|
||||||
profileKey: this.get('profileKey'),
|
profileKey: this.get('profileKey'),
|
||||||
messageRequestsEnabled,
|
messageRequestsEnabled,
|
||||||
accessControlAddFromInviteLink:
|
accessControlAddFromInviteLink:
|
||||||
|
@ -2337,6 +2352,40 @@ export class ConversationModel extends window.Backbone
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async addBannedMember(
|
||||||
|
uuid: UUIDStringType
|
||||||
|
): Promise<Proto.GroupChange.Actions | undefined> {
|
||||||
|
if (this.isMember(uuid)) {
|
||||||
|
log.warn('addBannedMember: Member is a part of the group!');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isMemberPending(uuid)) {
|
||||||
|
log.warn('addBannedMember: Member is pending to be added to group!');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isMemberBanned(uuid)) {
|
||||||
|
log.warn('addBannedMember: Member is already banned!');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return window.Signal.Groups.buildAddBannedMemberChange({
|
||||||
|
group: this.attributes,
|
||||||
|
uuid,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async blockGroupLinkRequests(uuid: UUIDStringType): Promise<void> {
|
||||||
|
await this.modifyGroupV2({
|
||||||
|
name: 'addBannedMember',
|
||||||
|
createGroupChange: async () => this.addBannedMember(uuid),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async toggleAdmin(conversationId: string): Promise<void> {
|
async toggleAdmin(conversationId: string): Promise<void> {
|
||||||
if (!isGroupV2(this.attributes)) {
|
if (!isGroupV2(this.attributes)) {
|
||||||
return;
|
return;
|
||||||
|
@ -3495,6 +3544,14 @@ export class ConversationModel extends window.Backbone
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getBannedMemberships(): Array<UUIDStringType> {
|
||||||
|
if (!isGroupV2(this.attributes)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.get('bannedMembersV2') || [];
|
||||||
|
}
|
||||||
|
|
||||||
getMembers(
|
getMembers(
|
||||||
options: { includePendingMembers?: boolean } = {}
|
options: { includePendingMembers?: boolean } = {}
|
||||||
): Array<ConversationModel> {
|
): Array<ConversationModel> {
|
||||||
|
@ -4069,17 +4126,17 @@ export class ConversationModel extends window.Backbone
|
||||||
const conversationId = this.id;
|
const conversationId = this.id;
|
||||||
|
|
||||||
const ourUuid = window.textsecure.storage.user.getCheckedUuid().toString();
|
const ourUuid = window.textsecure.storage.user.getCheckedUuid().toString();
|
||||||
const lastMessages = await window.Signal.Data.getLastConversationMessages({
|
const stats = await window.Signal.Data.getConversationMessageStats({
|
||||||
conversationId,
|
conversationId,
|
||||||
ourUuid,
|
ourUuid,
|
||||||
});
|
});
|
||||||
|
|
||||||
// This runs as a job to avoid race conditions
|
// This runs as a job to avoid race conditions
|
||||||
this.queueJob('maybeSetPendingUniversalTimer', async () =>
|
this.queueJob('maybeSetPendingUniversalTimer', async () =>
|
||||||
this.maybeSetPendingUniversalTimer(lastMessages.hasUserInitiatedMessages)
|
this.maybeSetPendingUniversalTimer(stats.hasUserInitiatedMessages)
|
||||||
);
|
);
|
||||||
|
|
||||||
const { preview, activity } = lastMessages;
|
const { preview, activity } = stats;
|
||||||
let previewMessage: MessageModel | undefined;
|
let previewMessage: MessageModel | undefined;
|
||||||
let activityMessage: MessageModel | undefined;
|
let activityMessage: MessageModel | undefined;
|
||||||
|
|
||||||
|
|
|
@ -51,6 +51,7 @@ import { isImage, isVideo } from '../types/Attachment';
|
||||||
import * as Attachment from '../types/Attachment';
|
import * as Attachment from '../types/Attachment';
|
||||||
import { stringToMIMEType } from '../types/MIME';
|
import { stringToMIMEType } from '../types/MIME';
|
||||||
import * as MIME from '../types/MIME';
|
import * as MIME from '../types/MIME';
|
||||||
|
import * as GroupChange from '../groupChange';
|
||||||
import { ReadStatus } from '../messages/MessageReadStatus';
|
import { ReadStatus } from '../messages/MessageReadStatus';
|
||||||
import type { SendStateByConversationId } from '../messages/MessageSendState';
|
import type { SendStateByConversationId } from '../messages/MessageSendState';
|
||||||
import {
|
import {
|
||||||
|
@ -486,7 +487,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
'getNotificationData: isGroupV2Change true, but no groupV2Change!'
|
'getNotificationData: isGroupV2Change true, but no groupV2Change!'
|
||||||
);
|
);
|
||||||
|
|
||||||
const lines = window.Signal.GroupChange.renderChange<string>(change, {
|
const changes = GroupChange.renderChange<string>(change, {
|
||||||
i18n: window.i18n,
|
i18n: window.i18n,
|
||||||
ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(),
|
ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(),
|
||||||
renderContact: (conversationId: string) => {
|
renderContact: (conversationId: string) => {
|
||||||
|
@ -503,7 +504,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
) => window.i18n(key, components),
|
) => window.i18n(key, components),
|
||||||
});
|
});
|
||||||
|
|
||||||
return { text: lines.join(' ') };
|
return { text: changes.map(({ text }) => text).join(' ') };
|
||||||
}
|
}
|
||||||
|
|
||||||
const attachments = this.get('attachments') || [];
|
const attachments = this.get('attachments') || [];
|
||||||
|
|
|
@ -56,7 +56,7 @@ import type {
|
||||||
IdentityKeyType,
|
IdentityKeyType,
|
||||||
ItemKeyType,
|
ItemKeyType,
|
||||||
ItemType,
|
ItemType,
|
||||||
LastConversationMessagesType,
|
ConversationMessageStatsType,
|
||||||
MessageType,
|
MessageType,
|
||||||
MessageTypeUnhydrated,
|
MessageTypeUnhydrated,
|
||||||
PreKeyIdType,
|
PreKeyIdType,
|
||||||
|
@ -241,7 +241,8 @@ const dataInterface: ClientInterface = {
|
||||||
getNewerMessagesByConversation,
|
getNewerMessagesByConversation,
|
||||||
getMessageMetricsForConversation,
|
getMessageMetricsForConversation,
|
||||||
getConversationRangeCenteredOnMessage,
|
getConversationRangeCenteredOnMessage,
|
||||||
getLastConversationMessages,
|
getConversationMessageStats,
|
||||||
|
getLastConversationMessage,
|
||||||
hasGroupCallHistoryMessage,
|
hasGroupCallHistoryMessage,
|
||||||
migrateConversationMessages,
|
migrateConversationMessages,
|
||||||
|
|
||||||
|
@ -1097,7 +1098,7 @@ async function saveMessage(
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveMessages(
|
async function saveMessages(
|
||||||
arrayOfMessages: Array<MessageType>,
|
arrayOfMessages: ReadonlyArray<MessageType>,
|
||||||
options: { forceSave?: boolean; ourUuid: UUIDStringType }
|
options: { forceSave?: boolean; ourUuid: UUIDStringType }
|
||||||
) {
|
) {
|
||||||
await channels.saveMessages(
|
await channels.saveMessages(
|
||||||
|
@ -1291,15 +1292,15 @@ async function getNewerMessagesByConversation(
|
||||||
|
|
||||||
return handleMessageJSON(messages);
|
return handleMessageJSON(messages);
|
||||||
}
|
}
|
||||||
async function getLastConversationMessages({
|
async function getConversationMessageStats({
|
||||||
conversationId,
|
conversationId,
|
||||||
ourUuid,
|
ourUuid,
|
||||||
}: {
|
}: {
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
ourUuid: UUIDStringType;
|
ourUuid: UUIDStringType;
|
||||||
}): Promise<LastConversationMessagesType> {
|
}): Promise<ConversationMessageStatsType> {
|
||||||
const { preview, activity, hasUserInitiatedMessages } =
|
const { preview, activity, hasUserInitiatedMessages } =
|
||||||
await channels.getLastConversationMessages({
|
await channels.getConversationMessageStats({
|
||||||
conversationId,
|
conversationId,
|
||||||
ourUuid,
|
ourUuid,
|
||||||
});
|
});
|
||||||
|
@ -1310,6 +1311,13 @@ async function getLastConversationMessages({
|
||||||
hasUserInitiatedMessages,
|
hasUserInitiatedMessages,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
async function getLastConversationMessage({
|
||||||
|
conversationId,
|
||||||
|
}: {
|
||||||
|
conversationId: string;
|
||||||
|
}) {
|
||||||
|
return channels.getLastConversationMessage({ conversationId });
|
||||||
|
}
|
||||||
async function getMessageMetricsForConversation(
|
async function getMessageMetricsForConversation(
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
storyId?: UUIDStringType
|
storyId?: UUIDStringType
|
||||||
|
|
|
@ -218,7 +218,7 @@ export type UnprocessedUpdateType = {
|
||||||
decrypted?: string;
|
decrypted?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LastConversationMessagesType = {
|
export type ConversationMessageStatsType = {
|
||||||
activity?: MessageType;
|
activity?: MessageType;
|
||||||
preview?: MessageType;
|
preview?: MessageType;
|
||||||
hasUserInitiatedMessages: boolean;
|
hasUserInitiatedMessages: boolean;
|
||||||
|
@ -379,7 +379,7 @@ export type DataInterface = {
|
||||||
}
|
}
|
||||||
) => Promise<string>;
|
) => Promise<string>;
|
||||||
saveMessages: (
|
saveMessages: (
|
||||||
arrayOfMessages: Array<MessageType>,
|
arrayOfMessages: ReadonlyArray<MessageType>,
|
||||||
options: { forceSave?: boolean; ourUuid: UUIDStringType }
|
options: { forceSave?: boolean; ourUuid: UUIDStringType }
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
removeMessage: (id: string) => Promise<void>;
|
removeMessage: (id: string) => Promise<void>;
|
||||||
|
@ -453,10 +453,13 @@ export type DataInterface = {
|
||||||
storyId?: UUIDStringType
|
storyId?: UUIDStringType
|
||||||
) => Promise<ConversationMetricsType>;
|
) => Promise<ConversationMetricsType>;
|
||||||
// getConversationRangeCenteredOnMessage is JSON on server, full message on client
|
// getConversationRangeCenteredOnMessage is JSON on server, full message on client
|
||||||
getLastConversationMessages: (options: {
|
getConversationMessageStats: (options: {
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
ourUuid: UUIDStringType;
|
ourUuid: UUIDStringType;
|
||||||
}) => Promise<LastConversationMessagesType>;
|
}) => Promise<ConversationMessageStatsType>;
|
||||||
|
getLastConversationMessage(options: {
|
||||||
|
conversationId: string;
|
||||||
|
}): Promise<MessageType | undefined>;
|
||||||
hasGroupCallHistoryMessage: (
|
hasGroupCallHistoryMessage: (
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
eraId: string
|
eraId: string
|
||||||
|
|
|
@ -80,7 +80,7 @@ import type {
|
||||||
IdentityKeyType,
|
IdentityKeyType,
|
||||||
ItemKeyType,
|
ItemKeyType,
|
||||||
ItemType,
|
ItemType,
|
||||||
LastConversationMessagesType,
|
ConversationMessageStatsType,
|
||||||
MessageMetricsType,
|
MessageMetricsType,
|
||||||
MessageType,
|
MessageType,
|
||||||
MessageTypeUnhydrated,
|
MessageTypeUnhydrated,
|
||||||
|
@ -237,7 +237,8 @@ const dataInterface: ServerInterface = {
|
||||||
getTotalUnreadForConversation,
|
getTotalUnreadForConversation,
|
||||||
getMessageMetricsForConversation,
|
getMessageMetricsForConversation,
|
||||||
getConversationRangeCenteredOnMessage,
|
getConversationRangeCenteredOnMessage,
|
||||||
getLastConversationMessages,
|
getConversationMessageStats,
|
||||||
|
getLastConversationMessage,
|
||||||
hasGroupCallHistoryMessage,
|
hasGroupCallHistoryMessage,
|
||||||
migrateConversationMessages,
|
migrateConversationMessages,
|
||||||
|
|
||||||
|
@ -1912,7 +1913,7 @@ async function saveMessage(
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveMessages(
|
async function saveMessages(
|
||||||
arrayOfMessages: Array<MessageType>,
|
arrayOfMessages: ReadonlyArray<MessageType>,
|
||||||
options: { forceSave?: boolean; ourUuid: UUIDStringType }
|
options: { forceSave?: boolean; ourUuid: UUIDStringType }
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const db = getInstance();
|
const db = getInstance();
|
||||||
|
@ -2591,13 +2592,13 @@ function getLastConversationPreview({
|
||||||
return jsonToObject(row.json);
|
return jsonToObject(row.json);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getLastConversationMessages({
|
async function getConversationMessageStats({
|
||||||
conversationId,
|
conversationId,
|
||||||
ourUuid,
|
ourUuid,
|
||||||
}: {
|
}: {
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
ourUuid: UUIDStringType;
|
ourUuid: UUIDStringType;
|
||||||
}): Promise<LastConversationMessagesType> {
|
}): Promise<ConversationMessageStatsType> {
|
||||||
const db = getInstance();
|
const db = getInstance();
|
||||||
|
|
||||||
return db.transaction(() => {
|
return db.transaction(() => {
|
||||||
|
@ -2612,6 +2613,32 @@ async function getLastConversationMessages({
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getLastConversationMessage({
|
||||||
|
conversationId,
|
||||||
|
}: {
|
||||||
|
conversationId: string;
|
||||||
|
}): Promise<MessageType | undefined> {
|
||||||
|
const db = getInstance();
|
||||||
|
const row = db
|
||||||
|
.prepare<Query>(
|
||||||
|
`
|
||||||
|
SELECT * FROM messages WHERE
|
||||||
|
conversationId = $conversationId
|
||||||
|
ORDER BY received_at DESC, sent_at DESC
|
||||||
|
LIMIT 1;
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.get({
|
||||||
|
conversationId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!row) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonToObject(row.json);
|
||||||
|
}
|
||||||
|
|
||||||
function getOldestUnreadMessageForConversation(
|
function getOldestUnreadMessageForConversation(
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
storyId?: UUIDStringType
|
storyId?: UUIDStringType
|
||||||
|
|
|
@ -171,6 +171,7 @@ export type ConversationType = {
|
||||||
pendingApprovalMemberships?: Array<{
|
pendingApprovalMemberships?: Array<{
|
||||||
uuid: UUIDStringType;
|
uuid: UUIDStringType;
|
||||||
}>;
|
}>;
|
||||||
|
bannedMemberships?: Array<UUIDStringType>;
|
||||||
muteExpiresAt?: number;
|
muteExpiresAt?: number;
|
||||||
dontNotifyForMentionsIfMuted?: boolean;
|
dontNotifyForMentionsIfMuted?: boolean;
|
||||||
type: ConversationTypeType;
|
type: ConversationTypeType;
|
||||||
|
|
|
@ -858,7 +858,10 @@ function getPropsForGroupV2Change(
|
||||||
const conversation = getConversation(message, conversationSelector);
|
const conversation = getConversation(message, conversationSelector);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
areWeAdmin: Boolean(conversation.areWeAdmin),
|
||||||
groupName: conversation?.type === 'group' ? conversation?.name : undefined,
|
groupName: conversation?.type === 'group' ? conversation?.name : undefined,
|
||||||
|
groupMemberships: conversation.memberships,
|
||||||
|
groupBannedMemberships: conversation.bannedMemberships,
|
||||||
ourUuid,
|
ourUuid,
|
||||||
change,
|
change,
|
||||||
};
|
};
|
||||||
|
|
|
@ -61,6 +61,7 @@ export type TimelinePropsType = ExternalProps &
|
||||||
ComponentPropsType,
|
ComponentPropsType,
|
||||||
| 'acknowledgeGroupMemberNameCollisions'
|
| 'acknowledgeGroupMemberNameCollisions'
|
||||||
| 'contactSupport'
|
| 'contactSupport'
|
||||||
|
| 'blockGroupLinkRequests'
|
||||||
| 'deleteMessage'
|
| 'deleteMessage'
|
||||||
| 'deleteMessageForEveryone'
|
| 'deleteMessageForEveryone'
|
||||||
| 'displayTapToViewMessage'
|
| 'displayTapToViewMessage'
|
||||||
|
|
217
ts/test-both/groups/message_merge_test.ts
Normal file
217
ts/test-both/groups/message_merge_test.ts
Normal file
|
@ -0,0 +1,217 @@
|
||||||
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { assert } from 'chai';
|
||||||
|
|
||||||
|
import { UUID } from '../../types/UUID';
|
||||||
|
import {
|
||||||
|
_isGroupChangeMessageBounceable,
|
||||||
|
_mergeGroupChangeMessages,
|
||||||
|
} from '../../groups';
|
||||||
|
|
||||||
|
describe('group message merging', () => {
|
||||||
|
const defaultMessage = {
|
||||||
|
id: UUID.generate().toString(),
|
||||||
|
conversationId: UUID.generate().toString(),
|
||||||
|
timestamp: Date.now(),
|
||||||
|
sent_at: Date.now(),
|
||||||
|
received_at: Date.now(),
|
||||||
|
};
|
||||||
|
const uuid = UUID.generate().toString();
|
||||||
|
|
||||||
|
describe('_isGroupChangeMessageBounceable', () => {
|
||||||
|
it('should return true for admin approval add', () => {
|
||||||
|
assert.isTrue(
|
||||||
|
_isGroupChangeMessageBounceable({
|
||||||
|
...defaultMessage,
|
||||||
|
type: 'group-v2-change',
|
||||||
|
groupV2Change: {
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'admin-approval-add-one',
|
||||||
|
uuid,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for bounce message', () => {
|
||||||
|
assert.isTrue(
|
||||||
|
_isGroupChangeMessageBounceable({
|
||||||
|
...defaultMessage,
|
||||||
|
type: 'group-v2-change',
|
||||||
|
groupV2Change: {
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'admin-approval-bounce',
|
||||||
|
times: 1,
|
||||||
|
isApprovalPending: true,
|
||||||
|
uuid,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false otherwise', () => {
|
||||||
|
assert.isFalse(
|
||||||
|
_isGroupChangeMessageBounceable({
|
||||||
|
...defaultMessage,
|
||||||
|
type: 'group-v2-change',
|
||||||
|
groupV2Change: {
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'admin-approval-remove-one',
|
||||||
|
uuid,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('_mergeGroupChangeMessages', () => {
|
||||||
|
const add = {
|
||||||
|
...defaultMessage,
|
||||||
|
type: 'group-v2-change' as const,
|
||||||
|
groupV2Change: {
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'admin-approval-add-one' as const,
|
||||||
|
uuid,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const remove = {
|
||||||
|
...defaultMessage,
|
||||||
|
type: 'group-v2-change' as const,
|
||||||
|
groupV2Change: {
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'admin-approval-remove-one' as const,
|
||||||
|
uuid,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const addOther = {
|
||||||
|
...defaultMessage,
|
||||||
|
type: 'group-v2-change' as const,
|
||||||
|
groupV2Change: {
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'admin-approval-add-one' as const,
|
||||||
|
uuid: UUID.generate().toString(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const removeOther = {
|
||||||
|
...defaultMessage,
|
||||||
|
type: 'group-v2-change' as const,
|
||||||
|
groupV2Change: {
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'admin-approval-remove-one' as const,
|
||||||
|
uuid: UUID.generate().toString(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const bounce = {
|
||||||
|
...defaultMessage,
|
||||||
|
type: 'group-v2-change' as const,
|
||||||
|
groupV2Change: {
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'admin-approval-bounce' as const,
|
||||||
|
times: 1,
|
||||||
|
isApprovalPending: false,
|
||||||
|
uuid,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const bounceAndAdd = {
|
||||||
|
...defaultMessage,
|
||||||
|
type: 'group-v2-change' as const,
|
||||||
|
groupV2Change: {
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'admin-approval-bounce' as const,
|
||||||
|
times: 1,
|
||||||
|
isApprovalPending: true,
|
||||||
|
uuid,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should merge add with remove if uuid matches', () => {
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
_mergeGroupChangeMessages(add, remove)?.groupV2Change?.details,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
isApprovalPending: false,
|
||||||
|
times: 1,
|
||||||
|
type: 'admin-approval-bounce',
|
||||||
|
uuid,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not merge add with remove if uuid does not match', () => {
|
||||||
|
assert.isUndefined(_mergeGroupChangeMessages(add, removeOther));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should merge bounce with add if uuid matches', () => {
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
_mergeGroupChangeMessages(bounce, add)?.groupV2Change?.details,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
isApprovalPending: true,
|
||||||
|
times: 1,
|
||||||
|
type: 'admin-approval-bounce',
|
||||||
|
uuid,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should merge bounce and add with remove if uuid matches', () => {
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
_mergeGroupChangeMessages(bounceAndAdd, remove)?.groupV2Change?.details,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
isApprovalPending: false,
|
||||||
|
times: 2,
|
||||||
|
type: 'admin-approval-bounce',
|
||||||
|
uuid,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not merge bounce with add if uuid does not match', () => {
|
||||||
|
assert.isUndefined(_mergeGroupChangeMessages(bounce, addOther));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not merge bounce and add with add', () => {
|
||||||
|
assert.isUndefined(_mergeGroupChangeMessages(bounceAndAdd, add));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not merge bounce and add with remove if uuid does not match', () => {
|
||||||
|
assert.isUndefined(_mergeGroupChangeMessages(bounceAndAdd, removeOther));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not merge bounce with remove', () => {
|
||||||
|
assert.isUndefined(_mergeGroupChangeMessages(bounce, remove));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -13,7 +13,7 @@ const {
|
||||||
removeAll,
|
removeAll,
|
||||||
_getAllMessages,
|
_getAllMessages,
|
||||||
saveMessages,
|
saveMessages,
|
||||||
getLastConversationMessages,
|
getConversationMessageStats,
|
||||||
} = dataInterface;
|
} = dataInterface;
|
||||||
|
|
||||||
function getUuid(): UUIDStringType {
|
function getUuid(): UUIDStringType {
|
||||||
|
@ -25,7 +25,7 @@ describe('sql/conversationSummary', () => {
|
||||||
await removeAll();
|
await removeAll();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getLastConversationMessages', () => {
|
describe('getConversationMessageStats', () => {
|
||||||
it('returns the latest message in current conversation', async () => {
|
it('returns the latest message in current conversation', async () => {
|
||||||
assert.lengthOf(await _getAllMessages(), 0);
|
assert.lengthOf(await _getAllMessages(), 0);
|
||||||
|
|
||||||
|
@ -67,7 +67,7 @@ describe('sql/conversationSummary', () => {
|
||||||
|
|
||||||
assert.lengthOf(await _getAllMessages(), 3);
|
assert.lengthOf(await _getAllMessages(), 3);
|
||||||
|
|
||||||
const messages = await getLastConversationMessages({
|
const messages = await getConversationMessageStats({
|
||||||
conversationId,
|
conversationId,
|
||||||
ourUuid,
|
ourUuid,
|
||||||
});
|
});
|
||||||
|
@ -176,7 +176,7 @@ describe('sql/conversationSummary', () => {
|
||||||
|
|
||||||
assert.lengthOf(await _getAllMessages(), 8);
|
assert.lengthOf(await _getAllMessages(), 8);
|
||||||
|
|
||||||
const messages = await getLastConversationMessages({
|
const messages = await getConversationMessageStats({
|
||||||
conversationId,
|
conversationId,
|
||||||
ourUuid,
|
ourUuid,
|
||||||
});
|
});
|
||||||
|
@ -293,7 +293,7 @@ describe('sql/conversationSummary', () => {
|
||||||
|
|
||||||
assert.lengthOf(await _getAllMessages(), 9);
|
assert.lengthOf(await _getAllMessages(), 9);
|
||||||
|
|
||||||
const messages = await getLastConversationMessages({
|
const messages = await getConversationMessageStats({
|
||||||
conversationId,
|
conversationId,
|
||||||
ourUuid,
|
ourUuid,
|
||||||
});
|
});
|
||||||
|
@ -341,7 +341,7 @@ describe('sql/conversationSummary', () => {
|
||||||
|
|
||||||
assert.lengthOf(await _getAllMessages(), 2);
|
assert.lengthOf(await _getAllMessages(), 2);
|
||||||
|
|
||||||
const messages = await getLastConversationMessages({
|
const messages = await getConversationMessageStats({
|
||||||
conversationId,
|
conversationId,
|
||||||
ourUuid,
|
ourUuid,
|
||||||
});
|
});
|
||||||
|
@ -390,7 +390,7 @@ describe('sql/conversationSummary', () => {
|
||||||
|
|
||||||
assert.lengthOf(await _getAllMessages(), 2);
|
assert.lengthOf(await _getAllMessages(), 2);
|
||||||
|
|
||||||
const messages = await getLastConversationMessages({
|
const messages = await getConversationMessageStats({
|
||||||
conversationId,
|
conversationId,
|
||||||
ourUuid,
|
ourUuid,
|
||||||
});
|
});
|
||||||
|
@ -432,7 +432,7 @@ describe('sql/conversationSummary', () => {
|
||||||
|
|
||||||
assert.lengthOf(await _getAllMessages(), 2);
|
assert.lengthOf(await _getAllMessages(), 2);
|
||||||
|
|
||||||
const messages = await getLastConversationMessages({
|
const messages = await getConversationMessageStats({
|
||||||
conversationId,
|
conversationId,
|
||||||
ourUuid,
|
ourUuid,
|
||||||
});
|
});
|
||||||
|
@ -476,7 +476,7 @@ describe('sql/conversationSummary', () => {
|
||||||
|
|
||||||
assert.lengthOf(await _getAllMessages(), 2);
|
assert.lengthOf(await _getAllMessages(), 2);
|
||||||
|
|
||||||
const messages = await getLastConversationMessages({
|
const messages = await getConversationMessageStats({
|
||||||
conversationId,
|
conversationId,
|
||||||
ourUuid,
|
ourUuid,
|
||||||
});
|
});
|
||||||
|
@ -535,7 +535,7 @@ describe('sql/conversationSummary', () => {
|
||||||
|
|
||||||
assert.lengthOf(await _getAllMessages(), 2);
|
assert.lengthOf(await _getAllMessages(), 2);
|
||||||
|
|
||||||
const messages = await getLastConversationMessages({
|
const messages = await getConversationMessageStats({
|
||||||
conversationId,
|
conversationId,
|
||||||
ourUuid,
|
ourUuid,
|
||||||
});
|
});
|
||||||
|
|
|
@ -114,6 +114,7 @@ import { viewSyncJobQueue } from '../jobs/viewSyncJobQueue';
|
||||||
import { viewedReceiptsJobQueue } from '../jobs/viewedReceiptsJobQueue';
|
import { viewedReceiptsJobQueue } from '../jobs/viewedReceiptsJobQueue';
|
||||||
import { RecordingState } from '../state/ducks/audioRecorder';
|
import { RecordingState } from '../state/ducks/audioRecorder';
|
||||||
import { UUIDKind } from '../types/UUID';
|
import { UUIDKind } from '../types/UUID';
|
||||||
|
import type { UUIDStringType } from '../types/UUID';
|
||||||
import { retryDeleteForEveryone } from '../util/retryDeleteForEveryone';
|
import { retryDeleteForEveryone } from '../util/retryDeleteForEveryone';
|
||||||
|
|
||||||
type AttachmentOptions = {
|
type AttachmentOptions = {
|
||||||
|
@ -513,6 +514,9 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
): void => {
|
): void => {
|
||||||
this.model.acknowledgeGroupMemberNameCollisions(groupNameCollisions);
|
this.model.acknowledgeGroupMemberNameCollisions(groupNameCollisions);
|
||||||
},
|
},
|
||||||
|
blockGroupLinkRequests: (uuid: UUIDStringType) => {
|
||||||
|
this.model.blockGroupLinkRequests(uuid);
|
||||||
|
},
|
||||||
contactSupport,
|
contactSupport,
|
||||||
learnMoreAboutDeliveryIssue,
|
learnMoreAboutDeliveryIssue,
|
||||||
loadNewerMessages: this.model.loadNewerMessages.bind(this.model),
|
loadNewerMessages: this.model.loadNewerMessages.bind(this.model),
|
||||||
|
|
2
ts/window.d.ts
vendored
2
ts/window.d.ts
vendored
|
@ -112,7 +112,6 @@ import { IPCEventsType, IPCEventsValuesType } from './util/createIPCEvents';
|
||||||
import { ConversationView } from './views/conversation_view';
|
import { ConversationView } from './views/conversation_view';
|
||||||
import type { SignalContextType } from './windows/context';
|
import type { SignalContextType } from './windows/context';
|
||||||
import { GroupV2Change } from './components/conversation/GroupV2Change';
|
import { GroupV2Change } from './components/conversation/GroupV2Change';
|
||||||
import * as GroupChange from './groupChange';
|
|
||||||
|
|
||||||
export { Long } from 'long';
|
export { Long } from 'long';
|
||||||
|
|
||||||
|
@ -389,7 +388,6 @@ declare global {
|
||||||
QualifiedAddress: typeof QualifiedAddress;
|
QualifiedAddress: typeof QualifiedAddress;
|
||||||
};
|
};
|
||||||
Util: typeof Util;
|
Util: typeof Util;
|
||||||
GroupChange: typeof GroupChange;
|
|
||||||
Components: {
|
Components: {
|
||||||
AttachmentList: typeof AttachmentList;
|
AttachmentList: typeof AttachmentList;
|
||||||
ChatColorPicker: typeof ChatColorPicker;
|
ChatColorPicker: typeof ChatColorPicker;
|
||||||
|
|
Loading…
Add table
Reference in a new issue