Skip to content

Commit

Permalink
refactor: readonly conversation indication (#17369)
Browse files Browse the repository at this point in the history
* feat: add literal string replacement support for react localizer util (#17273)

* feat: add literal string replacement support for react localizer util

* feat: support string literal inside the component

* refactor: improve naming

* doc: add a comment

* refactor: readonly conversaiton indication
  • Loading branch information
PatrykBuniX committed May 7, 2024
1 parent 792f3c3 commit 07a6289
Show file tree
Hide file tree
Showing 9 changed files with 432 additions and 160 deletions.
4 changes: 2 additions & 2 deletions src/i18n/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -1166,7 +1166,7 @@
"ongoingGroupAudioCall": "Ongoing conference call with {{conversationName}}.",
"ongoingGroupVideoCall": "Ongoing video conference call with {{conversationName}}, your camera is {{cameraStatus}}.",
"ongoingVideoCall": "Ongoing video call with {{conversationName}}, your camera is {{cameraStatus}}.",
"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.",
"otherUserNotSupportMLSMsg": "You can't communicate with {{participantName}} anymore, as you two now use different protocols. When {{participantName}} gets an update, you can call and send messages and files again.",
"participantDevicesDetailHeadline": "Verify that this matches the fingerprint shown on [bold]{{user}}’s device[/bold].",
"participantDevicesDetailHowTo": "How do I do that?",
"participantDevicesDetailResetSession": "Reset session",
Expand Down Expand Up @@ -1377,7 +1377,7 @@
"searchTrySearch": "Find people by\nname or username",
"searchTrySearchFederation": "Find people in Wire by name or\n@username\n\nFind people from another domain\nby @username@domainname",
"searchTrySearchLearnMore": "Learn more",
"selfNotSupportMLSMsgPart1": "You can't communicate with [bold]{{selfUserName}}[/bold] anymore, as your device doesn't support the suitable protocol.",
"selfNotSupportMLSMsgPart1": "You can't communicate with {{selfUserName}} anymore, as your device doesn't support the suitable protocol.",
"selfNotSupportMLSMsgPart2": "to call, and send messages and files again.",
"selfProfileImageAlt": "Your profile picture",
"servicesOptionsTitle": "Services",
Expand Down
31 changes: 9 additions & 22 deletions src/script/components/Conversation/Conversation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ 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 @@ -45,7 +44,7 @@ import {safeMailOpen, safeWindowOpen} from 'Util/SanitizationUtil';
import {formatBytes, incomingCssClass, removeAnimationsClass} from 'Util/util';

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

import {ConversationState} from '../../conversation/ConversationState';
Expand Down Expand Up @@ -103,19 +102,11 @@ export const Conversation = ({
'isFileSharingSendingEnabled',
]);

const {
is1to1,
isRequest,
readOnlyState,
display_name: displayName,
} = useKoSubscribableChildren(activeConversation!, ['is1to1', 'isRequest', 'display_name', 'readOnlyState']);

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 {is1to1, isRequest, isReadOnlyConversation} = useKoSubscribableChildren(activeConversation!, [
'is1to1',
'isRequest',
'isReadOnlyConversation',
]);

const inTeam = teamState.isInTeam(selfUser);

Expand Down Expand Up @@ -488,7 +479,7 @@ export const Conversation = ({
callActions={mainViewModel.calling.callActions}
openRightSidebar={openRightSidebar}
isRightSidebarOpen={isRightSidebarOpen}
isReadOnlyConversation={showReadOnlyConversationMessage}
isReadOnlyConversation={isReadOnlyConversation}
/>

{activeCalls.map(call => {
Expand Down Expand Up @@ -539,12 +530,8 @@ export const Conversation = ({
/>

{isConversationLoaded &&
(showReadOnlyConversationMessage ? (
<ReadOnlyConversationMessage
state={readOnlyState}
handleMLSUpdate={reloadApp}
displayName={displayName}
/>
(isReadOnlyConversation ? (
<ReadOnlyConversationMessage reloadApp={reloadApp} conversation={activeConversation} />
) : (
<InputBar
key={activeConversation?.id}
Expand Down
77 changes: 0 additions & 77 deletions src/script/components/Conversation/ReadOnlyConversationMessage.tsx

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Wire
* Copyright (C) 2024 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 {render} from '@testing-library/react';
import {ConnectionStatus} from '@wireapp/api-client/lib/connection';
import {CONVERSATION_TYPE} from '@wireapp/api-client/lib/conversation';

import {withTheme} from 'src/script/auth/util/test/TestUtil';
import {ConnectionEntity} from 'src/script/connection/ConnectionEntity';
import {CONVERSATION_READONLY_STATE} from 'src/script/conversation/ConversationRepository';
import {Conversation} from 'src/script/entity/Conversation';
import {User} from 'src/script/entity/User';

import {ReadOnlyConversationMessage} from './ReadOnlyConversationMessage';

const generateConversation = (
readOnlyState: CONVERSATION_READONLY_STATE | null = null,
is1To1WithBlockedUser = false,
userName = 'John Doe',
) => {
const conversation = new Conversation();
conversation.type(CONVERSATION_TYPE.ONE_TO_ONE);
conversation.readOnlyState(readOnlyState);

const connection = new ConnectionEntity();

if (is1To1WithBlockedUser) {
connection.status(ConnectionStatus.BLOCKED);
}

const user = new User('user-id', 'user-domain');
user.name(userName);
conversation.participating_user_ets([user]);
conversation.participating_user_ids([user.qualifiedId]);

user.connection(connection);
connection.userId = user.qualifiedId;

return conversation;
};

describe('ReadOnlyConversationMessage', () => {
it('renders mls is not supported by the other user', () => {
const conversation = generateConversation(
CONVERSATION_READONLY_STATE.READONLY_ONE_TO_ONE_OTHER_UNSUPPORTED_MLS,
false,
);

const {getByText} = render(
withTheme(<ReadOnlyConversationMessage conversation={conversation} reloadApp={() => {}} />),
);

expect(getByText('otherUserNotSupportMLSMsg')).toBeDefined();
});

it('renders mls is not supported by the self user', () => {
const conversation = generateConversation(
CONVERSATION_READONLY_STATE.READONLY_ONE_TO_ONE_SELF_UNSUPPORTED_MLS,
false,
);

const reloadAppMock = jest.fn();

const {getByText} = render(
withTheme(<ReadOnlyConversationMessage reloadApp={reloadAppMock} conversation={conversation} />),
);

const reloadButton = getByText('downloadLatestMLS');

expect(getByText('selfNotSupportMLSMsgPart1')).toBeDefined();
expect(reloadButton).toBeDefined();
expect(getByText('selfNotSupportMLSMsgPart2')).toBeDefined();

reloadButton.click();

expect(reloadAppMock).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* 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, ReactNode} 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 {Conversation} from 'src/script/entity/Conversation';
import {useKoSubscribableChildren} from 'Util/ComponentUtil';
import {t} from 'Util/LocalizerUtil';
import {replaceReactComponents} from 'Util/LocalizerUtil/ReactLocalizerUtil';

interface ReadOnlyConversationMessageProps {
reloadApp: () => void;
conversation: Conversation;
}

export const ReadOnlyConversationMessage: FC<ReadOnlyConversationMessageProps> = ({conversation, reloadApp}) => {
const {
readOnlyState,
is1to1,
participating_user_ets: participatingUserEts,
} = useKoSubscribableChildren(conversation, ['readOnlyState', 'is1to1', 'participating_user_ets']);

const user = (is1to1 && participatingUserEts[0]) || null;

if (!user) {
// This should never happen for 1:1 conversations
return null;
}

if (readOnlyState) {
switch (readOnlyState) {
case CONVERSATION_READONLY_STATE.READONLY_ONE_TO_ONE_OTHER_UNSUPPORTED_MLS:
return (
<ReadOnlyConversationMessageBase>
<span>
{replaceReactComponents(t('otherUserNotSupportMLSMsg'), [
{
exactMatch: '{{participantName}}',
render: () => <strong>{user.name()}</strong>,
},
])}
</span>
</ReadOnlyConversationMessageBase>
);
case CONVERSATION_READONLY_STATE.READONLY_ONE_TO_ONE_SELF_UNSUPPORTED_MLS:
return (
<ReadOnlyConversationMessageBase>
<span>
{replaceReactComponents(t('selfNotSupportMLSMsgPart1'), [
{
exactMatch: '{{selfUserName}}',
render: () => <strong>{user.name()}</strong>,
},
])}
</span>
<>
{' '}
<Link
css={{fontSize: 'var(--font-size-small)', fontWeight: 'var(--font-weight-semibold)'}}
onClick={reloadApp}
variant={LinkVariant.PRIMARY}
data-uie-name="do-update-mls"
>
{t('downloadLatestMLS')}
</Link>{' '}
<span>{t('selfNotSupportMLSMsgPart2')}</span>
</>
</ReadOnlyConversationMessageBase>
);
}
}

return null;
};

const ReadOnlyConversationMessageBase = ({children}: {children: ReactNode}) => {
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>
<p className="readonly-message-header-label" data-uie-name="element-readonly-conversation">
{children}
</p>
</div>
);
};
4 changes: 4 additions & 0 deletions src/script/entity/Conversation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export class Conversation {
public readonly readOnlyState: ko.Observable<CONVERSATION_READONLY_STATE | null>;
private readonly incomingMessages: ko.ObservableArray<Message>;
public readonly isProteusTeam1to1: ko.PureComputed<boolean>;
public readonly isReadOnlyConversation: ko.PureComputed<boolean>;
public readonly last_server_timestamp: ko.Observable<number>;
private readonly logger: Logger;
public readonly mutedState: ko.Observable<number>;
Expand Down Expand Up @@ -242,6 +243,9 @@ export class Conversation {
otherMembersLength: this.participating_user_ids().length,
}),
);

this.isReadOnlyConversation = ko.pureComputed(() => this.readOnlyState() !== null);

this.isGroup = ko.pureComputed(() => {
const isGroupConversation = this.type() === CONVERSATION_TYPE.REGULAR;
return isGroupConversation && !this.isProteusTeam1to1();
Expand Down

0 comments on commit 07a6289

Please sign in to comment.