Skip to content

Commit

Permalink
feat: Delete group functionality (#7248)
Browse files Browse the repository at this point in the history
  • Loading branch information
AndyLnd authored and bennycode committed Sep 9, 2019
1 parent 5bc5391 commit 2f5cc5d
Show file tree
Hide file tree
Showing 14 changed files with 231 additions and 22 deletions.
1 change: 1 addition & 0 deletions src/script/backup/BackupRepository.js
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ class BackupRepository {
.then(importedEntities => {
this.conversationRepository.updateConversations(importedEntities);
this.conversationRepository.map_connections(this.connectionRepository.connectionEntities());
this.conversationRepository.checkForDeletedConversations();
});
}

Expand Down
84 changes: 68 additions & 16 deletions src/script/conversation/ConversationRepository.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ import {BaseError} from '../error/BaseError';
import {BackendClientError} from '../error/BackendClientError';
import {showLegalHoldWarning} from '../legal-hold/LegalHoldWarning';
import * as LegalHoldEvaluator from '../legal-hold/LegalHoldEvaluator';
import {DeleteConversationMessage} from '../entity/message/DeleteConversationMessage';

// Conversation repository for all conversation interactions with the conversation service
export class ConversationRepository {
Expand Down Expand Up @@ -296,6 +297,7 @@ export class ConversationRepository {

_init_subscriptions() {
amplify.subscribe(WebAppEvents.CONVERSATION.ASSET.CANCEL, this.cancel_asset_upload.bind(this));
amplify.subscribe(WebAppEvents.CONVERSATION.DELETE, this.deleteConversationLocally.bind(this));
amplify.subscribe(WebAppEvents.CONVERSATION.EVENT_FROM_BACKEND, this.onConversationEvent.bind(this));
amplify.subscribe(WebAppEvents.CONVERSATION.MAP_CONNECTION, this.map_connection.bind(this));
amplify.subscribe(WebAppEvents.CONVERSATION.MISSED_EVENTS, this.on_missed_events.bind(this));
Expand Down Expand Up @@ -338,7 +340,7 @@ export class ConversationRepository {
conversationEntity.removed_from_conversation()
) {
this.conversation_service.delete_conversation_from_db(conversationEntity.id);
this.delete_conversation(conversationEntity.id);
this.deleteConversationFromRepository(conversationEntity.id);
}
});
}
Expand Down Expand Up @@ -418,36 +420,39 @@ export class ConversationRepository {

/**
* Get a conversation from the backend.
* @param {string} conversation_id - Conversation to be retrieved from the backend
* @param {string} conversationId - Conversation to be retrieved from the backend
* @returns {Promise} Resolve with the conversation entity
*/
fetch_conversation_by_id(conversation_id) {
if (this.fetching_conversations.hasOwnProperty(conversation_id)) {
fetch_conversation_by_id(conversationId) {
if (this.fetching_conversations.hasOwnProperty(conversationId)) {
return new Promise((resolve, reject) => {
this.fetching_conversations[conversation_id].push({reject_fn: reject, resolve_fn: resolve});
this.fetching_conversations[conversationId].push({reject_fn: reject, resolve_fn: resolve});
});
}

this.fetching_conversations[conversation_id] = [];
this.fetching_conversations[conversationId] = [];

return this.conversation_service
.get_conversation_by_id(conversation_id)
.get_conversation_by_id(conversationId)
.then(response => {
const conversationEntity = this.mapConversations(response);

this.logger.info(`Fetched conversation '${conversation_id}' from backend`);
this.logger.info(`Fetched conversation '${conversationId}' from backend`);
this.save_conversation(conversationEntity);

this.fetching_conversations[conversation_id].forEach(({resolve_fn}) => resolve_fn(conversationEntity));
delete this.fetching_conversations[conversation_id];
this.fetching_conversations[conversationId].forEach(({resolve_fn}) => resolve_fn(conversationEntity));
delete this.fetching_conversations[conversationId];

return conversationEntity;
})
.catch(() => {
.catch(({code}) => {
if (code === BackendClientError.STATUS_CODE.NOT_FOUND) {
return this.deleteConversationLocally(conversationId);
}
const error = new z.error.ConversationError(z.error.ConversationError.TYPE.CONVERSATION_NOT_FOUND);

this.fetching_conversations[conversation_id].forEach(({reject_fn}) => reject_fn(error));
delete this.fetching_conversations[conversation_id];
this.fetching_conversations[conversationId].forEach(({reject_fn}) => reject_fn(error));
delete this.fetching_conversations[conversationId];

throw error;
});
Expand Down Expand Up @@ -805,10 +810,40 @@ export class ConversationRepository {
* @param {string} conversation_id - ID of conversation to be deleted from the repository
* @returns {undefined} No return value
*/
delete_conversation(conversation_id) {
deleteConversationFromRepository(conversation_id) {
this.conversations.remove(conversationEntity => conversationEntity.id === conversation_id);
}

deleteConversation(conversationEntity) {
this.conversation_service
.deleteConversation(this.team().id, conversationEntity.id)
.then(() => {
this.deleteConversationLocally(conversationEntity.id, true);
})
.catch(() => {
amplify.publish(WebAppEvents.WARNING.MODAL, ModalsViewModel.TYPE.ACKNOWLEDGE, {
text: {
message: t('modalConversationDeleteErrorMessage', conversationEntity.name()),
title: t('modalConversationDeleteErrorHeadline'),
},
});
});
}

deleteConversationLocally(conversationId, skipNotification = false) {
const conversationEntity = this.find_conversation_by_id(conversationId);
if (this.is_active_conversation(conversationEntity)) {
const nextConversation = this.get_next_conversation(conversationEntity);
amplify.publish(WebAppEvents.CONVERSATION.SHOW, nextConversation);
}
if (!skipNotification && conversationEntity) {
const deletionMessage = new DeleteConversationMessage(conversationEntity);
amplify.publish(WebAppEvents.NOTIFICATION.NOTIFY, deletionMessage);
}
this.deleteConversationFromRepository(conversationId);
this.conversation_service.delete_conversation_from_db(conversationId);
}

/**
* Find a local conversation by ID.
* @param {string} conversation_id - ID of conversation to get
Expand Down Expand Up @@ -1084,6 +1119,20 @@ export class ConversationRepository {
});
}

async checkForDeletedConversations() {
return Promise.all(
this.conversations().map(async conversation => {
try {
await this.conversation_service.get_conversation_by_id(conversation.id);
} catch ({code}) {
if (code === BackendClientError.STATUS_CODE.NOT_FOUND) {
this.deleteConversationLocally(conversation.id, true);
}
}
}),
);
}

/**
* Maps user connections to the corresponding conversations.
* @param {Array<ConnectionEntity>} connectionEntities - Connections entities
Expand Down Expand Up @@ -1672,7 +1721,7 @@ export class ConversationRepository {

if (conversationEntity.removed_from_conversation()) {
this.conversation_service.delete_conversation_from_db(conversationEntity.id);
this.delete_conversation(conversationEntity.id);
this.deleteConversationFromRepository(conversationEntity.id);
}
}

Expand Down Expand Up @@ -3185,6 +3234,9 @@ export class ConversationRepository {
case BackendEvent.CONVERSATION.CREATE:
return this._onCreate(eventJson, eventSource);

case BackendEvent.CONVERSATION.DELETE:
return this.deleteConversationLocally(eventJson.conversation);

case BackendEvent.CONVERSATION.MEMBER_JOIN:
return this._onMemberJoin(conversationEntity, eventJson);

Expand Down Expand Up @@ -3438,7 +3490,7 @@ export class ConversationRepository {
.then(messageEntity => {
const creatorId = conversationEntity.creator;
const createdByParticipant = !!conversationEntity.participating_user_ids().find(userId => userId === creatorId);
const createdBySelfUser = this.selfUser().id === creatorId && !conversationEntity.removed_from_conversation();
const createdBySelfUser = conversationEntity.isCreatedBySelf();

const creatorIsParticipant = createdByParticipant || createdBySelfUser;
if (!creatorIsParticipant) {
Expand Down
8 changes: 8 additions & 0 deletions src/script/conversation/ConversationService.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {getLogger} from 'Util/Logger';
import {StorageSchemata} from '../storage/StorageSchemata';
import {MessageCategory} from '../message/MessageCategory';
import {search as fullTextSearch} from '../search/FullTextSearch';
import {TeamService} from '../team/TeamService';

// Conversation service for all conversation calls to the backend REST API.
export class ConversationService {
Expand Down Expand Up @@ -316,6 +317,13 @@ export class ConversationService {
});
}

deleteConversation(teamId, conversationId) {
return this.backendClient.sendRequest({
type: 'DELETE',
url: `${TeamService.URL.TEAMS}/${teamId}/conversations/${conversationId}`,
});
}

/**
* Add a service to an existing conversation.
*
Expand Down
4 changes: 4 additions & 0 deletions src/script/entity/Conversation.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,10 @@ export class Conversation {
}
});

this.isCreatedBySelf = ko.pureComputed(
() => this.selfUser().id === this.creator && !this.removed_from_conversation(),
);

this.showNotificationsEverything = ko.pureComputed(() => {
return this.notificationState() === NOTIFICATION_STATE.EVERYTHING;
});
Expand Down
36 changes: 36 additions & 0 deletions src/script/entity/message/DeleteConversationMessage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Wire
* Copyright (C) 2019 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
*/

import {t} from 'Util/LocalizerUtil';
import {BackendEvent} from '../../event/Backend';
import {SystemMessageType} from '../../message/SystemMessageType';
import {SystemMessage} from './SystemMessage';

export class DeleteConversationMessage extends SystemMessage {
constructor(conversationEntity) {
super();

this.type = BackendEvent.TEAM.DELETE;
this.system_message_type = SystemMessageType.CONVERSATION_DELETE;

this.caption = conversationEntity
? t('notificationConversationDeletedNamed', conversationEntity.name())
: t('notificationConversationDeleted');
}
}
1 change: 1 addition & 0 deletions src/script/event/WebApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export const WebAppEvents = {
},
CREATE_GROUP: 'wire.webapp.conversation.create_group',
DEBUG: 'wire.webapp.conversation.debug',
DELETE: 'wire.webapp.conversation.delete',
DETAIL_VIEW: {
SHOW: 'wire.webapp.conversation.detail_view.show',
},
Expand Down
1 change: 1 addition & 0 deletions src/script/message/SystemMessageType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export enum SystemMessageType {
CONNECTION_CONNECTED = 'connected',
CONNECTION_REQUEST = 'connecting',
CONVERSATION_CREATE = 'created-group',
CONVERSATION_DELETE = 'deleted-group',
CONVERSATION_MESSAGE_TIMER_UPDATE = 'message-timer-update',
CONVERSATION_RECEIPT_MODE_UPDATE = 'receipt-mode-update',
CONVERSATION_RENAME = 'rename',
Expand Down
8 changes: 7 additions & 1 deletion src/script/notification/NotificationRepository.js
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,9 @@ export class NotificationRepository {
case SystemMessageType.CONVERSATION_MESSAGE_TIMER_UPDATE: {
return createBodyMessageTimerUpdate(messageEntity);
}
case SystemMessageType.CONVERSATION_DELETE: {
return messageEntity.caption;
}
}
}

Expand Down Expand Up @@ -628,7 +631,10 @@ export class NotificationRepository {
* @returns {string} ID of conversation
*/
_getConversationId(connectionEntity, conversationEntity) {
return connectionEntity ? connectionEntity.conversationId : conversationEntity.id;
if (connectionEntity) {
return connectionEntity.conversationId;
}
return conversationEntity && conversationEntity.id;
}

/**
Expand Down
8 changes: 6 additions & 2 deletions src/script/team/TeamRepository.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,8 @@ export class TeamRepository {
this.logger.info(`»» Team Event: '${type}' (Source: ${source})`, logObject);

switch (type) {
case BackendEvent.TEAM.CONVERSATION_CREATE:
case BackendEvent.TEAM.CONVERSATION_DELETE: {
this._onUnhandled(eventJson);
this._onDeleteConversation(eventJson);
break;
}
case BackendEvent.TEAM.DELETE: {
Expand All @@ -170,6 +169,7 @@ export class TeamRepository {
this._onUpdate(eventJson);
break;
}
case BackendEvent.TEAM.CONVERSATION_CREATE:
default: {
this._onUnhandled(eventJson);
}
Expand Down Expand Up @@ -257,6 +257,10 @@ export class TeamRepository {
}
}

_onDeleteConversation({data: {conv: conversationId}}) {
amplify.publish(WebAppEvents.CONVERSATION.DELETE, conversationId);
}

_onMemberJoin(eventJson) {
const {
data: {user: userId},
Expand Down
19 changes: 19 additions & 0 deletions src/script/view_model/ActionsViewModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,25 @@ z.viewModel.ActionsViewModel = class ActionsViewModel {
}
}

deleteConversation(conversationEntity) {
if (conversationEntity && conversationEntity.isCreatedBySelf()) {
return new Promise(() => {
amplify.publish(WebAppEvents.WARNING.MODAL, ModalsViewModel.TYPE.CONFIRM, {
primaryAction: {
action: () => {
return this.conversationRepository.deleteConversation(conversationEntity, true);
},
text: t('modalConversationDeleteGroupAction'),
},
text: {
message: t('modalConversationDeleteGroupMessage'),
title: t('modalConversationDeleteGroupHeadline', conversationEntity.display_name()),
},
});
});
}
}

open1to1Conversation(userEntity) {
if (userEntity) {
return this.conversationRepository
Expand Down
4 changes: 3 additions & 1 deletion src/script/view_model/ContentViewModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -278,8 +278,10 @@ export class ContentViewModel {
if (isStateRequests) {
this.switchContent(ContentViewModel.STATE.CONNECTION_REQUESTS);
}
const previousId = this.previousConversation && this.previousConversation.id;
const repoHasConversation = this.conversationRepository.conversations().some(({id}) => id === previousId);

if (this.previousConversation && !this.previousConversation.is_archived()) {
if (this.previousConversation && repoHasConversation && !this.previousConversation.is_archived()) {
return this.showConversation(this.previousConversation);
}

Expand Down
16 changes: 15 additions & 1 deletion src/script/view_model/panel/ConversationDetailsViewModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ export class ConversationDetailsViewModel extends BasePanelViewModel {
condition: () => conversationEntity.isClearable(),
item: {
click: () => this.clickToClear(),
icon: 'delete-icon',
icon: 'eraser-icon',
identifier: 'do-clear',
label: t('conversationDetailsActionClear'),
},
Expand All @@ -282,6 +282,16 @@ export class ConversationDetailsViewModel extends BasePanelViewModel {
label: t('conversationDetailsActionLeave'),
},
},
// TODO: Uncomment this when there should be support for conversation deletion
// {
// condition: () => !isSingleUserMode && this.isTeam() && conversationEntity.isCreatedBySelf(),
// item: {
// click: () => this.clickToDelete(),
// icon: 'delete-icon',
// identifier: 'do-delete',
// label: t('conversationDetailsActionDelete'),
// },
// },
];

return allMenuElements.filter(menuElement => menuElement.condition()).map(menuElement => menuElement.item);
Expand Down Expand Up @@ -363,6 +373,10 @@ export class ConversationDetailsViewModel extends BasePanelViewModel {
this.actionsViewModel.leaveConversation(this.activeConversation());
}

clickToDelete() {
this.actionsViewModel.deleteConversation(this.activeConversation());
}

clickToToggleMute() {
this.actionsViewModel.toggleMuteConversation(this.activeConversation());
}
Expand Down
Loading

0 comments on commit 2f5cc5d

Please sign in to comment.