Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions apps/webapp/src/script/components/Conversation/Conversation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,16 @@
import {container} from 'tsyringe';

import {useMatchMedia} from '@wireapp/react-ui-kit';
import {WebAppEvents} from '@wireapp/webapp-events';

import {CallingCell} from 'Components/calling/CallingCell';
import {parseAccountDeepLink} from 'Components/Conversation/utils/parseAccountDeepLink';
import {Giphy} from 'Components/Giphy';
import {InputBar} from 'Components/InputBar';
import {MessageListWrapper} from 'Components/MessagesList/MessageListWrapper';
import {showDetailViewModal} from 'Components/Modals/DetailViewModal';
import {PrimaryModal} from 'Components/Modals/PrimaryModal';
import {showUserModal} from 'Components/Modals/UserModal';
import {showWarningModal} from 'Components/Modals/utils/showWarningModal';
import {TitleBar} from 'Components/TitleBar';
import {CallState} from 'Repositories/calling/CallState';
Expand Down Expand Up @@ -296,8 +299,48 @@
return false;
};

const openUserProfile = async (id: string, domain?: string) => {
try {
const userEntity = await repositories.user.getUserById({
id,
domain: domain ?? '',
});

showUserModal(userEntity);
} catch (error: unknown) {
if (error instanceof UserError && error.type === UserError.TYPE.USER_NOT_FOUND) {
messageListLogger.warn('Could not resolve user profile deep link', {id, domain});
return;
}
throw error;
}
};

const handleMarkdownLinkClick = (event: MouseEvent | KeyboardEvent, messageDetails: MessageDetails) => {
const href = messageDetails.href!;

const parsed = parseAccountDeepLink(href, CONFIG.URL.ACCOUNT_BASE);

if (parsed?.type === 'user-profile') {
event.preventDefault();
void openUserProfile(parsed.id, parsed.domain);
return false;
}

if (parsed?.type === 'conversation-join') {
event.preventDefault();

window.dispatchEvent(

Check warning on line 333 in apps/webapp/src/script/components/Conversation/Conversation.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=wireapp_wire-webapp&issues=AZ0gzIpKXlpOqkNVUgYi&open=AZ0gzIpKXlpOqkNVUgYi&pullRequest=20842
new CustomEvent(WebAppEvents.CONVERSATION.JOIN, {
detail: {
key: parsed.key,
code: parsed.code,
domain: parsed.domain,
},
}),
);
return false;
}
PrimaryModal.show(PrimaryModal.type.CONFIRM, {
primaryAction: {
action: () => safeWindowOpen(href),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* Wire
* Copyright (C) 2022 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 {parseAccountDeepLink} from 'Components/Conversation/utils/parseAccountDeepLink';

describe('parseAccountDeepLink', () => {
const accountBase = 'https://account.wire.com';

it('parses a user-profile link with raw id and explicit domain', () => {
expect(
parseAccountDeepLink('https://account.wire.com/user-profile/?id=user-123&domain=wire.com', accountBase),
).toEqual({
type: 'user-profile',
id: 'user-123',
domain: 'wire.com',
});
});

it('prefers explicit domain over domain embedded in id', () => {
expect(
parseAccountDeepLink(
'https://account.wire.com/user-profile/?id=user-123@old.example&domain=new.example',
accountBase,
),
).toEqual({
type: 'user-profile',
id: 'user-123',
domain: 'new.example',
});
});

it('parses a conversation-join link', () => {
expect(
parseAccountDeepLink(
'https://account.wire.com/conversation-join/?key=test-key&code=test-code&domain=wire.com',
accountBase,
),
).toEqual({
type: 'conversation-join',
key: 'test-key',
code: 'test-code',
domain: 'wire.com',
});
});

it('returns null for a different origin', () => {
expect(parseAccountDeepLink('https://google.com/user-profile/?id=user-123', accountBase)).toBeNull();
});

it('returns null when user-profile is missing id', () => {
expect(parseAccountDeepLink('https://account.wire.com/user-profile/', accountBase)).toBeNull();
});

it('returns null when conversation-join is missing key or code', () => {
expect(parseAccountDeepLink('https://account.wire.com/conversation-join/?key=test-key', accountBase)).toBeNull();
});

it('returns null for unsupported account paths', () => {
expect(parseAccountDeepLink('https://account.wire.com/something-else/?id=user=123', accountBase)).toBeNull();
});

it('returns null for invalid urls', () => {
expect(parseAccountDeepLink('not-a-url', accountBase)).toBeNull();
});

it('returns null when accountBase is missing', () => {
expect(parseAccountDeepLink('https://account.wire.com/user-profile/?id=user-123', undefined)).toBeNull();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* Wire
* Copyright (C) 2022 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/.
*
*/

export type ParseAccountDeepLink =
| {type: 'user-profile'; id: string; domain?: string}
| {type: 'conversation-join'; key: string; code: string; domain?: string}
| null;

type ParsedQualifiedId = {
id: string;
domain?: string;
};

const parseQualifiedUserId = (value: string): ParsedQualifiedId => {
const atIndex = value.lastIndexOf('@');
if (atIndex <= 0) {
return {id: value};
}

return {
id: value.slice(0, atIndex),
domain: value.slice(atIndex + 1) || undefined,
};
};

const normalizePath = (pathname: string): string => {
let end = pathname.length;

while (end > 0 && pathname[end - 1] === '/') {
end--;
}

return pathname.slice(0, end);
};

const normalizeOrigin = (url: URL): string => url.origin.toLowerCase();

export const parseAccountDeepLink = (href: string, accountBase?: string): ParseAccountDeepLink => {
if (!href || !accountBase) {
return null;
}
let linkUrl: URL;
let accountBaseUrl: URL;

try {
linkUrl = new URL(href);
accountBaseUrl = new URL(accountBase);
} catch {
return null;
}

if (normalizeOrigin(linkUrl) !== normalizeOrigin(accountBaseUrl)) {
return null;
}

const pathname = normalizePath(linkUrl.pathname);

if (pathname === '/user-profile') {
const rawId = linkUrl.searchParams.get('id');
const explicitDomain = linkUrl.searchParams.get('domain') || undefined;

if (!rawId) {
return null;
}

const qualified = parseQualifiedUserId(rawId);

return {type: 'user-profile', id: qualified.id, domain: explicitDomain ?? qualified.domain};
}

if (pathname === '/conversation-join') {
const key = linkUrl.searchParams.get('key');
const code = linkUrl.searchParams.get('code');
const domain = linkUrl.searchParams.get('domain') || undefined;

if (!key || !code) {
return null;
}
return {type: 'conversation-join', key, code, domain};
}

return null;
};
Loading