Skip to content

Commit

Permalink
feat: add readOnly flag for conversation (#15759)
Browse files Browse the repository at this point in the history
* feat: add readOnly flag for conversation

* fix: fix pipeline issue

* feat:  Mark 1:1 conversation as read-only if there's no common protocol

* feat: disable audio/video call buttons if readonly conversation

* refactor: rename variable

* fix: remove test trigger

* fix: code review comments

* fix: test pipeline

* feat: rename variable and trigger conversation readonly

* fix: remove TODO comment
  • Loading branch information
arjita-mitra authored Sep 12, 2023
1 parent 164d887 commit 75860e5
Show file tree
Hide file tree
Showing 13 changed files with 199 additions and 21 deletions.
6 changes: 5 additions & 1 deletion src/i18n/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -731,9 +731,13 @@
"messageFailedToSendWillReceivePlural": "will get your message later.",
"messageFailedToSendWillReceiveSingular": "will get your message later.",
"messageReactionDetails": "{{emojiCount}} reaction, react with {{emojiName}} emoji",
"mlsConversationRecovered": "You havent used this device for a while, or an issue has occurred. Some older messages may not appear here.",
"mlsConversationRecovered": "You haven't used this device for a while, or an issue has occurred. Some older messages may not appear here.",
"mlsToggleInfo": "When this is on, conversation will use the new messaging layer security (MLS) protocol.",
"mlsToggleName": "MLS",
"selfNotSupportMLSMsgPart1": "You can't communicate with [bold]{{selfUserName}}[/bold] anymore, as your device doesn't support the suitable protocol.",
"downloadLatestMLS": "Download the latest MLS Wire version ",
"selfNotSupportMLSMsgPart2": " to call, and send messages and files again.",
"otherUserNotSupportMLSMsg": "You can't communicate with [bold]{{participantName}}[/bold] anymore, as you two now use different protocols. When [bold]{{participantName}}[/bold] gets an update, you can call and send messages and files again.",
"modalAccountCreateAction": "OK",
"modalAccountCreateHeadline": "Create an account?",
"modalAccountCreateMessage": "By creating an account you will lose the conversation history in this guest room.",
Expand Down
54 changes: 37 additions & 17 deletions src/script/components/Conversation/Conversation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {showWarningModal} from 'Components/Modals/utils/showWarningModal';
import {TitleBar} from 'Components/TitleBar';
import {CallState} from 'src/script/calling/CallState';
import {Config} from 'src/script/Config';
import {CONVERSATION_READONLY_STATE} from 'src/script/conversation/ConversationRepository';
import {useKoSubscribableChildren} from 'Util/ComponentUtil';
import {allowsAllFiles, getFileExtensionOrName, hasAllowedExtension} from 'Util/FileTypeUtil';
import {isHittingUploadLimit} from 'Util/isHittingUploadLimit';
Expand All @@ -44,6 +45,7 @@ import {safeMailOpen, safeWindowOpen} from 'Util/SanitizationUtil';
import {formatBytes, incomingCssClass, removeAnimationsClass} from 'Util/util';

import {useReadReceiptSender} from './hooks/useReadReceipt';
import {ReadOnlyConversationMessage} from './ReadOnlyConversationMessage';
import {checkFileSharingPermission} from './utils/checkFileSharingPermission';

import {ConversationState} from '../../conversation/ConversationState';
Expand Down Expand Up @@ -71,6 +73,7 @@ interface ConversationProps {
readonly userState: UserState;
openRightSidebar: (panelState: PanelState, params: RightSidebarParams, compareEntityId?: boolean) => void;
isRightSidebarOpen?: boolean;
onRefresh: () => void;
}

const CONFIG = Config.getConfig();
Expand All @@ -81,6 +84,7 @@ export const Conversation: FC<ConversationProps> = ({
userState,
openRightSidebar,
isRightSidebarOpen = false,
onRefresh,
}) => {
const messageListLogger = getLogger('ConversationList');

Expand All @@ -99,7 +103,18 @@ export const Conversation: FC<ConversationProps> = ({
'classifiedDomains',
'isFileSharingSendingEnabled',
]);
const {is1to1, isRequest} = useKoSubscribableChildren(activeConversation!, ['is1to1', 'isRequest']);
const {
is1to1,
isRequest,
readOnlyState,
display_name: displayName,
} = useKoSubscribableChildren(activeConversation!, ['is1to1', 'isRequest', 'readOnlyState', 'display_name']);
const showReadOnlyConversationMessage =
readOnlyState !== null &&
[
CONVERSATION_READONLY_STATE.READONLY_ONE_TO_ONE_OTHER_UNSUPPORTED_MLS,
CONVERSATION_READONLY_STATE.READONLY_ONE_TO_ONE_SELF_UNSUPPORTED_MLS,
].includes(readOnlyState);
const {self: selfUser} = useKoSubscribableChildren(userState, ['self']);
const {inTeam} = useKoSubscribableChildren(selfUser, ['inTeam']);

Expand Down Expand Up @@ -472,6 +487,7 @@ export const Conversation: FC<ConversationProps> = ({
callActions={mainViewModel.calling.callActions}
openRightSidebar={openRightSidebar}
isRightSidebarOpen={isRightSidebarOpen}
isReadOnlyConversation={showReadOnlyConversationMessage}
/>

{activeCalls.map(call => {
Expand Down Expand Up @@ -520,22 +536,26 @@ export const Conversation: FC<ConversationProps> = ({
setMsgElementsFocusable={setMsgElementsFocusable}
/>

<InputBar
conversationEntity={activeConversation}
conversationRepository={repositories.conversation}
eventRepository={repositories.event}
messageRepository={repositories.message}
openGiphy={openGiphy}
propertiesRepository={repositories.properties}
searchRepository={repositories.search}
storageRepository={repositories.storage}
teamState={teamState}
selfUser={selfUser}
onShiftTab={() => setMsgElementsFocusable(false)}
uploadDroppedFiles={uploadDroppedFiles}
uploadImages={uploadImages}
uploadFiles={uploadFiles}
/>
{showReadOnlyConversationMessage ? (
<ReadOnlyConversationMessage state={readOnlyState} handleMLSUpdate={onRefresh} displayName={displayName} />
) : (
<InputBar
conversationEntity={activeConversation}
conversationRepository={repositories.conversation}
eventRepository={repositories.event}
messageRepository={repositories.message}
openGiphy={openGiphy}
propertiesRepository={repositories.properties}
searchRepository={repositories.search}
storageRepository={repositories.storage}
teamState={teamState}
selfUser={selfUser}
onShiftTab={() => setMsgElementsFocusable(false)}
uploadDroppedFiles={uploadDroppedFiles}
uploadImages={uploadImages}
uploadFiles={uploadFiles}
/>
)}

<div className="conversation-loading">
<div className="icon-spinner spin accent-text"></div>
Expand Down
77 changes: 77 additions & 0 deletions src/script/components/Conversation/ReadOnlyConversationMessage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Wire
* Copyright (C) 2023 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 {FC} from 'react';

import {Link, LinkVariant} from '@wireapp/react-ui-kit';

import {Icon} from 'Components/Icon';
import {CONVERSATION_READONLY_STATE} from 'src/script/conversation/ConversationRepository';
import {t} from 'Util/LocalizerUtil';

interface ReadOnlyConversationMessageProps {
state: CONVERSATION_READONLY_STATE;
handleMLSUpdate: () => void;
displayName: string;
}
export const ReadOnlyConversationMessage: FC<ReadOnlyConversationMessageProps> = ({
state,
handleMLSUpdate,
displayName,
}) => {
const mlsCompatibilityMessage =
state === CONVERSATION_READONLY_STATE.READONLY_ONE_TO_ONE_OTHER_UNSUPPORTED_MLS
? t('otherUserNotSupportMLSMsg', displayName)
: t('selfNotSupportMLSMsgPart1', displayName);

return (
<div className="readonly-message-header readonly-message-container">
<div className="readonly-message-header-icon readonly-message-header-icon--svg">
<div>
<Icon.Info />
</div>
</div>
<div className="readonly-message-header-label" data-uie-name="element-readonly-conversation">
<span
dangerouslySetInnerHTML={{
__html: mlsCompatibilityMessage,
}}
/>
{state === CONVERSATION_READONLY_STATE.READONLY_ONE_TO_ONE_SELF_UNSUPPORTED_MLS && (
<>
<Link
css={{fontSize: 'var(--font-size-small)', marginLeft: 2, fontWeight: 600}}
onClick={handleMLSUpdate}
variant={LinkVariant.PRIMARY}
data-uie-name="do-update-mls"
>
{t('downloadLatestMLS')}
</Link>
<span
css={{marginLeft: 5}}
dangerouslySetInnerHTML={{
__html: t('selfNotSupportMLSMsgPart2'),
}}
/>
</>
)}
</div>
</div>
);
};
4 changes: 4 additions & 0 deletions src/script/components/TitleBar/TitleBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export interface TitleBarProps {
teamState: TeamState;
isRightSidebarOpen?: boolean;
callState?: CallState;
isReadOnlyConversation?: boolean;
}

export const TitleBar: React.FC<TitleBarProps> = ({
Expand All @@ -71,6 +72,7 @@ export const TitleBar: React.FC<TitleBarProps> = ({
userState = container.resolve(UserState),
callState = container.resolve(CallState),
teamState = container.resolve(TeamState),
isReadOnlyConversation = false,
}) => {
const {calling: callingRepository} = repositories;
const {
Expand Down Expand Up @@ -297,6 +299,7 @@ export const TitleBar: React.FC<TitleBarProps> = ({
showStartedCallAlert(isGroup, true);
}}
data-uie-name="do-video-call"
disabled={isReadOnlyConversation}
>
<Icon.Camera />
</button>
Expand All @@ -313,6 +316,7 @@ export const TitleBar: React.FC<TitleBarProps> = ({
showStartedCallAlert(isGroup);
}}
data-uie-name="do-call"
disabled={isReadOnlyConversation}
>
<Icon.Pickup />
</button>
Expand Down
2 changes: 2 additions & 0 deletions src/script/conversation/ConversationFilter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ describe('ConversationFilter', () => {
accessRoleV2: undefined,
access_role: undefined,
archived_state: false,
readonly_state: null,
archived_timestamp: 0,
cipher_suite: 1,
cleared_timestamp: 0,
Expand Down Expand Up @@ -92,6 +93,7 @@ describe('ConversationFilter', () => {
accessRoleV2: undefined,
access_role: CONVERSATION_LEGACY_ACCESS_ROLE.PRIVATE,
archived_state: false,
readonly_state: null,
archived_timestamp: 0,
cipher_suite: 1,
cleared_timestamp: 0,
Expand Down
23 changes: 21 additions & 2 deletions src/script/conversation/ConversationRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,11 @@ type FetchPromise = {rejectFn: (error: ConversationError) => void; resolveFn: (c
type EntityObject = {conversationEntity: Conversation; messageEntity: Message};
type IncomingEvent = ConversationEvent | ClientConversationEvent;

export enum CONVERSATION_READONLY_STATE {
READONLY_ONE_TO_ONE_SELF_UNSUPPORTED_MLS = 'READONLY_ONE_TO_ONE_SELF_UNSUPPORTED_MLS',
READONLY_ONE_TO_ONE_OTHER_UNSUPPORTED_MLS = 'READONLY_ONE_TO_ONE_OTHER_UNSUPPORTED_MLS',
}

export class ConversationRepository {
private isBlockingNotificationHandling: boolean;
private readonly conversationsWithNewEvents: Map<any, any>;
Expand Down Expand Up @@ -1381,6 +1386,14 @@ export class ConversationRepository {
}
};

private readonly markConversationReadOnly = async (
conversationEntity: Conversation,
conversationReadOnlyState: CONVERSATION_READONLY_STATE,
) => {
conversationEntity.readOnlyState(conversationReadOnlyState);
await this.saveConversationStateInDb(conversationEntity);
};

private readonly getProtocolFor1to1Conversation = async (
otherUserId: QualifiedId,
): Promise<{
Expand Down Expand Up @@ -1599,7 +1612,10 @@ export class ConversationRepository {
//if mls is not supported by the other user we do not establish the group yet
//we just mark the mls conversation as readonly and return it
if (!isMLSSupportedByTheOtherUser) {
//TODO: mark conversation as readonly
await this.markConversationReadOnly(
mlsConversation,
CONVERSATION_READONLY_STATE.READONLY_ONE_TO_ONE_OTHER_UNSUPPORTED_MLS,
);
this.logger.info(
`MLS 1:1 conversation with user ${otherUserId.id} is not supported by the other user, conversation will become readonly`,
);
Expand Down Expand Up @@ -1636,7 +1652,10 @@ export class ConversationRepository {

// If proteus is not supported by the other user we have to mark conversation as readonly
if (!doesOtherUserSupportProteus) {
//TODO: mark conversation as readonly
await this.markConversationReadOnly(
proteusConversation,
CONVERSATION_READONLY_STATE.READONLY_ONE_TO_ONE_SELF_UNSUPPORTED_MLS,
);
}

return proteusConversation;
Expand Down
7 changes: 6 additions & 1 deletion src/script/entity/Conversation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ import {ClientRepository} from '../client';
import {Config} from '../Config';
import {ConnectionEntity} from '../connection/ConnectionEntity';
import {ACCESS_STATE} from '../conversation/AccessState';
import {ConversationRepository} from '../conversation/ConversationRepository';
import {ConversationRepository, CONVERSATION_READONLY_STATE} from '../conversation/ConversationRepository';
import {isSelfConversation} from '../conversation/ConversationSelectors';
import {ConversationStatus} from '../conversation/ConversationStatus';
import {ConversationVerificationState} from '../conversation/ConversationVerificationState';
Expand Down Expand Up @@ -85,6 +85,7 @@ enum TIMESTAMP_TYPE {
export class Conversation {
private readonly teamState: TeamState;
public readonly archivedState: ko.Observable<boolean>;
public readonly readOnlyState: ko.Observable<CONVERSATION_READONLY_STATE | null>;
private readonly incomingMessages: ko.ObservableArray<Message>;
private readonly isTeam1to1: ko.PureComputed<boolean>;
public readonly last_server_timestamp: ko.Observable<number>;
Expand Down Expand Up @@ -295,6 +296,8 @@ export class Conversation {

this.call = ko.observable(null);

this.readOnlyState = ko.observable<CONVERSATION_READONLY_STATE | null>(null);

// Conversation states for view
this.notificationState = ko.pureComputed(() => {
if (!this.selfUser()) {
Expand Down Expand Up @@ -567,6 +570,7 @@ export class Conversation {
private _initSubscriptions() {
[
this.archivedState,
this.readOnlyState,
this.archivedTimestamp,
this.cleared_timestamp,
this.messageTimer,
Expand Down Expand Up @@ -1032,6 +1036,7 @@ export class Conversation {
access: this.accessModes,
access_role: this.accessRole,
archived_state: this.archivedState(),
readonly_state: this.readOnlyState(),
archived_timestamp: this.archivedTimestamp(),
cipher_suite: this.cipherSuite,
cleared_timestamp: this.cleared_timestamp(),
Expand Down
1 change: 1 addition & 0 deletions src/script/page/AppMain.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ const AppMain: FC<AppMainProps> = ({
selfUser={selfUser}
isRightSidebarOpen={!!currentState}
openRightSidebar={toggleRightSidebar}
onRefresh={app.refresh}
/>
)}

Expand Down
1 change: 1 addition & 0 deletions src/script/page/MainContent/MainContent.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ describe('Preferences', () => {
const defaultParams = {
openRightSidebar: jest.fn(),
selfUser: new User('selfUser'),
onRefresh: jest.fn(),
};

it('renders the right component according to view state', () => {
Expand Down
3 changes: 3 additions & 0 deletions src/script/page/MainContent/MainContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,15 @@ interface MainContentProps {
isRightSidebarOpen?: boolean;
selfUser: User;
conversationState?: ConversationState;
onRefresh: () => void;
}

const MainContent: FC<MainContentProps> = ({
openRightSidebar,
isRightSidebarOpen = false,
selfUser,
conversationState = container.resolve(ConversationState),
onRefresh,
}) => {
const [uploadedFile, setUploadedFile] = useState<File | null>(null);
const mainViewModel = useContext(RootContext);
Expand Down Expand Up @@ -239,6 +241,7 @@ const MainContent: FC<MainContentProps> = ({
userState={userState}
isRightSidebarOpen={isRightSidebarOpen}
openRightSidebar={openRightSidebar}
onRefresh={onRefresh}
/>
)}

Expand Down
3 changes: 3 additions & 0 deletions src/script/storage/record/ConversationRecord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,16 @@ import type {QualifiedId} from '@wireapp/api-client/lib/user/';

import {LegalHoldStatus} from '@wireapp/protocol-messaging';

import {CONVERSATION_READONLY_STATE} from 'src/script/conversation/ConversationRepository';

import {ConversationStatus} from '../../conversation/ConversationStatus';
import {ConversationVerificationState} from '../../conversation/ConversationVerificationState';

export interface ConversationRecord {
access_role: CONVERSATION_LEGACY_ACCESS_ROLE | CONVERSATION_ACCESS_ROLE[];
access: CONVERSATION_ACCESS[];
archived_state: boolean;
readonly_state: CONVERSATION_READONLY_STATE | null;
archived_timestamp: number;
cipher_suite: number;
cleared_timestamp: number;
Expand Down
Loading

0 comments on commit 75860e5

Please sign in to comment.