/* * 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 React from 'react'; import {ConnectionStatus} from '@wireapp/api-client/lib/connection'; import {CONVERSATION_TYPE} from '@wireapp/api-client/lib/conversation'; import {ClientMLSError, ClientMLSErrorLabel} from '@wireapp/core/lib/messagingProtocols/mls'; import {amplify} from 'amplify'; import {container} from 'tsyringe'; import {WebAppEvents} from '@wireapp/webapp-events'; import * as Icon from 'Components/Icon'; import {PrimaryModal} from 'Components/Modals/PrimaryModal'; import {listCSS} from 'Components/panel/PanelActions/PanelActions.styles'; import {ConversationState} from 'src/script/conversation/ConversationState'; import {SidebarTabs, useSidebarStore} from 'src/script/page/LeftSidebar/panels/Conversations/useSidebarStore'; import {TeamState} from 'src/script/team/TeamState'; import {useKoSubscribableChildren} from 'Util/ComponentUtil'; import {t} from 'Util/LocalizerUtil'; import {matchQualifiedIds} from 'Util/QualifiedId'; import type {MenuItem} from './PanelActions'; import {PanelActions} from './PanelActions'; import {SingleAction} from './SingleAction/SingleAction'; import {ACCESS_STATE} from '../../conversation/AccessState'; import type {ConversationRoleRepository} from '../../conversation/ConversationRoleRepository'; import {Conversation} from '../../entity/Conversation'; import type {User} from '../../entity/User'; import type {ActionsViewModel} from '../../view_model/ActionsViewModel'; export enum Actions { ACCEPT_REQUEST = 'UserActions.ACCEPT_REQUEST', BLOCK = 'UserActions.BLOCK', CANCEL_REQUEST = 'UserActions.CANCEL_REQUEST', IGNORE_REQUEST = 'UserActions.IGNORE_REQUEST', LEAVE = 'UserActions.LEAVE', OPEN_CONVERSATION = 'UserActions.OPEN_CONVERSATION', START_CONVERSATION = 'UserActions.START_CONVERSATION', OPEN_PROFILE = 'UserActions.OPEN_PROFILE', REMOVE = 'UserActions.REMOVE', SEND_REQUEST = 'UserActions.SEND_REQUEST', UNBLOCK = 'UserActions.UNBLOCK', } export const ActionIdentifier = { [Actions.ACCEPT_REQUEST]: 'do-accept-request', [Actions.BLOCK]: 'do-block', [Actions.CANCEL_REQUEST]: 'do-cancel-request', [Actions.IGNORE_REQUEST]: 'do-ignore-request', [Actions.LEAVE]: 'do-leave', [Actions.OPEN_CONVERSATION]: 'go-conversation', [Actions.START_CONVERSATION]: 'start-conversation', [Actions.OPEN_PROFILE]: 'go-profile', [Actions.REMOVE]: 'do-remove', [Actions.SEND_REQUEST]: 'do-send-request', [Actions.UNBLOCK]: 'do-unblock', }; export interface UserActionsProps { actionsViewModel: ActionsViewModel; conversation?: Conversation; conversationRoleRepository?: ConversationRoleRepository; isSelfActivated: boolean; onAction: (action: Actions) => void; selfUser: User; user: User; isModal?: boolean; teamState?: TeamState; conversationState?: ConversationState; } function createPlaceholder1to1Conversation(user: User, selfUser: User) { const userConnection = user.connection(); if (!userConnection) { throw new Error(`There's no connection with user ${user.qualifiedId.id}.`); } const {id, domain} = userConnection.conversationId; const conversation = new Conversation(id, domain); conversation.name(user.name()); conversation.selfUser(selfUser); conversation.type(CONVERSATION_TYPE.CONNECT); conversation.participating_user_ids([user.qualifiedId]); conversation.participating_user_ets([user]); conversation.accessState(ACCESS_STATE.PERSONAL.ONE2ONE); conversation.last_event_timestamp(Date.now()); conversation.connection(userConnection); return conversation; } const UserActions: React.FC = ({ actionsViewModel, isSelfActivated, user, conversation, onAction, conversationRoleRepository, selfUser, isModal = false, teamState = container.resolve(TeamState), conversationState = container.resolve(ConversationState), }) => { const { isAvailable, isBlocked, isCanceled, isRequest, isTemporaryGuest, isUnknown, isConnected, isOutgoingRequest, isIncomingRequest, } = useKoSubscribableChildren(user, [ 'isAvailable', 'isTemporaryGuest', 'isBlocked', 'isOutgoingRequest', 'isIncomingRequest', 'isRequest', 'isCanceled', 'isUnknown', 'isConnected', ]); const isTeamMember = teamState.isInTeam(user); const {setCurrentTab: setCurrentSidebarTab} = useSidebarStore(); const has1to1Conversation = conversationState.has1to1ConversationWithUser(user.qualifiedId); const isNotMe = !user.isMe && isSelfActivated; const create1to1Conversation = async (userEntity: User, showConversation: boolean): Promise => { const conversationEntity = await actionsViewModel.getOrCreate1to1Conversation(userEntity); if (showConversation) { setCurrentSidebarTab(SidebarTabs.RECENT); actionsViewModel.open1to1Conversation(conversationEntity); } }; const openSelfProfile: MenuItem | undefined = user.isMe ? { click: () => { amplify.publish(WebAppEvents.PREFERENCES.MANAGE_ACCOUNT); onAction(Actions.OPEN_PROFILE); }, Icon: Icon.ProfileIcon, identifier: ActionIdentifier[Actions.OPEN_PROFILE], label: t('groupParticipantActionSelfProfile'), } : undefined; const leaveConversation: MenuItem | undefined = user.isMe && isSelfActivated && conversation?.isGroup() && !conversation.isSelfUserRemoved() && conversationRoleRepository?.canLeaveGroup(conversation) ? { click: async () => { await actionsViewModel.leaveConversation(conversation); onAction(Actions.LEAVE); }, Icon: Icon.LeaveIcon, identifier: ActionIdentifier[Actions.LEAVE], label: t('groupParticipantActionLeave'), } : undefined; const open1To1Conversation: MenuItem | undefined = isNotMe && isAvailable && (isConnected || isTeamMember) && has1to1Conversation ? { click: async () => { await create1to1Conversation(user, true); onAction(Actions.OPEN_CONVERSATION); }, Icon: Icon.MessageIcon, identifier: ActionIdentifier[Actions.OPEN_CONVERSATION], label: t('groupParticipantActionOpenConversation'), } : undefined; const start1To1Conversation: MenuItem | undefined = isNotMe && isAvailable && (isConnected || isTeamMember) && !has1to1Conversation ? { click: async () => { try { await create1to1Conversation(user, true); onAction(Actions.START_CONVERSATION); } catch (error) { if (error instanceof ClientMLSError && error.label === ClientMLSErrorLabel.NO_KEY_PACKAGES_AVAILABLE) { return PrimaryModal.show(PrimaryModal.type.ACKNOWLEDGE, { text: { title: t('modal1To1ConversationCreateErrorNoKeyPackagesHeadline'), htmlMessage: t('modal1To1ConversationCreateErrorNoKeyPackagesMessage', user.name()), }, }); } throw error; } }, Icon: Icon.MessageIcon, identifier: ActionIdentifier[Actions.START_CONVERSATION], label: t('groupParticipantActionStartConversation'), } : undefined; const acceptConnectionRequest: MenuItem | undefined = isNotMe && isAvailable && isIncomingRequest ? { click: async () => { await actionsViewModel.acceptConnectionRequest(user); await create1to1Conversation(user, true); onAction(Actions.ACCEPT_REQUEST); }, Icon: Icon.CheckIcon, identifier: ActionIdentifier[Actions.ACCEPT_REQUEST], label: t('groupParticipantActionIncomingRequest'), } : undefined; const ignoreConnectionRequest: MenuItem | undefined = isNotMe && isIncomingRequest ? { click: async () => { await actionsViewModel.ignoreConnectionRequest(user); onAction(Actions.IGNORE_REQUEST); }, Icon: Icon.CloseIcon, identifier: ActionIdentifier[Actions.IGNORE_REQUEST], label: t('groupParticipantActionIgnoreRequest'), } : undefined; const cancelConnectionRequest: MenuItem | undefined = isNotMe && isOutgoingRequest ? { click: async () => { await actionsViewModel.cancelConnectionRequest(user); await create1to1Conversation(user, false); onAction(Actions.CANCEL_REQUEST); }, Icon: Icon.UndoIcon, identifier: ActionIdentifier[Actions.CANCEL_REQUEST], label: t('groupParticipantActionCancelRequest'), } : undefined; const isNotConnectedUser = isCanceled || isUnknown; const canConnect = !isTeamMember && !isTemporaryGuest; const sendConnectionRequest: MenuItem | undefined = isNotMe && isAvailable && isNotConnectedUser && canConnect ? { click: async () => { const connectionData = await actionsViewModel.sendConnectionRequest(user); if (!connectionData) { // Sending the connection failed, there is nothing more to do return; } const {connectionStatus, conversationId} = connectionData; // If connection's state is SENT, we create a local 1:1 conversation that will act as a placeholder // before the other user has accepted the request. const connectionConversation = connectionStatus === ConnectionStatus.SENT ? createPlaceholder1to1Conversation(user, selfUser) : await actionsViewModel.getConversationById(conversationId); const savedConversation = await actionsViewModel.saveConversation(connectionConversation); if (!conversation) { // Only open the new conversation if we aren't currently in a conversation context await actionsViewModel.open1to1Conversation(savedConversation); } setCurrentSidebarTab(SidebarTabs.RECENT); onAction(Actions.SEND_REQUEST); }, Icon: Icon.PlusIcon, identifier: ActionIdentifier[Actions.SEND_REQUEST], label: t('groupParticipantActionSendRequest'), } : undefined; const blockUser: MenuItem | undefined = isNotMe && isAvailable && (isConnected || isRequest) ? { click: async () => { await actionsViewModel.blockUser(user); await create1to1Conversation(user, false); onAction(Actions.BLOCK); }, Icon: Icon.BlockIcon, identifier: ActionIdentifier[Actions.BLOCK], label: t('groupParticipantActionBlock'), } : undefined; const unblockUser: MenuItem | undefined = isNotMe && isAvailable && isBlocked ? { click: async () => { await actionsViewModel.unblockUser(user); await create1to1Conversation(user, !conversation); onAction(Actions.UNBLOCK); }, Icon: Icon.BlockIcon, identifier: ActionIdentifier[Actions.UNBLOCK], label: t('groupParticipantActionUnblock'), } : undefined; const removeUserFromConversation: MenuItem | undefined = isNotMe && conversation && !conversation.isSelfUserRemoved() && conversation.participating_user_ids().some(userId => matchQualifiedIds(userId, user)) && conversationRoleRepository?.canRemoveParticipants(conversation) ? { click: async () => { await actionsViewModel.removeFromConversation(conversation, user); onAction(Actions.REMOVE); }, Icon: Icon.MinusIcon, identifier: 'do-remove', label: t('groupParticipantActionRemove'), } : undefined; const items = [ openSelfProfile, leaveConversation, open1To1Conversation, start1To1Conversation, acceptConnectionRequest, ignoreConnectionRequest, cancelConnectionRequest, sendConnectionRequest, blockUser, unblockUser, removeUserFromConversation, ].filter((item): item is MenuItem => !!item); return items.length === 1 && isModal ? ( ) : (
); }; export {UserActions};