Support for people banned from joining groups via link
This commit is contained in:
parent
1b7496399b
commit
f217730b84
17 changed files with 455 additions and 108 deletions
|
@ -4059,6 +4059,14 @@
|
||||||
"message": "This group link is no longer valid.",
|
"message": "This group link is no longer valid.",
|
||||||
"description": "Shown if you click a group link and we can't get information about it"
|
"description": "Shown if you click a group link and we can't get information about it"
|
||||||
},
|
},
|
||||||
|
"GroupV2--join--link-forbidden--title": {
|
||||||
|
"message": "Can’t Join Group",
|
||||||
|
"description": "Shown if you click a group link and you have been forbidden from joining via the link"
|
||||||
|
},
|
||||||
|
"GroupV2--join--link-forbidden": {
|
||||||
|
"message": "You can't join this group via the group link because an admin removed you.",
|
||||||
|
"description": "Shown if you click a group link and you have been forbidden from joining via the link"
|
||||||
|
},
|
||||||
"GroupV2--join--prompt-with-approval": {
|
"GroupV2--join--prompt-with-approval": {
|
||||||
"message": "An admin of this group must approve your request before you can join this group. If approved, your name and photo will be shared with its members.",
|
"message": "An admin of this group must approve your request before you can join this group. If approved, your name and photo will be shared with its members.",
|
||||||
"description": "Shown when you click on a group link to confirm, if it requires admin approval"
|
"description": "Shown when you click on a group link to confirm, if it requires admin approval"
|
||||||
|
@ -5688,6 +5696,16 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"PendingRequests--deny-for--with-link": {
|
||||||
|
"message": "Deny request from \"$name$\"? They will not be able to request to join via the group link again.",
|
||||||
|
"description": "This is the modal content when confirming denying a group request to join",
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Meowsy Purrington"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"PendingInvites--invites": {
|
"PendingInvites--invites": {
|
||||||
"message": "Invited by you",
|
"message": "Invited by you",
|
||||||
"description": "This is the title list of all invites"
|
"description": "This is the title list of all invites"
|
||||||
|
@ -6070,6 +6088,16 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"RemoveGroupMemberConfirmation__description__with-link": {
|
||||||
|
"message": "Remove \"$name$\" from the group? They will not be able to rejoin via the group link.",
|
||||||
|
"description": "When confirming the removal of a group member, show this text in the dialog",
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Jane Doe"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"CaptchaDialog__title": {
|
"CaptchaDialog__title": {
|
||||||
"message": "Verify to continue messaging",
|
"message": "Verify to continue messaging",
|
||||||
"description": "Header in the captcha dialog"
|
"description": "Header in the captcha dialog"
|
||||||
|
|
|
@ -45,6 +45,10 @@ message MemberPendingAdminApproval {
|
||||||
uint64 timestamp = 4;
|
uint64 timestamp = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message MemberBanned {
|
||||||
|
bytes userId = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message AccessControl {
|
message AccessControl {
|
||||||
enum AccessRequired {
|
enum AccessRequired {
|
||||||
UNKNOWN = 0;
|
UNKNOWN = 0;
|
||||||
|
@ -72,6 +76,8 @@ message Group {
|
||||||
bytes inviteLinkPassword = 10;
|
bytes inviteLinkPassword = 10;
|
||||||
bytes descriptionBytes = 11;
|
bytes descriptionBytes = 11;
|
||||||
bool announcementsOnly = 12;
|
bool announcementsOnly = 12;
|
||||||
|
repeated MemberBanned membersBanned = 13;
|
||||||
|
// next: 14
|
||||||
}
|
}
|
||||||
|
|
||||||
message GroupChange {
|
message GroupChange {
|
||||||
|
@ -121,6 +127,14 @@ message GroupChange {
|
||||||
Member.Role role = 2;
|
Member.Role role = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message AddMemberBannedAction {
|
||||||
|
MemberBanned added = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeleteMemberBannedAction {
|
||||||
|
bytes deletedUserId = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message ModifyTitleAction {
|
message ModifyTitleAction {
|
||||||
bytes title = 1;
|
bytes title = 1;
|
||||||
}
|
}
|
||||||
|
@ -183,6 +197,9 @@ message GroupChange {
|
||||||
ModifyInviteLinkPasswordAction modifyInviteLinkPassword = 19; // change epoch = 1
|
ModifyInviteLinkPasswordAction modifyInviteLinkPassword = 19; // change epoch = 1
|
||||||
ModifyDescriptionAction modifyDescription = 20; // change epoch = 2
|
ModifyDescriptionAction modifyDescription = 20; // change epoch = 2
|
||||||
ModifyAnnouncementsOnlyAction modifyAnnouncementsOnly = 21; // change epoch = 3
|
ModifyAnnouncementsOnlyAction modifyAnnouncementsOnly = 21; // change epoch = 3
|
||||||
|
repeated AddMemberBannedAction addMembersBanned = 22; // change epoch = 4
|
||||||
|
repeated DeleteMemberBannedAction deleteMembersBanned = 23; // change epoch = 4
|
||||||
|
// next: 24
|
||||||
}
|
}
|
||||||
|
|
||||||
bytes actions = 1; // The serialized actions
|
bytes actions = 1; // The serialized actions
|
||||||
|
|
|
@ -22,17 +22,23 @@ const story = storiesOf('Components/Conversation/ContactModal', module);
|
||||||
|
|
||||||
const defaultContact: ConversationType = getDefaultConversation({
|
const defaultContact: ConversationType = getDefaultConversation({
|
||||||
id: 'abcdef',
|
id: 'abcdef',
|
||||||
areWeAdmin: false,
|
|
||||||
title: 'Pauline Oliveros',
|
title: 'Pauline Oliveros',
|
||||||
phoneNumber: '(333) 444-5515',
|
phoneNumber: '(333) 444-5515',
|
||||||
about: '👍 Free to chat',
|
about: '👍 Free to chat',
|
||||||
});
|
});
|
||||||
|
const defaultGroup: ConversationType = getDefaultConversation({
|
||||||
|
id: 'abcdef',
|
||||||
|
areWeAdmin: true,
|
||||||
|
title: "It's a group",
|
||||||
|
groupLink: 'something',
|
||||||
|
});
|
||||||
|
|
||||||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
areWeASubscriber: false,
|
areWeASubscriber: false,
|
||||||
areWeAdmin: boolean('areWeAdmin', overrideProps.areWeAdmin || false),
|
areWeAdmin: boolean('areWeAdmin', overrideProps.areWeAdmin || false),
|
||||||
badges: overrideProps.badges || [],
|
badges: overrideProps.badges || [],
|
||||||
contact: overrideProps.contact || defaultContact,
|
contact: overrideProps.contact || defaultContact,
|
||||||
|
conversation: overrideProps.conversation || defaultGroup,
|
||||||
hideContactModal: action('hideContactModal'),
|
hideContactModal: action('hideContactModal'),
|
||||||
i18n,
|
i18n,
|
||||||
isAdmin: boolean('isAdmin', overrideProps.isAdmin || false),
|
isAdmin: boolean('isAdmin', overrideProps.isAdmin || false),
|
||||||
|
@ -62,6 +68,17 @@ story.add('As admin', () => {
|
||||||
return <ContactModal {...props} />;
|
return <ContactModal {...props} />;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
story.add('As admin with no group link', () => {
|
||||||
|
const props = createProps({
|
||||||
|
areWeAdmin: true,
|
||||||
|
conversation: {
|
||||||
|
...defaultGroup,
|
||||||
|
groupLink: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return <ContactModal {...props} />;
|
||||||
|
});
|
||||||
|
|
||||||
story.add('As admin, viewing non-member of group', () => {
|
story.add('As admin, viewing non-member of group', () => {
|
||||||
const props = createProps({
|
const props = createProps({
|
||||||
isMember: false,
|
isMember: false,
|
||||||
|
|
|
@ -2,7 +2,9 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
import * as log from '../../logging/log';
|
||||||
import { missingCaseError } from '../../util/missingCaseError';
|
import { missingCaseError } from '../../util/missingCaseError';
|
||||||
import { About } from './About';
|
import { About } from './About';
|
||||||
import { Avatar } from '../Avatar';
|
import { Avatar } from '../Avatar';
|
||||||
|
@ -14,13 +16,14 @@ import { BadgeDialog } from '../BadgeDialog';
|
||||||
import type { BadgeType } from '../../badges/types';
|
import type { BadgeType } from '../../badges/types';
|
||||||
import { SharedGroupNames } from '../SharedGroupNames';
|
import { SharedGroupNames } from '../SharedGroupNames';
|
||||||
import { ConfirmationDialog } from '../ConfirmationDialog';
|
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||||
|
import { RemoveGroupMemberConfirmationDialog } from './RemoveGroupMemberConfirmationDialog';
|
||||||
|
|
||||||
export type PropsDataType = {
|
export type PropsDataType = {
|
||||||
areWeASubscriber: boolean;
|
areWeASubscriber: boolean;
|
||||||
areWeAdmin: boolean;
|
areWeAdmin: boolean;
|
||||||
badges: ReadonlyArray<BadgeType>;
|
badges: ReadonlyArray<BadgeType>;
|
||||||
contact?: ConversationType;
|
contact?: ConversationType;
|
||||||
conversationId?: string;
|
conversation?: ConversationType;
|
||||||
readonly i18n: LocalizerType;
|
readonly i18n: LocalizerType;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
isMember: boolean;
|
isMember: boolean;
|
||||||
|
@ -50,12 +53,18 @@ enum ContactModalView {
|
||||||
ShowingBadges,
|
ShowingBadges,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum SubModalState {
|
||||||
|
None = 'None',
|
||||||
|
ToggleAdmin = 'ToggleAdmin',
|
||||||
|
MemberRemove = 'MemberRemove',
|
||||||
|
}
|
||||||
|
|
||||||
export const ContactModal = ({
|
export const ContactModal = ({
|
||||||
areWeASubscriber,
|
areWeASubscriber,
|
||||||
areWeAdmin,
|
areWeAdmin,
|
||||||
badges,
|
badges,
|
||||||
contact,
|
contact,
|
||||||
conversationId,
|
conversation,
|
||||||
hideContactModal,
|
hideContactModal,
|
||||||
i18n,
|
i18n,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
|
@ -72,14 +81,78 @@ export const ContactModal = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
const [view, setView] = useState(ContactModalView.Default);
|
const [view, setView] = useState(ContactModalView.Default);
|
||||||
const [confirmToggleAdmin, setConfirmToggleAdmin] = useState(false);
|
const [subModalState, setSubModalState] = useState<SubModalState>(
|
||||||
|
SubModalState.None
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (conversationId) {
|
if (conversation?.id) {
|
||||||
// Kick off the expensive hydration of the current sharedGroupNames
|
// Kick off the expensive hydration of the current sharedGroupNames
|
||||||
updateConversationModelSharedGroups(conversationId);
|
updateConversationModelSharedGroups(conversation.id);
|
||||||
}
|
}
|
||||||
}, [conversationId, updateConversationModelSharedGroups]);
|
}, [conversation?.id, updateConversationModelSharedGroups]);
|
||||||
|
|
||||||
|
let modalNode: ReactNode;
|
||||||
|
switch (subModalState) {
|
||||||
|
case SubModalState.None:
|
||||||
|
modalNode = undefined;
|
||||||
|
break;
|
||||||
|
case SubModalState.ToggleAdmin:
|
||||||
|
if (!conversation?.id) {
|
||||||
|
log.warn('ContactModal: ToggleAdmin state - missing conversationId');
|
||||||
|
modalNode = undefined;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
modalNode = (
|
||||||
|
<ConfirmationDialog
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
action: () => toggleAdmin(conversation.id, contact.id),
|
||||||
|
text: isAdmin
|
||||||
|
? i18n('ContactModal--rm-admin')
|
||||||
|
: i18n('ContactModal--make-admin'),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
i18n={i18n}
|
||||||
|
onClose={() => setSubModalState(SubModalState.None)}
|
||||||
|
>
|
||||||
|
{isAdmin
|
||||||
|
? i18n('ContactModal--rm-admin-info', [contact.title])
|
||||||
|
: i18n('ContactModal--make-admin-info', [contact.title])}
|
||||||
|
</ConfirmationDialog>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case SubModalState.MemberRemove:
|
||||||
|
if (!contact || !conversation?.id) {
|
||||||
|
log.warn(
|
||||||
|
'ContactModal: MemberRemove state - missing contact or conversationId'
|
||||||
|
);
|
||||||
|
modalNode = undefined;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
modalNode = (
|
||||||
|
<RemoveGroupMemberConfirmationDialog
|
||||||
|
conversation={contact}
|
||||||
|
group={conversation}
|
||||||
|
i18n={i18n}
|
||||||
|
onClose={() => {
|
||||||
|
setSubModalState(SubModalState.None);
|
||||||
|
}}
|
||||||
|
onRemove={() => {
|
||||||
|
removeMemberFromGroup(conversation?.id, contact.id);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default: {
|
||||||
|
const state: never = subModalState;
|
||||||
|
log.warn(`ContactModal: unexpected ${state}!`);
|
||||||
|
modalNode = undefined;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch (view) {
|
switch (view) {
|
||||||
case ContactModalView.Default: {
|
case ContactModalView.Default: {
|
||||||
|
@ -155,12 +228,12 @@ export const ContactModal = ({
|
||||||
<span>{i18n('showSafetyNumber')}</span>
|
<span>{i18n('showSafetyNumber')}</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{!contact.isMe && areWeAdmin && isMember && conversationId && (
|
{!contact.isMe && areWeAdmin && isMember && conversation?.id && (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="ContactModal__button ContactModal__make-admin"
|
className="ContactModal__button ContactModal__make-admin"
|
||||||
onClick={() => setConfirmToggleAdmin(true)}
|
onClick={() => setSubModalState(SubModalState.ToggleAdmin)}
|
||||||
>
|
>
|
||||||
<div className="ContactModal__bubble-icon">
|
<div className="ContactModal__bubble-icon">
|
||||||
<div className="ContactModal__make-admin__bubble-icon" />
|
<div className="ContactModal__make-admin__bubble-icon" />
|
||||||
|
@ -174,9 +247,7 @@ export const ContactModal = ({
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="ContactModal__button ContactModal__remove-from-group"
|
className="ContactModal__button ContactModal__remove-from-group"
|
||||||
onClick={() =>
|
onClick={() => setSubModalState(SubModalState.MemberRemove)}
|
||||||
removeMemberFromGroup(conversationId, contact.id)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<div className="ContactModal__bubble-icon">
|
<div className="ContactModal__bubble-icon">
|
||||||
<div className="ContactModal__remove-from-group__bubble-icon" />
|
<div className="ContactModal__remove-from-group__bubble-icon" />
|
||||||
|
@ -186,24 +257,7 @@ export const ContactModal = ({
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{confirmToggleAdmin && conversationId && (
|
{modalNode}
|
||||||
<ConfirmationDialog
|
|
||||||
actions={[
|
|
||||||
{
|
|
||||||
action: () => toggleAdmin(conversationId, contact.id),
|
|
||||||
text: isAdmin
|
|
||||||
? i18n('ContactModal--rm-admin')
|
|
||||||
: i18n('ContactModal--make-admin'),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
i18n={i18n}
|
|
||||||
onClose={() => setConfirmToggleAdmin(false)}
|
|
||||||
>
|
|
||||||
{isAdmin
|
|
||||||
? i18n('ContactModal--rm-admin-info', [contact.title])
|
|
||||||
: i18n('ContactModal--make-admin-info', [contact.title])}
|
|
||||||
</ConfirmationDialog>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|
|
@ -23,6 +23,7 @@ const story = storiesOf(
|
||||||
const getCommonProps = () => ({
|
const getCommonProps = () => ({
|
||||||
getPreferredBadge: () => undefined,
|
getPreferredBadge: () => undefined,
|
||||||
i18n,
|
i18n,
|
||||||
|
group: getDefaultConversation(),
|
||||||
onBlock: action('onBlock'),
|
onBlock: action('onBlock'),
|
||||||
onBlockAndReportSpam: action('onBlockAndReportSpam'),
|
onBlockAndReportSpam: action('onBlockAndReportSpam'),
|
||||||
onClose: action('onClose'),
|
onClose: action('onClose'),
|
||||||
|
@ -51,7 +52,10 @@ story.add('Direct conversations with same title', () => (
|
||||||
<ContactSpoofingReviewDialog
|
<ContactSpoofingReviewDialog
|
||||||
{...getCommonProps()}
|
{...getCommonProps()}
|
||||||
type={ContactSpoofingType.MultipleGroupMembersWithSameTitle}
|
type={ContactSpoofingType.MultipleGroupMembersWithSameTitle}
|
||||||
areWeAdmin={areWeAdmin}
|
group={{
|
||||||
|
...getDefaultConversation(),
|
||||||
|
areWeAdmin,
|
||||||
|
}}
|
||||||
collisionInfoByTitle={{
|
collisionInfoByTitle={{
|
||||||
Alice: times(2, () => ({
|
Alice: times(2, () => ({
|
||||||
oldName: 'Alicia',
|
oldName: 'Alicia',
|
||||||
|
|
|
@ -43,7 +43,7 @@ type PropsType = {
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: ContactSpoofingType.MultipleGroupMembersWithSameTitle;
|
type: ContactSpoofingType.MultipleGroupMembersWithSameTitle;
|
||||||
areWeAdmin: boolean;
|
group: ConversationType;
|
||||||
collisionInfoByTitle: Record<
|
collisionInfoByTitle: Record<
|
||||||
string,
|
string,
|
||||||
Array<{
|
Array<{
|
||||||
|
@ -78,13 +78,20 @@ export const ContactSpoofingReviewDialog: FunctionComponent<PropsType> =
|
||||||
const [confirmationState, setConfirmationState] = useState<
|
const [confirmationState, setConfirmationState] = useState<
|
||||||
| undefined
|
| undefined
|
||||||
| {
|
| {
|
||||||
type: ConfirmationStateType;
|
type: ConfirmationStateType.ConfirmingGroupRemoval;
|
||||||
|
affectedConversation: ConversationType;
|
||||||
|
group: ConversationType;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type:
|
||||||
|
| ConfirmationStateType.ConfirmingDelete
|
||||||
|
| ConfirmationStateType.ConfirmingBlock;
|
||||||
affectedConversation: ConversationType;
|
affectedConversation: ConversationType;
|
||||||
}
|
}
|
||||||
>();
|
>();
|
||||||
|
|
||||||
if (confirmationState) {
|
if (confirmationState) {
|
||||||
const { affectedConversation, type } = confirmationState;
|
const { type, affectedConversation } = confirmationState;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case ConfirmationStateType.ConfirmingDelete:
|
case ConfirmationStateType.ConfirmingDelete:
|
||||||
case ConfirmationStateType.ConfirmingBlock:
|
case ConfirmationStateType.ConfirmingBlock:
|
||||||
|
@ -140,10 +147,12 @@ export const ContactSpoofingReviewDialog: FunctionComponent<PropsType> =
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case ConfirmationStateType.ConfirmingGroupRemoval:
|
case ConfirmationStateType.ConfirmingGroupRemoval: {
|
||||||
|
const { group } = confirmationState;
|
||||||
return (
|
return (
|
||||||
<RemoveGroupMemberConfirmationDialog
|
<RemoveGroupMemberConfirmationDialog
|
||||||
conversation={affectedConversation}
|
conversation={affectedConversation}
|
||||||
|
group={group}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setConfirmationState(undefined);
|
setConfirmationState(undefined);
|
||||||
|
@ -153,6 +162,7 @@ export const ContactSpoofingReviewDialog: FunctionComponent<PropsType> =
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
throw missingCaseError(type);
|
throw missingCaseError(type);
|
||||||
}
|
}
|
||||||
|
@ -227,7 +237,7 @@ export const ContactSpoofingReviewDialog: FunctionComponent<PropsType> =
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ContactSpoofingType.MultipleGroupMembersWithSameTitle: {
|
case ContactSpoofingType.MultipleGroupMembersWithSameTitle: {
|
||||||
const { areWeAdmin, collisionInfoByTitle } = props;
|
const { group, collisionInfoByTitle } = props;
|
||||||
|
|
||||||
const unsortedConversationInfos = concat(
|
const unsortedConversationInfos = concat(
|
||||||
// This empty array exists to appease Lodash's type definitions.
|
// This empty array exists to appease Lodash's type definitions.
|
||||||
|
@ -254,7 +264,7 @@ export const ContactSpoofingReviewDialog: FunctionComponent<PropsType> =
|
||||||
</h2>
|
</h2>
|
||||||
{conversationInfos.map((conversationInfo, index) => {
|
{conversationInfos.map((conversationInfo, index) => {
|
||||||
let button: ReactNode;
|
let button: ReactNode;
|
||||||
if (areWeAdmin) {
|
if (group.areWeAdmin) {
|
||||||
button = (
|
button = (
|
||||||
<Button
|
<Button
|
||||||
variant={ButtonVariant.SecondaryAffirmative}
|
variant={ButtonVariant.SecondaryAffirmative}
|
||||||
|
@ -262,6 +272,7 @@ export const ContactSpoofingReviewDialog: FunctionComponent<PropsType> =
|
||||||
setConfirmationState({
|
setConfirmationState({
|
||||||
type: ConfirmationStateType.ConfirmingGroupRemoval,
|
type: ConfirmationStateType.ConfirmingGroupRemoval,
|
||||||
affectedConversation: conversationInfo.conversation,
|
affectedConversation: conversationInfo.conversation,
|
||||||
|
group,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -6,12 +6,14 @@ import React from 'react';
|
||||||
|
|
||||||
import type { ConversationType } from '../../state/ducks/conversations';
|
import type { ConversationType } from '../../state/ducks/conversations';
|
||||||
import type { LocalizerType } from '../../types/Util';
|
import type { LocalizerType } from '../../types/Util';
|
||||||
|
import { isAccessControlEnabled } from '../../groups/util';
|
||||||
|
|
||||||
import { ConfirmationDialog } from '../ConfirmationDialog';
|
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||||
import { Intl } from '../Intl';
|
import { Intl } from '../Intl';
|
||||||
import { ContactName } from './ContactName';
|
import { ContactName } from './ContactName';
|
||||||
|
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
|
group: ConversationType;
|
||||||
conversation: ConversationType;
|
conversation: ConversationType;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
@ -19,25 +21,33 @@ type PropsType = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RemoveGroupMemberConfirmationDialog: FunctionComponent<PropsType> =
|
export const RemoveGroupMemberConfirmationDialog: FunctionComponent<PropsType> =
|
||||||
({ conversation, i18n, onClose, onRemove }) => (
|
({ conversation, group, i18n, onClose, onRemove }) => {
|
||||||
<ConfirmationDialog
|
const descriptionKey = isAccessControlEnabled(
|
||||||
actions={[
|
group.accessControlAddFromInviteLink
|
||||||
{
|
)
|
||||||
action: onRemove,
|
? 'RemoveGroupMemberConfirmation__description__with-link'
|
||||||
text: i18n('RemoveGroupMemberConfirmation__remove-button'),
|
: 'RemoveGroupMemberConfirmation__description';
|
||||||
style: 'negative',
|
|
||||||
},
|
return (
|
||||||
]}
|
<ConfirmationDialog
|
||||||
i18n={i18n}
|
actions={[
|
||||||
onClose={onClose}
|
{
|
||||||
title={
|
action: onRemove,
|
||||||
<Intl
|
text: i18n('RemoveGroupMemberConfirmation__remove-button'),
|
||||||
i18n={i18n}
|
style: 'negative',
|
||||||
id="RemoveGroupMemberConfirmation__description"
|
},
|
||||||
components={{
|
]}
|
||||||
name: <ContactName title={conversation.title} />,
|
i18n={i18n}
|
||||||
}}
|
onClose={onClose}
|
||||||
/>
|
title={
|
||||||
}
|
<Intl
|
||||||
/>
|
i18n={i18n}
|
||||||
);
|
id={descriptionKey}
|
||||||
|
components={{
|
||||||
|
name: <ContactName title={conversation.title} />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -492,6 +492,7 @@ const renderTypingBubble = () => (
|
||||||
);
|
);
|
||||||
|
|
||||||
const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
|
conversation: overrideProps.conversation || getDefaultConversation(),
|
||||||
discardMessages: action('discardMessages'),
|
discardMessages: action('discardMessages'),
|
||||||
getPreferredBadge: () => undefined,
|
getPreferredBadge: () => undefined,
|
||||||
i18n,
|
i18n,
|
||||||
|
|
|
@ -94,7 +94,7 @@ export type PropsDataType = {
|
||||||
|
|
||||||
type PropsHousekeepingType = {
|
type PropsHousekeepingType = {
|
||||||
id: string;
|
id: string;
|
||||||
areWeAdmin?: boolean;
|
conversation: ConversationType;
|
||||||
isConversationSelected: boolean;
|
isConversationSelected: boolean;
|
||||||
isGroupV1AndDisabled?: boolean;
|
isGroupV1AndDisabled?: boolean;
|
||||||
isIncomingMessageRequest: boolean;
|
isIncomingMessageRequest: boolean;
|
||||||
|
@ -734,10 +734,10 @@ export class Timeline extends React.Component<
|
||||||
public override render(): JSX.Element | null {
|
public override render(): JSX.Element | null {
|
||||||
const {
|
const {
|
||||||
acknowledgeGroupMemberNameCollisions,
|
acknowledgeGroupMemberNameCollisions,
|
||||||
areWeAdmin,
|
|
||||||
clearInvitedUuidsForNewlyCreatedGroup,
|
clearInvitedUuidsForNewlyCreatedGroup,
|
||||||
closeContactSpoofingReview,
|
closeContactSpoofingReview,
|
||||||
contactSpoofingReview,
|
contactSpoofingReview,
|
||||||
|
conversation,
|
||||||
getPreferredBadge,
|
getPreferredBadge,
|
||||||
getTimestampForMessage,
|
getTimestampForMessage,
|
||||||
haveNewest,
|
haveNewest,
|
||||||
|
@ -1018,7 +1018,7 @@ export class Timeline extends React.Component<
|
||||||
<ContactSpoofingReviewDialog
|
<ContactSpoofingReviewDialog
|
||||||
{...commonProps}
|
{...commonProps}
|
||||||
type={ContactSpoofingType.MultipleGroupMembersWithSameTitle}
|
type={ContactSpoofingType.MultipleGroupMembersWithSameTitle}
|
||||||
areWeAdmin={Boolean(areWeAdmin)}
|
group={conversation}
|
||||||
collisionInfoByTitle={contactSpoofingReview.collisionInfoByTitle}
|
collisionInfoByTitle={contactSpoofingReview.collisionInfoByTitle}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { ConfirmationDialog } from '../../ConfirmationDialog';
|
||||||
import { PanelSection } from './PanelSection';
|
import { PanelSection } from './PanelSection';
|
||||||
import { PanelRow } from './PanelRow';
|
import { PanelRow } from './PanelRow';
|
||||||
import { ConversationDetailsIcon, IconType } from './ConversationDetailsIcon';
|
import { ConversationDetailsIcon, IconType } from './ConversationDetailsIcon';
|
||||||
|
import { isAccessControlEnabled } from '../../../groups/util';
|
||||||
|
|
||||||
export type PropsType = {
|
export type PropsType = {
|
||||||
readonly conversation?: ConversationType;
|
readonly conversation?: ConversationType;
|
||||||
|
@ -147,6 +148,7 @@ export const PendingInvites: React.ComponentType<PropsType> = ({
|
||||||
{stagedMemberships && stagedMemberships.length && (
|
{stagedMemberships && stagedMemberships.length && (
|
||||||
<MembershipActionConfirmation
|
<MembershipActionConfirmation
|
||||||
approvePendingMembership={approvePendingMembership}
|
approvePendingMembership={approvePendingMembership}
|
||||||
|
conversation={conversation}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
members={conversation.sortedGroupMembers || []}
|
members={conversation.sortedGroupMembers || []}
|
||||||
onClose={() => setStagedMemberships(null)}
|
onClose={() => setStagedMemberships(null)}
|
||||||
|
@ -161,6 +163,7 @@ export const PendingInvites: React.ComponentType<PropsType> = ({
|
||||||
|
|
||||||
function MembershipActionConfirmation({
|
function MembershipActionConfirmation({
|
||||||
approvePendingMembership,
|
approvePendingMembership,
|
||||||
|
conversation,
|
||||||
i18n,
|
i18n,
|
||||||
members,
|
members,
|
||||||
onClose,
|
onClose,
|
||||||
|
@ -169,6 +172,7 @@ function MembershipActionConfirmation({
|
||||||
stagedMemberships,
|
stagedMemberships,
|
||||||
}: {
|
}: {
|
||||||
approvePendingMembership: (conversationId: string) => void;
|
approvePendingMembership: (conversationId: string) => void;
|
||||||
|
conversation: ConversationType;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
members: Array<ConversationType>;
|
members: Array<ConversationType>;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
@ -222,6 +226,7 @@ function MembershipActionConfirmation({
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
>
|
>
|
||||||
{getConfirmationMessage({
|
{getConfirmationMessage({
|
||||||
|
conversation,
|
||||||
i18n,
|
i18n,
|
||||||
members,
|
members,
|
||||||
ourUuid,
|
ourUuid,
|
||||||
|
@ -232,11 +237,13 @@ function MembershipActionConfirmation({
|
||||||
}
|
}
|
||||||
|
|
||||||
function getConfirmationMessage({
|
function getConfirmationMessage({
|
||||||
|
conversation,
|
||||||
i18n,
|
i18n,
|
||||||
members,
|
members,
|
||||||
ourUuid,
|
ourUuid,
|
||||||
stagedMemberships,
|
stagedMemberships,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
|
conversation: ConversationType;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
members: ReadonlyArray<ConversationType>;
|
members: ReadonlyArray<ConversationType>;
|
||||||
ourUuid: string;
|
ourUuid: string;
|
||||||
|
@ -251,7 +258,12 @@ function getConfirmationMessage({
|
||||||
|
|
||||||
// Requesting a membership since they weren't added by anyone
|
// Requesting a membership since they weren't added by anyone
|
||||||
if (membershipType === StageType.DENY_REQUEST) {
|
if (membershipType === StageType.DENY_REQUEST) {
|
||||||
return i18n('PendingRequests--deny-for', {
|
const key = isAccessControlEnabled(
|
||||||
|
conversation.accessControlAddFromInviteLink
|
||||||
|
)
|
||||||
|
? 'PendingRequests--deny-for--with-link'
|
||||||
|
: 'PendingRequests--deny-for';
|
||||||
|
return i18n(key, {
|
||||||
name: firstMembership.member.title,
|
name: firstMembership.member.title,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
177
ts/groups.ts
177
ts/groups.ts
|
@ -74,13 +74,15 @@ import { UUID, isValidUuid } from './types/UUID';
|
||||||
import type { UUIDStringType } from './types/UUID';
|
import type { UUIDStringType } from './types/UUID';
|
||||||
import * as Errors from './types/errors';
|
import * as Errors from './types/errors';
|
||||||
import { SignalService as Proto } from './protobuf';
|
import { SignalService as Proto } from './protobuf';
|
||||||
|
import { isNotNil } from './util/isNotNil';
|
||||||
|
import { isAccessControlEnabled } from './groups/util';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
conversationJobQueue,
|
conversationJobQueue,
|
||||||
conversationQueueJobEnum,
|
conversationQueueJobEnum,
|
||||||
} from './jobs/conversationJobQueue';
|
} from './jobs/conversationJobQueue';
|
||||||
|
|
||||||
import AccessRequiredEnum = Proto.AccessControl.AccessRequired;
|
type AccessRequiredEnum = Proto.AccessControl.AccessRequired;
|
||||||
|
|
||||||
export { joinViaLink } from './groups/joinViaLink';
|
export { joinViaLink } from './groups/joinViaLink';
|
||||||
|
|
||||||
|
@ -271,7 +273,7 @@ export const ID_LENGTH = 32;
|
||||||
const TEMPORAL_AUTH_REJECTED_CODE = 401;
|
const TEMPORAL_AUTH_REJECTED_CODE = 401;
|
||||||
const GROUP_ACCESS_DENIED_CODE = 403;
|
const GROUP_ACCESS_DENIED_CODE = 403;
|
||||||
const GROUP_NONEXISTENT_CODE = 404;
|
const GROUP_NONEXISTENT_CODE = 404;
|
||||||
const SUPPORTED_CHANGE_EPOCH = 2;
|
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;
|
||||||
|
|
||||||
|
@ -582,7 +584,7 @@ function buildGroupProto(
|
||||||
export async function buildAddMembersChange(
|
export async function buildAddMembersChange(
|
||||||
conversation: Pick<
|
conversation: Pick<
|
||||||
ConversationAttributesType,
|
ConversationAttributesType,
|
||||||
'id' | 'publicParams' | 'revision' | 'secretParams'
|
'bannedMembersV2' | 'id' | 'publicParams' | 'revision' | 'secretParams'
|
||||||
>,
|
>,
|
||||||
conversationIds: ReadonlyArray<string>
|
conversationIds: ReadonlyArray<string>
|
||||||
): Promise<undefined | Proto.GroupChange.Actions> {
|
): Promise<undefined | Proto.GroupChange.Actions> {
|
||||||
|
@ -621,6 +623,7 @@ export async function buildAddMembersChange(
|
||||||
const addMembers: Array<Proto.GroupChange.Actions.AddMemberAction> = [];
|
const addMembers: Array<Proto.GroupChange.Actions.AddMemberAction> = [];
|
||||||
const addPendingMembers: Array<Proto.GroupChange.Actions.AddMemberPendingProfileKeyAction> =
|
const addPendingMembers: Array<Proto.GroupChange.Actions.AddMemberPendingProfileKeyAction> =
|
||||||
[];
|
[];
|
||||||
|
const actions = new Proto.GroupChange.Actions();
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
conversationIds.map(async conversationId => {
|
conversationIds.map(async conversationId => {
|
||||||
|
@ -679,10 +682,22 @@ export async function buildAddMembersChange(
|
||||||
|
|
||||||
addPendingMembers.push(addPendingMemberAction);
|
addPendingMembers.push(addPendingMemberAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const doesMemberNeedUnban = conversation.bannedMembersV2?.includes(uuid);
|
||||||
|
if (doesMemberNeedUnban) {
|
||||||
|
const uuidCipherTextBuffer = encryptUuid(clientZkGroupCipher, uuid);
|
||||||
|
|
||||||
|
const deleteMemberBannedAction =
|
||||||
|
new Proto.GroupChange.Actions.DeleteMemberBannedAction();
|
||||||
|
|
||||||
|
deleteMemberBannedAction.deletedUserId = uuidCipherTextBuffer;
|
||||||
|
|
||||||
|
actions.deleteMembersBanned = actions.deleteMembersBanned || [];
|
||||||
|
actions.deleteMembersBanned.push(deleteMemberBannedAction);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const actions = new Proto.GroupChange.Actions();
|
|
||||||
if (!addMembers.length && !addPendingMembers.length) {
|
if (!addMembers.length && !addPendingMembers.length) {
|
||||||
// This shouldn't happen. When these actions are passed to `modifyGroupV2`, a warning
|
// This shouldn't happen. When these actions are passed to `modifyGroupV2`, a warning
|
||||||
// will be logged.
|
// will be logged.
|
||||||
|
@ -920,9 +935,11 @@ export function buildAccessControlMembersChange(
|
||||||
// TODO AND-1101
|
// TODO AND-1101
|
||||||
export function buildDeletePendingAdminApprovalMemberChange({
|
export function buildDeletePendingAdminApprovalMemberChange({
|
||||||
group,
|
group,
|
||||||
|
ourUuid,
|
||||||
uuid,
|
uuid,
|
||||||
}: {
|
}: {
|
||||||
group: ConversationAttributesType;
|
group: ConversationAttributesType;
|
||||||
|
ourUuid: UUIDStringType;
|
||||||
uuid: UUIDStringType;
|
uuid: UUIDStringType;
|
||||||
}): Proto.GroupChange.Actions {
|
}): Proto.GroupChange.Actions {
|
||||||
const actions = new Proto.GroupChange.Actions();
|
const actions = new Proto.GroupChange.Actions();
|
||||||
|
@ -944,6 +961,18 @@ export function buildDeletePendingAdminApprovalMemberChange({
|
||||||
deleteMemberPendingAdminApproval,
|
deleteMemberPendingAdminApproval,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const doesMemberNeedBan =
|
||||||
|
!group.bannedMembersV2?.includes(uuid) && uuid !== ourUuid;
|
||||||
|
if (doesMemberNeedBan) {
|
||||||
|
const addMemberBannedAction =
|
||||||
|
new Proto.GroupChange.Actions.AddMemberBannedAction();
|
||||||
|
|
||||||
|
addMemberBannedAction.added = new Proto.MemberBanned();
|
||||||
|
addMemberBannedAction.added.userId = uuidCipherTextBuffer;
|
||||||
|
|
||||||
|
actions.addMembersBanned = [addMemberBannedAction];
|
||||||
|
}
|
||||||
|
|
||||||
return actions;
|
return actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -990,11 +1019,13 @@ export function buildAddMember({
|
||||||
group,
|
group,
|
||||||
profileKeyCredentialBase64,
|
profileKeyCredentialBase64,
|
||||||
serverPublicParamsBase64,
|
serverPublicParamsBase64,
|
||||||
|
uuid,
|
||||||
}: {
|
}: {
|
||||||
group: ConversationAttributesType;
|
group: ConversationAttributesType;
|
||||||
profileKeyCredentialBase64: string;
|
profileKeyCredentialBase64: string;
|
||||||
serverPublicParamsBase64: string;
|
serverPublicParamsBase64: string;
|
||||||
joinFromInviteLink?: boolean;
|
joinFromInviteLink?: boolean;
|
||||||
|
uuid: UUIDStringType;
|
||||||
}): Proto.GroupChange.Actions {
|
}): Proto.GroupChange.Actions {
|
||||||
const MEMBER_ROLE_ENUM = Proto.Member.Role;
|
const MEMBER_ROLE_ENUM = Proto.Member.Role;
|
||||||
|
|
||||||
|
@ -1023,6 +1054,18 @@ export function buildAddMember({
|
||||||
actions.version = (group.revision || 0) + 1;
|
actions.version = (group.revision || 0) + 1;
|
||||||
actions.addMembers = [addMember];
|
actions.addMembers = [addMember];
|
||||||
|
|
||||||
|
const doesMemberNeedUnban = group.bannedMembersV2?.includes(uuid);
|
||||||
|
if (doesMemberNeedUnban) {
|
||||||
|
const clientZkGroupCipher = getClientZkGroupCipher(group.secretParams);
|
||||||
|
const uuidCipherTextBuffer = encryptUuid(clientZkGroupCipher, uuid);
|
||||||
|
|
||||||
|
const deleteMemberBannedAction =
|
||||||
|
new Proto.GroupChange.Actions.DeleteMemberBannedAction();
|
||||||
|
|
||||||
|
deleteMemberBannedAction.deletedUserId = uuidCipherTextBuffer;
|
||||||
|
actions.deleteMembersBanned = [deleteMemberBannedAction];
|
||||||
|
}
|
||||||
|
|
||||||
return actions;
|
return actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1057,11 +1100,13 @@ export function buildDeletePendingMemberChange({
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildDeleteMemberChange({
|
export function buildDeleteMemberChange({
|
||||||
uuid,
|
|
||||||
group,
|
group,
|
||||||
|
ourUuid,
|
||||||
|
uuid,
|
||||||
}: {
|
}: {
|
||||||
uuid: UUIDStringType;
|
|
||||||
group: ConversationAttributesType;
|
group: ConversationAttributesType;
|
||||||
|
ourUuid: UUIDStringType;
|
||||||
|
uuid: UUIDStringType;
|
||||||
}): Proto.GroupChange.Actions {
|
}): Proto.GroupChange.Actions {
|
||||||
const actions = new Proto.GroupChange.Actions();
|
const actions = new Proto.GroupChange.Actions();
|
||||||
|
|
||||||
|
@ -1077,6 +1122,19 @@ export function buildDeleteMemberChange({
|
||||||
actions.version = (group.revision || 0) + 1;
|
actions.version = (group.revision || 0) + 1;
|
||||||
actions.deleteMembers = [deleteMember];
|
actions.deleteMembers = [deleteMember];
|
||||||
|
|
||||||
|
const doesMemberNeedBan =
|
||||||
|
!group.bannedMembersV2?.includes(uuid) && uuid !== ourUuid;
|
||||||
|
|
||||||
|
if (doesMemberNeedBan) {
|
||||||
|
const addMemberBannedAction =
|
||||||
|
new Proto.GroupChange.Actions.AddMemberBannedAction();
|
||||||
|
|
||||||
|
addMemberBannedAction.added = new Proto.MemberBanned();
|
||||||
|
addMemberBannedAction.added.userId = uuidCipherTextBuffer;
|
||||||
|
|
||||||
|
actions.addMembersBanned = [addMemberBannedAction];
|
||||||
|
}
|
||||||
|
|
||||||
return actions;
|
return actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3548,12 +3606,12 @@ function extractDiffs({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const linkPreviouslyEnabled =
|
const linkPreviouslyEnabled = isAccessControlEnabled(
|
||||||
old.accessControl?.addFromInviteLink === ACCESS_ENUM.ANY ||
|
old.accessControl?.addFromInviteLink
|
||||||
old.accessControl?.addFromInviteLink === ACCESS_ENUM.ADMINISTRATOR;
|
);
|
||||||
const linkCurrentlyEnabled =
|
const linkCurrentlyEnabled = isAccessControlEnabled(
|
||||||
current.accessControl?.addFromInviteLink === ACCESS_ENUM.ANY ||
|
current.accessControl?.addFromInviteLink
|
||||||
current.accessControl?.addFromInviteLink === ACCESS_ENUM.ADMINISTRATOR;
|
);
|
||||||
|
|
||||||
if (!linkPreviouslyEnabled && linkCurrentlyEnabled) {
|
if (!linkPreviouslyEnabled && linkCurrentlyEnabled) {
|
||||||
details.push({
|
details.push({
|
||||||
|
@ -3808,6 +3866,8 @@ function extractDiffs({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note: currently no diff generated for bannedMembersV2 changes
|
||||||
|
|
||||||
// final processing
|
// final processing
|
||||||
|
|
||||||
let message: MessageAttributesType | undefined;
|
let message: MessageAttributesType | undefined;
|
||||||
|
@ -3967,6 +4027,9 @@ async function applyGroupChange({
|
||||||
> = fromPairs(
|
> = fromPairs(
|
||||||
(result.pendingAdminApprovalV2 || []).map(member => [member.uuid, member])
|
(result.pendingAdminApprovalV2 || []).map(member => [member.uuid, member])
|
||||||
);
|
);
|
||||||
|
const bannedMembers: Record<UUIDStringType, UUIDStringType> = fromPairs(
|
||||||
|
(result.bannedMembersV2 || []).map(uuid => [uuid, uuid])
|
||||||
|
);
|
||||||
|
|
||||||
// version?: number;
|
// version?: number;
|
||||||
result.revision = version;
|
result.revision = version;
|
||||||
|
@ -4388,6 +4451,32 @@ async function applyGroupChange({
|
||||||
result.announcementsOnly = announcementsOnly;
|
result.announcementsOnly = announcementsOnly;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (actions.addMembersBanned && actions.addMembersBanned.length > 0) {
|
||||||
|
actions.addMembersBanned.forEach(uuid => {
|
||||||
|
if (bannedMembers[uuid]) {
|
||||||
|
log.warn(
|
||||||
|
`applyGroupChange/${logId}: Attempt to add banned member failed; was already in banned list.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bannedMembers[uuid] = uuid;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actions.deleteMembersBanned && actions.deleteMembersBanned.length > 0) {
|
||||||
|
actions.deleteMembersBanned.forEach(uuid => {
|
||||||
|
if (!bannedMembers[uuid]) {
|
||||||
|
log.warn(
|
||||||
|
`applyGroupChange/${logId}: Attempt to remove banned member failed; was not in banned list.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete bannedMembers[uuid];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (ourUuid) {
|
if (ourUuid) {
|
||||||
result.left = !members[ourUuid];
|
result.left = !members[ourUuid];
|
||||||
}
|
}
|
||||||
|
@ -4396,6 +4485,7 @@ async function applyGroupChange({
|
||||||
result.membersV2 = values(members);
|
result.membersV2 = values(members);
|
||||||
result.pendingMembersV2 = values(pendingMembers);
|
result.pendingMembersV2 = values(pendingMembers);
|
||||||
result.pendingAdminApprovalV2 = values(pendingAdminApprovalMembers);
|
result.pendingAdminApprovalV2 = values(pendingAdminApprovalMembers);
|
||||||
|
result.bannedMembersV2 = values(bannedMembers);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
newAttributes: result,
|
newAttributes: result,
|
||||||
|
@ -4650,6 +4740,9 @@ async function applyGroupState({
|
||||||
// announcementsOnly
|
// announcementsOnly
|
||||||
result.announcementsOnly = groupState.announcementsOnly;
|
result.announcementsOnly = groupState.announcementsOnly;
|
||||||
|
|
||||||
|
// membersBanned
|
||||||
|
result.bannedMembersV2 = groupState.membersBanned;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
newAttributes: result,
|
newAttributes: result,
|
||||||
newProfileKeys,
|
newProfileKeys,
|
||||||
|
@ -4759,6 +4852,8 @@ type DecryptedGroupChangeActions = {
|
||||||
modifyAnnouncementsOnly?: {
|
modifyAnnouncementsOnly?: {
|
||||||
announcementsOnly: boolean;
|
announcementsOnly: boolean;
|
||||||
};
|
};
|
||||||
|
addMembersBanned?: ReadonlyArray<UUIDStringType>;
|
||||||
|
deleteMembersBanned?: ReadonlyArray<UUIDStringType>;
|
||||||
} & Pick<
|
} & Pick<
|
||||||
Proto.GroupChange.IActions,
|
Proto.GroupChange.IActions,
|
||||||
| 'modifyAttributesAccess'
|
| 'modifyAttributesAccess'
|
||||||
|
@ -5286,6 +5381,42 @@ function decryptGroupChange(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// addMembersBanned
|
||||||
|
if (actions.addMembersBanned && actions.addMembersBanned.length > 0) {
|
||||||
|
result.addMembersBanned = actions.addMembersBanned
|
||||||
|
.map(item => {
|
||||||
|
if (!item.added || !item.added.userId) {
|
||||||
|
log.warn(
|
||||||
|
`decryptGroupChange/${logId}: addMembersBanned had a blank entry`
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return normalizeUuid(
|
||||||
|
decryptUuid(clientZkGroupCipher, item.added.userId),
|
||||||
|
'addMembersBanned.added.userId'
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.filter(isNotNil);
|
||||||
|
}
|
||||||
|
|
||||||
|
// deleteMembersBanned
|
||||||
|
if (actions.deleteMembersBanned && actions.deleteMembersBanned.length > 0) {
|
||||||
|
result.deleteMembersBanned = actions.deleteMembersBanned
|
||||||
|
.map(item => {
|
||||||
|
if (!item.deletedUserId) {
|
||||||
|
log.warn(
|
||||||
|
`decryptGroupChange/${logId}: deleteMembersBanned had a blank entry`
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return normalizeUuid(
|
||||||
|
decryptUuid(clientZkGroupCipher, item.deletedUserId),
|
||||||
|
'deleteMembersBanned.deletedUserId'
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.filter(isNotNil);
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5344,6 +5475,7 @@ type DecryptedGroupState = {
|
||||||
descriptionBytes?: Proto.GroupAttributeBlob;
|
descriptionBytes?: Proto.GroupAttributeBlob;
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
announcementsOnly?: boolean;
|
announcementsOnly?: boolean;
|
||||||
|
membersBanned?: Array<UUIDStringType>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function decryptGroupState(
|
function decryptGroupState(
|
||||||
|
@ -5479,6 +5611,27 @@ function decryptGroupState(
|
||||||
const { announcementsOnly } = groupState;
|
const { announcementsOnly } = groupState;
|
||||||
result.announcementsOnly = Boolean(announcementsOnly);
|
result.announcementsOnly = Boolean(announcementsOnly);
|
||||||
|
|
||||||
|
// membersBanned
|
||||||
|
const { membersBanned } = groupState;
|
||||||
|
if (membersBanned && membersBanned.length > 0) {
|
||||||
|
result.membersBanned = membersBanned
|
||||||
|
.map(item => {
|
||||||
|
if (!item.userId) {
|
||||||
|
log.warn(
|
||||||
|
`decryptGroupState/${logId}: membersBanned had a blank entry`
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return normalizeUuid(
|
||||||
|
decryptUuid(clientZkGroupCipher, item.userId),
|
||||||
|
'membersBanned.added.userId'
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.filter(isNotNil);
|
||||||
|
} else {
|
||||||
|
result.membersBanned = [];
|
||||||
|
}
|
||||||
|
|
||||||
result.avatar = dropNull(groupState.avatar);
|
result.avatar = dropNull(groupState.avatar);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
LINK_VERSION_ERROR,
|
LINK_VERSION_ERROR,
|
||||||
parseGroupLink,
|
parseGroupLink,
|
||||||
} from '../groups';
|
} from '../groups';
|
||||||
|
import * as Errors from '../types/errors';
|
||||||
import * as Bytes from '../Bytes';
|
import * as Bytes from '../Bytes';
|
||||||
import { longRunningTaskWrapper } from '../util/longRunningTaskWrapper';
|
import { longRunningTaskWrapper } from '../util/longRunningTaskWrapper';
|
||||||
import { isGroupV1 } from '../util/whatTypeOfConversation';
|
import { isGroupV1 } from '../util/whatTypeOfConversation';
|
||||||
|
@ -24,16 +25,19 @@ import * as log from '../logging/log';
|
||||||
import { showToast } from '../util/showToast';
|
import { showToast } from '../util/showToast';
|
||||||
import { ToastAlreadyGroupMember } from '../components/ToastAlreadyGroupMember';
|
import { ToastAlreadyGroupMember } from '../components/ToastAlreadyGroupMember';
|
||||||
import { ToastAlreadyRequestedToJoin } from '../components/ToastAlreadyRequestedToJoin';
|
import { ToastAlreadyRequestedToJoin } from '../components/ToastAlreadyRequestedToJoin';
|
||||||
|
import { HTTPError } from '../textsecure/Errors';
|
||||||
|
import { isAccessControlEnabled } from './util';
|
||||||
|
|
||||||
export async function joinViaLink(hash: string): Promise<void> {
|
export async function joinViaLink(hash: string): Promise<void> {
|
||||||
let inviteLinkPassword: string;
|
let inviteLinkPassword: string;
|
||||||
let masterKey: string;
|
let masterKey: string;
|
||||||
try {
|
try {
|
||||||
({ inviteLinkPassword, masterKey } = parseGroupLink(hash));
|
({ inviteLinkPassword, masterKey } = parseGroupLink(hash));
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
const errorString = error && error.stack ? error.stack : error;
|
const errorString = Errors.toLogFormat(error);
|
||||||
log.error(`joinViaLink: Failed to parse group link ${errorString}`);
|
log.error(`joinViaLink: Failed to parse group link ${errorString}`);
|
||||||
if (error && error.name === LINK_VERSION_ERROR) {
|
|
||||||
|
if (error instanceof Error && error.name === LINK_VERSION_ERROR) {
|
||||||
showErrorDialog(
|
showErrorDialog(
|
||||||
window.i18n('GroupV2--join--unknown-link-version'),
|
window.i18n('GroupV2--join--unknown-link-version'),
|
||||||
window.i18n('GroupV2--join--unknown-link-version--title')
|
window.i18n('GroupV2--join--unknown-link-version--title')
|
||||||
|
@ -84,28 +88,35 @@ export async function joinViaLink(hash: string): Promise<void> {
|
||||||
suppressErrorDialog: true,
|
suppressErrorDialog: true,
|
||||||
task: () => getPreJoinGroupInfo(inviteLinkPassword, masterKey),
|
task: () => getPreJoinGroupInfo(inviteLinkPassword, masterKey),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
const errorString = error && error.stack ? error.stack : error;
|
const errorString = Errors.toLogFormat(error);
|
||||||
log.error(
|
log.error(
|
||||||
`joinViaLink/${logId}: Failed to fetch group info - ${errorString}`
|
`joinViaLink/${logId}: Failed to fetch group info - ${errorString}`
|
||||||
);
|
);
|
||||||
|
|
||||||
showErrorDialog(
|
if (
|
||||||
error.code && error.code === 403
|
error instanceof HTTPError &&
|
||||||
? window.i18n('GroupV2--join--link-revoked')
|
error.responseHeaders['x-signal-forbidden-reason']
|
||||||
: window.i18n('GroupV2--join--general-join-failure'),
|
) {
|
||||||
error.code && error.code === 403
|
showErrorDialog(
|
||||||
? window.i18n('GroupV2--join--link-revoked--title')
|
window.i18n('GroupV2--join--link-forbidden'),
|
||||||
: window.i18n('GroupV2--join--general-join-failure--title')
|
window.i18n('GroupV2--join--link-forbidden--title')
|
||||||
);
|
);
|
||||||
|
} else if (error instanceof HTTPError && error.code === 403) {
|
||||||
|
showErrorDialog(
|
||||||
|
window.i18n('GroupV2--join--link-revoked'),
|
||||||
|
window.i18n('GroupV2--join--link-revoked--title')
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showErrorDialog(
|
||||||
|
window.i18n('GroupV2--join--general-join-failure'),
|
||||||
|
window.i18n('GroupV2--join--general-join-failure--title')
|
||||||
|
);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ACCESS_ENUM = Proto.AccessControl.AccessRequired;
|
if (!isAccessControlEnabled(result.addFromInviteLink)) {
|
||||||
if (
|
|
||||||
result.addFromInviteLink !== ACCESS_ENUM.ADMINISTRATOR &&
|
|
||||||
result.addFromInviteLink !== ACCESS_ENUM.ANY
|
|
||||||
) {
|
|
||||||
log.error(
|
log.error(
|
||||||
`joinViaLink/${logId}: addFromInviteLink value of ${result.addFromInviteLink} is invalid`
|
`joinViaLink/${logId}: addFromInviteLink value of ${result.addFromInviteLink} is invalid`
|
||||||
);
|
);
|
||||||
|
@ -124,7 +135,8 @@ export async function joinViaLink(hash: string): Promise<void> {
|
||||||
| undefined = result.avatar ? { loading: true } : undefined;
|
| undefined = result.avatar ? { loading: true } : undefined;
|
||||||
const memberCount = result.memberCount || 1;
|
const memberCount = result.memberCount || 1;
|
||||||
const approvalRequired =
|
const approvalRequired =
|
||||||
result.addFromInviteLink === ACCESS_ENUM.ADMINISTRATOR;
|
result.addFromInviteLink ===
|
||||||
|
Proto.AccessControl.AccessRequired.ADMINISTRATOR;
|
||||||
const title =
|
const title =
|
||||||
decryptGroupTitle(result.title, secretParams) ||
|
decryptGroupTitle(result.title, secretParams) ||
|
||||||
window.i18n('unknownGroup');
|
window.i18n('unknownGroup');
|
||||||
|
|
15
ts/groups/util.ts
Normal file
15
ts/groups/util.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { SignalService as Proto } from '../protobuf';
|
||||||
|
|
||||||
|
const ACCESS_ENUM = Proto.AccessControl.AccessRequired;
|
||||||
|
|
||||||
|
export function isAccessControlEnabled(
|
||||||
|
accessControl: number | undefined
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
accessControl === ACCESS_ENUM.ANY ||
|
||||||
|
accessControl === ACCESS_ENUM.ADMINISTRATOR
|
||||||
|
);
|
||||||
|
}
|
1
ts/model-types.d.ts
vendored
1
ts/model-types.d.ts
vendored
|
@ -344,6 +344,7 @@ export type ConversationAttributesType = {
|
||||||
membersV2?: Array<GroupV2MemberType>;
|
membersV2?: Array<GroupV2MemberType>;
|
||||||
pendingMembersV2?: Array<GroupV2PendingMemberType>;
|
pendingMembersV2?: Array<GroupV2PendingMemberType>;
|
||||||
pendingAdminApprovalV2?: Array<GroupV2PendingAdminApprovalType>;
|
pendingAdminApprovalV2?: Array<GroupV2PendingAdminApprovalType>;
|
||||||
|
bannedMembersV2?: Array<UUIDStringType>;
|
||||||
groupInviteLinkPassword?: string;
|
groupInviteLinkPassword?: string;
|
||||||
previousGroupV1Id?: string;
|
previousGroupV1Id?: string;
|
||||||
previousGroupV1Members?: Array<string>;
|
previousGroupV1Members?: Array<string>;
|
||||||
|
|
|
@ -52,7 +52,7 @@ import { sniffImageMimeType } from '../util/sniffImageMimeType';
|
||||||
import { isValidE164 } from '../util/isValidE164';
|
import { isValidE164 } from '../util/isValidE164';
|
||||||
import type { MIMEType } from '../types/MIME';
|
import type { MIMEType } from '../types/MIME';
|
||||||
import { IMAGE_JPEG, IMAGE_GIF, IMAGE_WEBP } from '../types/MIME';
|
import { IMAGE_JPEG, IMAGE_GIF, IMAGE_WEBP } from '../types/MIME';
|
||||||
import { UUID, UUIDKind, isValidUuid } from '../types/UUID';
|
import { UUID, isValidUuid, UUIDKind } from '../types/UUID';
|
||||||
import type { UUIDStringType } from '../types/UUID';
|
import type { UUIDStringType } from '../types/UUID';
|
||||||
import { deriveAccessKey, decryptProfileName, decryptProfile } from '../Crypto';
|
import { deriveAccessKey, decryptProfileName, decryptProfile } from '../Crypto';
|
||||||
import * as Bytes from '../Bytes';
|
import * as Bytes from '../Bytes';
|
||||||
|
@ -582,8 +582,13 @@ export class ConversationModel extends window.Backbone
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ourUuid = window.textsecure.storage.user
|
||||||
|
.getCheckedUuid(UUIDKind.ACI)
|
||||||
|
.toString();
|
||||||
|
|
||||||
return window.Signal.Groups.buildDeletePendingAdminApprovalMemberChange({
|
return window.Signal.Groups.buildDeletePendingAdminApprovalMemberChange({
|
||||||
group: this.attributes,
|
group: this.attributes,
|
||||||
|
ourUuid,
|
||||||
uuid,
|
uuid,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -660,10 +665,19 @@ export class ConversationModel extends window.Backbone
|
||||||
this.get('announcementsOnly') &&
|
this.get('announcementsOnly') &&
|
||||||
!toRequest.get('capabilities')?.announcementGroup
|
!toRequest.get('capabilities')?.announcementGroup
|
||||||
) {
|
) {
|
||||||
log.warn(`addMember/${idLog}: ${conversationId} needs to upgrade.`);
|
log.warn(
|
||||||
|
`addMember/${idLog}: ${toRequest.idForLogging()} needs to upgrade.`
|
||||||
|
);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const uuid = toRequest.get('uuid');
|
||||||
|
if (!uuid) {
|
||||||
|
throw new Error(
|
||||||
|
`addMember/${idLog}: ${toRequest.idForLogging()} is missing a uuid!`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// We need the user's profileKeyCredential, which requires a roundtrip with the
|
// We need the user's profileKeyCredential, which requires a roundtrip with the
|
||||||
// server, and most definitely their profileKey. A getProfiles() call will
|
// server, and most definitely their profileKey. A getProfiles() call will
|
||||||
// ensure that we have as much as we can get with the data we have.
|
// ensure that we have as much as we can get with the data we have.
|
||||||
|
@ -691,6 +705,7 @@ export class ConversationModel extends window.Backbone
|
||||||
group: this.attributes,
|
group: this.attributes,
|
||||||
profileKeyCredentialBase64,
|
profileKeyCredentialBase64,
|
||||||
serverPublicParamsBase64: window.getServerPublicParams(),
|
serverPublicParamsBase64: window.getServerPublicParams(),
|
||||||
|
uuid,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -769,8 +784,13 @@ export class ConversationModel extends window.Backbone
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ourUuid = window.textsecure.storage.user
|
||||||
|
.getCheckedUuid(UUIDKind.ACI)
|
||||||
|
.toString();
|
||||||
|
|
||||||
return window.Signal.Groups.buildDeleteMemberChange({
|
return window.Signal.Groups.buildDeleteMemberChange({
|
||||||
group: this.attributes,
|
group: this.attributes,
|
||||||
|
ourUuid,
|
||||||
uuid,
|
uuid,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -2260,12 +2280,7 @@ export class ConversationModel extends window.Backbone
|
||||||
name: 'addMembersV2',
|
name: 'addMembersV2',
|
||||||
createGroupChange: () =>
|
createGroupChange: () =>
|
||||||
window.Signal.Groups.buildAddMembersChange(
|
window.Signal.Groups.buildAddMembersChange(
|
||||||
{
|
this.attributes,
|
||||||
id: this.id,
|
|
||||||
publicParams: this.get('publicParams'),
|
|
||||||
revision: this.get('revision'),
|
|
||||||
secretParams: this.get('secretParams'),
|
|
||||||
},
|
|
||||||
conversationIds
|
conversationIds
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
|
@ -40,7 +40,7 @@ const mapStateToProps = (state: StateType): PropsDataType => {
|
||||||
areWeAdmin,
|
areWeAdmin,
|
||||||
badges: getBadgesSelector(state)(contact.badges),
|
badges: getBadgesSelector(state)(contact.badges),
|
||||||
contact,
|
contact,
|
||||||
conversationId,
|
conversation: currentConversation,
|
||||||
i18n: getIntl(state),
|
i18n: getIntl(state),
|
||||||
isAdmin,
|
isAdmin,
|
||||||
isMember,
|
isMember,
|
||||||
|
|
|
@ -284,11 +284,8 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
...pick(conversation, [
|
...pick(conversation, ['unreadCount', 'isGroupV1AndDisabled']),
|
||||||
'areWeAdmin',
|
conversation,
|
||||||
'unreadCount',
|
|
||||||
'isGroupV1AndDisabled',
|
|
||||||
]),
|
|
||||||
isConversationSelected: state.conversations.selectedConversationId === id,
|
isConversationSelected: state.conversations.selectedConversationId === id,
|
||||||
isIncomingMessageRequest: Boolean(
|
isIncomingMessageRequest: Boolean(
|
||||||
conversation.messageRequestsEnabled &&
|
conversation.messageRequestsEnabled &&
|
||||||
|
|
Loading…
Reference in a new issue