add admin dialog to warn him about leaving for v2 closed group

pull/1424/head
Audric Ackermann 4 years ago
parent d43ae09eb0
commit cbd0e63641
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4

@ -1735,11 +1735,19 @@
"androidKey": "conversation__menu_leave_group",
"ignoreCase": true
},
"leaveAndRemoveForEveryone": {
"message": "Leave Group and remove for everyone",
"description": "Button action confirmation that the user can click to leave the group as an admin"
},
"leaveGroupConfirmation": {
"message": "Are you sure you want to leave this group?",
"description": "Confirmation dialog text that tells the user what will happen if they leave the group.",
"androidKey": "activity_home_leave_group_dialog_message"
},
"leaveGroupConfirmationAdmin": {
"message": "As you are the admin of this group, if you leave it it will be removed for every current members. Are you sure you want to leave this group?",
"description": "Confirmation dialog text that tells the user what will happen if they leave the group as admin."
},
"noContactsForGroup": {
"message": "You don't have any contacts yet",
"androidKey": "activity_create_closed_group_empty_state_message"

@ -167,6 +167,8 @@
<script type='text/javascript' src='js/views/create_group_dialog_view.js'></script>
<script type='text/javascript' src='js/views/edit_profile_dialog_view.js'></script>
<script type='text/javascript' src='js/views/invite_contacts_dialog_view.js'></script>
<script type='text/javascript' src='js/views/admin_leave_closed_group_dialog_view.js'></script>
<script type='text/javascript' src='js/views/moderators_add_dialog_view.js'></script>
<script type='text/javascript' src='js/views/moderators_remove_dialog_view.js'></script>
<script type='text/javascript' src='js/views/user_details_dialog_view.js'></script>

@ -171,6 +171,8 @@
<script type='text/javascript' src='js/views/create_group_dialog_view.js'></script>
<script type='text/javascript' src='js/views/edit_profile_dialog_view.js'></script>
<script type='text/javascript' src='js/views/invite_contacts_dialog_view.js'></script>
<script type='text/javascript' src='js/views/admin_leave_closed_group_dialog_view.js'></script>
<script type='text/javascript' src='js/views/moderators_add_dialog_view.js'></script>
<script type='text/javascript' src='js/views/moderators_remove_dialog_view.js'></script>
<script type='text/javascript' src='js/views/user_details_dialog_view.js'></script>

@ -1172,7 +1172,6 @@
'sent',
window.DataMessageReceiver.handleMessageEvent
);
messageReceiver.addEventListener('empty', onEmpty);
messageReceiver.addEventListener('reconnect', onReconnect);
messageReceiver.addEventListener('configuration', onConfiguration);
// messageReceiver.addEventListener('typing', onTyping);
@ -1250,15 +1249,6 @@
window.readyForUpdates();
// let interval = setInterval(() => {
// const view = window.owsDesktopApp.appView;
// if (view) {
// clearInterval(interval);
// interval = null;
// view.onEmpty();
// }
// }, 500);
Whisper.Notifications.enable();
}
function onReconnect() {

@ -523,6 +523,9 @@
onUnblockContact: () => this.unblock(),
onCopyPublicKey: () => this.copyPublicKey(),
onDeleteContact: () => this.deleteContact(),
onLeaveGroup: () => {
window.Whisper.events.trigger('leaveGroup', this);
},
onDeleteMessages: () => this.deleteMessages(),
onInviteContacts: () => {
window.Whisper.events.trigger('inviteContacts', this);

@ -69,6 +69,9 @@ const {
const {
InviteContactsDialog,
} = require('../../ts/components/conversation/InviteContactsDialog');
const {
AdminLeaveClosedGroupDialog,
} = require('../../ts/components/conversation/AdminLeaveClosedGroupDialog');
const {
AddModeratorsDialog,
@ -237,6 +240,7 @@ exports.setup = (options = {}) => {
UpdateGroupNameDialog,
UpdateGroupMembersDialog,
InviteContactsDialog,
AdminLeaveClosedGroupDialog,
AddModeratorsDialog,
RemoveModeratorsDialog,
GroupInvitation,

@ -0,0 +1,43 @@
/* global Whisper */
// eslint-disable-next-line func-names
(function() {
'use strict';
window.Whisper = window.Whisper || {};
Whisper.AdminLeaveClosedGroupDialog = Whisper.View.extend({
className: 'loki-dialog modal',
initialize(convo) {
this.close = this.close.bind(this);
this.submit = this.submit.bind(this);
this.theme = convo.theme;
this.groupName = convo.get('name');
this.convo = convo;
this.$el.focus();
this.render();
},
render() {
const view = new Whisper.ReactWrapperView({
className: 'admin-leave-closed-group',
Component: window.Signal.Components.AdminLeaveClosedGroupDialog,
props: {
onSubmit: this.submit,
onClose: this.close,
groupName: this.groupName,
theme: this.theme,
},
});
this.$el.append(view.el);
return this;
},
close() {
this.remove();
},
submit() {
this.convo.leaveGroup();
},
});
})();

@ -90,18 +90,6 @@
this.closeStandalone();
},
openInbox(options = {}) {
// The inbox can be created before the 'empty' event fires or afterwards. If
// before, it's straightforward: the onEmpty() handler below updates the
// view directly, and we're in good shape. If we create the inbox late, we
// need to be sure that the current value of initialLoadComplete is provided
// so its loading screen doesn't stick around forever.
// Two primary techniques at play for this situation:
// - background.js has X number of openInbox() calls,
// and passes initalLoadComplete directly via the options parameter.
// - in other situations openInbox() will be called with no options. So this
// view keeps track of whether onEmpty() has ever been called with
// this.initialLoadComplete. An example of this: on a phone-pairing setup.
_.defaults(options, { initialLoadComplete: this.initialLoadComplete });
if (!this.inboxView) {
@ -125,7 +113,6 @@
window.focus(); // FIXME
return Promise.resolve();
},
onEmpty() {},
showEditProfileDialog(options) {
// eslint-disable-next-line no-param-reassign
options.theme = this.getThemeObject();
@ -211,14 +198,23 @@
const title = i18n('leaveGroup');
const message = i18n('leaveGroupConfirmation');
window.confirmationDialog({
title,
message,
resolve: () =>
window.getConversationController().deleteContact(groupConvo.id),
theme: this.getThemeObject(),
});
const ourPK = window.textsecure.storage.user.getNumber();
const isAdmin = (groupConvo.get('groupAdmins') || []).includes(ourPK);
const isClosedGroup = groupConvo.get('is_medium_group') || false;
// if this is not a closed group, or we are not admin, we can just show a confirmation dialog
if (!isClosedGroup || (isClosedGroup && !isAdmin)) {
window.confirmationDialog({
title,
message,
resolve: () =>
window.getConversationController().deleteContact(groupConvo.id),
theme: this.getThemeObject(),
});
} else {
// we are the admin on a closed group. We have to warn the user about the group Deletion
this.showAdminLeaveClosedGroupDialog(groupConvo);
}
},
showInviteContactsDialog(groupConvo) {
// eslint-disable-next-line no-param-reassign
@ -226,6 +222,13 @@
const dialog = new Whisper.InviteContactsDialogView(groupConvo);
this.el.append(dialog.el);
},
showAdminLeaveClosedGroupDialog(groupConvo) {
// eslint-disable-next-line no-param-reassign
groupConvo.theme = this.getThemeObject();
const dialog = new Whisper.AdminLeaveClosedGroupDialog(groupConvo);
this.el.append(dialog.el);
},
showAddModeratorsDialog(groupConvo) {
// eslint-disable-next-line no-param-reassign
groupConvo.theme = this.getThemeObject();

@ -209,6 +209,8 @@
<script type="text/javascript" src="../js/views/create_group_dialog_view.js"></script>
<script type="text/javascript" src="../js/views/edit_profile_dialog_view.js"></script>
<script type="text/javascript" src="../js/views/invite_contacts_dialog_view.js"></script>
<script type='text/javascript' src='../js/views/admin_leave_closed_group_dialog_view.js'></script>
<script type="text/javascript" src="../js/views/moderators_add_dialog_view.js"></script>
<script type="text/javascript" src="../js/views/moderators_remove_dialog_view.js"></script>
<script type="text/javascript" src="../js/views/user_details_dialog_view.js"></script>

@ -64,6 +64,7 @@ type PropsHousekeeping = {
onClick?: (id: string) => void;
onDeleteMessages?: () => void;
onDeleteContact?: () => void;
onLeaveGroup?: () => void;
onBlockContact?: () => void;
onCopyPublicKey?: () => void;
onUnblockContact?: () => void;

@ -0,0 +1,58 @@
import React from 'react';
import { SessionModal } from '../session/SessionModal';
import { SessionButton, SessionButtonColor } from '../session/SessionButton';
import { DefaultTheme } from 'styled-components';
interface Props {
groupName: string;
onSubmit: any;
onClose: any;
theme: DefaultTheme;
}
class AdminLeaveClosedGroupDialogInner extends React.Component<Props> {
constructor(props: any) {
super(props);
this.closeDialog = this.closeDialog.bind(this);
this.onClickOK = this.onClickOK.bind(this);
}
public render() {
const titleText = `${window.i18n('leaveGroup')} ${this.props.groupName}`;
const warningAsAdmin = `${window.i18n('leaveGroupConfirmationAdmin')}`;
const okText = window.i18n('leaveAndRemoveForEveryone');
return (
<SessionModal
title={titleText}
onOk={() => null}
onClose={this.closeDialog}
theme={this.props.theme}
>
<div className="spacer-lg" />
<p>{warningAsAdmin}</p>
<div className="session-modal__button-group">
<SessionButton
text={okText}
onClick={this.onClickOK}
buttonColor={SessionButtonColor.Danger}
/>
</div>
</SessionModal>
);
}
private onClickOK() {
this.props.onSubmit();
this.closeDialog();
}
private closeDialog() {
this.props.onClose();
}
}
export const AdminLeaveClosedGroupDialog = AdminLeaveClosedGroupDialogInner;

@ -1,14 +1,9 @@
import React, { useState } from 'react';
import React from 'react';
import { SessionModal } from './SessionModal';
import { SessionButton, SessionButtonColor } from './SessionButton';
import { DefaultTheme, withTheme } from 'styled-components';
import {
SessionIcon,
SessionIconButton,
SessionIconSize,
SessionIconType,
} from './icon';
import { SessionIcon, SessionIconSize, SessionIconType } from './icon';
type Props = {
onClose: any;

@ -309,10 +309,6 @@ class SessionRightPanel extends React.Component<Props, State> {
{window.i18n('groupMembers')}
</div>
)}
{/*<div className="group-settings-item">
{window.i18n('notifications')}
</div>
*/}
{hasDisappearingMessages && (
<SessionDropdown
@ -330,6 +326,7 @@ class SessionRightPanel extends React.Component<Props, State> {
<SessionButton
text={leaveGroupString}
buttonColor={SessionButtonColor.Danger}
disabled={isKickedFromGroup}
buttonType={SessionButtonType.SquareOutline}
onClick={onLeaveGroup}
/>

@ -24,6 +24,7 @@ export type PropsContextConversationItem = {
onDeleteMessages?: () => void;
onDeleteContact?: () => void;
onLeaveGroup?: () => void;
onBlockContact?: () => void;
onCopyPublicKey?: () => void;
onUnblockContact?: () => void;
@ -51,6 +52,7 @@ export const ConversationListItemContextMenu = (
onCopyPublicKey,
onUnblockContact,
onInviteContacts,
onLeaveGroup,
} = props;
return (
@ -104,7 +106,7 @@ export const ConversationListItemContextMenu = (
type === 'group',
isPublic,
isRss,
onDeleteContact,
onLeaveGroup,
window.i18n
)}
</Menu>

@ -85,8 +85,7 @@ async function decryptForClosedGroupV2(
keyIndex++;
} catch (e) {
window.log.info(
`Failed to decrypt closed group v2 with key index ${keyIndex}`,
e
`Failed to decrypt closed group v2 with key index ${keyIndex}. We have ${encryptionKeyPairs.length} keys to try left.`
);
}
} while (encryptionKeyPairs.length > 0);

@ -486,7 +486,7 @@ export function initIncomingMessage(data: MessageCreationData): MessageModel {
messageGroupId && messageGroupId.length > 0 ? messageGroupId : null;
if (groupId) {
groupId = groupId.replace(PubKey.PREFIX_GROUP_TEXTSECURE, '');
groupId = PubKey.removeTextSecurePrefixIfNeeded(groupId);
}
const messageData: any = {
@ -658,10 +658,8 @@ export async function handleMessageEvent(event: MessageEvent): Promise<void> {
// - group.id if it is a group message
if (isGroupMessage) {
// remove the prefix from the source object so this is correct for all other
message.group.id = message.group.id.replace(
PubKey.PREFIX_GROUP_TEXTSECURE,
''
);
message.group.id = PubKey.removeTextSecurePrefixIfNeeded(message.group.id);
conversationId = message.group.id;
}

@ -8,5 +8,5 @@ export abstract class ContentMessage extends Message {
}
public abstract ttl(): number;
protected abstract contentProto(): SignalService.Content;
public abstract contentProto(): SignalService.Content;
}

@ -7,7 +7,7 @@ export class EndSessionMessage extends SessionRequestMessage {
return Constants.TTL_DEFAULT.END_SESSION_MESSAGE;
}
protected contentProto(): SignalService.Content {
public contentProto(): SignalService.Content {
const dataMessage = new SignalService.DataMessage({
body: 'TERMINATE',
flags: SignalService.DataMessage.Flags.END_SESSION,

@ -22,7 +22,7 @@ export class SessionEstablishedMessage extends ContentMessage {
return Constants.TTL_DEFAULT.SESSION_ESTABLISHED;
}
protected contentProto(): SignalService.Content {
public contentProto(): SignalService.Content {
const nullMessage = new SignalService.NullMessage({});
nullMessage.padding = this.padding;

@ -39,11 +39,7 @@ export class SessionRequestMessage extends ContentMessage {
return Constants.TTL_DEFAULT.SESSION_REQUEST;
}
protected getPreKeyBundleMessage(): SignalService.PreKeyBundleMessage {
return new SignalService.PreKeyBundleMessage(this.preKeyBundle);
}
protected contentProto(): SignalService.Content {
public contentProto(): SignalService.Content {
const nullMessage = new SignalService.NullMessage({});
const preKeyBundleMessage = this.getPreKeyBundleMessage();
nullMessage.padding = this.padding;
@ -52,4 +48,8 @@ export class SessionRequestMessage extends ContentMessage {
preKeyBundleMessage,
});
}
protected getPreKeyBundleMessage(): SignalService.PreKeyBundleMessage {
return new SignalService.PreKeyBundleMessage(this.preKeyBundle);
}
}

@ -29,7 +29,7 @@ export class TypingMessage extends ContentMessage {
return Constants.TTL_DEFAULT.TYPING_MESSAGE;
}
protected contentProto(): SignalService.Content {
public contentProto(): SignalService.Content {
return new SignalService.Content({
typingMessage: this.typingProto(),
});

@ -4,7 +4,7 @@ import { SignalService } from '../../../../../protobuf';
export abstract class DataMessage extends ContentMessage {
public abstract dataProto(): SignalService.DataMessage;
protected contentProto(): SignalService.Content {
public contentProto(): SignalService.Content {
return new SignalService.Content({
dataMessage: this.dataProto(),
});

@ -35,12 +35,13 @@ export class ExpirationTimerUpdateMessage extends DataMessage {
data.flags = SignalService.DataMessage.Flags.EXPIRATION_TIMER_UPDATE;
// FIXME we shouldn't need this once android recieving refactor is done.
// the envelope stores the groupId for a closed group v2 already.
if (this.groupId) {
const groupMessage = new SignalService.GroupContext();
let groupIdWithPrefix: string = this.groupId.key;
if (!this.groupId.key.startsWith(PubKey.PREFIX_GROUP_TEXTSECURE)) {
groupIdWithPrefix = PubKey.PREFIX_GROUP_TEXTSECURE + this.groupId.key;
}
const groupIdWithPrefix = PubKey.addTextSecurePrefixIfNeeded(
this.groupId.key
);
const encoded = StringUtils.encode(groupIdWithPrefix, 'utf8');
const id = new Uint8Array(encoded);
groupMessage.id = id;

@ -3,6 +3,7 @@ import { ChatMessage } from '../ChatMessage';
import { ClosedGroupV2Message } from './ClosedGroupV2Message';
import { PubKey } from '../../../../../types';
import { Constants } from '../../../../..';
import { StringUtils } from '../../../../../utils';
interface ClosedGroupV2ChatMessageParams {
identifier?: string;
@ -28,8 +29,21 @@ export class ClosedGroupV2ChatMessage extends ClosedGroupV2Message {
}
public dataProto(): SignalService.DataMessage {
const messageProto = this.chatMessage.dataProto();
const dataProto = this.chatMessage.dataProto();
return messageProto;
if (this.groupId) {
const groupMessage = new SignalService.GroupContext();
const groupIdWithPrefix = PubKey.addTextSecurePrefixIfNeeded(
this.groupId.key
);
const encoded = StringUtils.encode(groupIdWithPrefix, 'utf8');
const id = new Uint8Array(encoded);
groupMessage.id = id;
groupMessage.type = SignalService.GroupContext.Type.DELIVER;
dataProto.group = groupMessage;
}
return dataProto;
}
}

@ -34,6 +34,13 @@ export class DeviceLinkRequestMessage extends ContentMessage {
return Constants.TTL_DEFAULT.PAIRING_REQUEST;
}
public contentProto(): SignalService.Content {
return new SignalService.Content({
pairingAuthorisation: this.getPairingAuthorisationMessage(),
dataMessage: this.getDataMessage(),
});
}
protected getDataMessage(): SignalService.DataMessage | undefined {
return undefined;
}
@ -46,11 +53,4 @@ export class DeviceLinkRequestMessage extends ContentMessage {
grantSignature: null,
});
}
protected contentProto(): SignalService.Content {
return new SignalService.Content({
pairingAuthorisation: this.getPairingAuthorisationMessage(),
dataMessage: this.getDataMessage(),
});
}
}

@ -20,7 +20,7 @@ export abstract class ReceiptMessage extends ContentMessage {
public abstract getReceiptType(): SignalService.ReceiptMessage.Type;
protected contentProto(): SignalService.Content {
public contentProto(): SignalService.Content {
return new SignalService.Content({
receiptMessage: this.receiptProto(),
});

@ -19,7 +19,7 @@ export abstract class RequestSyncMessage extends SyncMessage {
return Constants.TTL_DEFAULT.REGULAR_MESSAGE;
}
protected contentProto(): SignalService.Content {
public contentProto(): SignalService.Content {
return new SignalService.Content({
syncMessage: this.syncProto(),
});

@ -8,7 +8,7 @@ export abstract class SyncMessage extends ContentMessage {
return Constants.TTL_DEFAULT.REGULAR_MESSAGE;
}
protected contentProto(): SignalService.Content {
public contentProto(): SignalService.Content {
return new SignalService.Content({
syncMessage: this.syncProto(),
});

@ -70,11 +70,49 @@ export class PubKey {
return this.regex.test(pubkeyString);
}
public static remove05PrefixIfNeeded(recipient: string): string {
if (recipient.length === 66 && recipient.startsWith('05')) {
return recipient.substr(2);
/**
* This removes the 05 prefix from a Pubkey which have it and have a length of 66
* @param keyWithOrWithoutPrefix the key with or without the prefix
*/
public static remove05PrefixIfNeeded(keyWithOrWithoutPrefix: string): string {
if (
keyWithOrWithoutPrefix.length === 66 &&
keyWithOrWithoutPrefix.startsWith('05')
) {
return keyWithOrWithoutPrefix.substr(2);
}
return recipient;
return keyWithOrWithoutPrefix;
}
/**
* This adds the `__textsecure_group__!` prefix to a pubkey if this pubkey does not already have it
* @param keyWithOrWithoutPrefix the key to use as base
*/
public static addTextSecurePrefixIfNeeded(
keyWithOrWithoutPrefix: string | PubKey
): string {
const key =
keyWithOrWithoutPrefix instanceof PubKey
? keyWithOrWithoutPrefix.key
: keyWithOrWithoutPrefix;
if (!key.startsWith(PubKey.PREFIX_GROUP_TEXTSECURE)) {
return PubKey.PREFIX_GROUP_TEXTSECURE + key;
}
return key;
}
/**
* This removes the `__textsecure_group__!` prefix from a pubkey if this pubkey have one
* @param keyWithOrWithoutPrefix the key to use as base
*/
public static removeTextSecurePrefixIfNeeded(
keyWithOrWithoutPrefix: string | PubKey
): string {
const key =
keyWithOrWithoutPrefix instanceof PubKey
? keyWithOrWithoutPrefix.key
: keyWithOrWithoutPrefix;
return key.replace(PubKey.PREFIX_GROUP_TEXTSECURE, '');
}
public isEqual(comparator: PubKey | string) {

Loading…
Cancel
Save