diff --git a/src/shared/utils.ts b/src/shared/utils.ts index 5e01fc63..ea3a566d 100644 --- a/src/shared/utils.ts +++ b/src/shared/utils.ts @@ -26,3 +26,16 @@ export function once any) | ((...args: any[]) => void)>(func: F } /* eslint-enable @typescript-eslint/no-explicit-any */ + +/** + * Create URL instance or return null if invalid + * + * @param url - URL string + */ +export function createURL(url: string) { + try { + return new URL(url) + } catch { + return null + } +} diff --git a/src/talk/renderer/TitleBar/components/MainMenu.vue b/src/talk/renderer/TitleBar/components/MainMenu.vue index 37bf2122..22c90d79 100644 --- a/src/talk/renderer/TitleBar/components/MainMenu.vue +++ b/src/talk/renderer/TitleBar/components/MainMenu.vue @@ -8,6 +8,7 @@ import type { Ref } from 'vue' import { t } from '@nextcloud/l10n' import { generateUrl } from '@nextcloud/router' +import { spawnDialog } from '@nextcloud/vue/functions/dialog' import { inject } from 'vue' import NcActionButton from '@nextcloud/vue/components/NcActionButton' import NcActionLink from '@nextcloud/vue/components/NcActionLink' @@ -16,9 +17,11 @@ import NcActionSeparator from '@nextcloud/vue/components/NcActionSeparator' import IconBugOutline from 'vue-material-design-icons/BugOutline.vue' import IconCogOutline from 'vue-material-design-icons/CogOutline.vue' import IconInformationOutline from 'vue-material-design-icons/InformationOutline.vue' +import IconLink from 'vue-material-design-icons/Link.vue' import IconMenu from 'vue-material-design-icons/Menu.vue' import IconReload from 'vue-material-design-icons/Reload.vue' import IconWeb from 'vue-material-design-icons/Web.vue' +import OpenConversationLinkDialog from './OpenConversationLinkDialog.vue' import { BUILD_CONFIG } from '../../../../shared/build.config.ts' import { getCurrentTalkRoutePath } from '../../TalkWrapper/talk.service.ts' @@ -30,6 +33,8 @@ const showHelp = () => window.TALK_DESKTOP.showHelp() const reload = () => window.location.reload() const openSettings = () => window.OCA.Talk.Settings.open() const openInWeb = () => window.open(generateUrl(getCurrentTalkRoutePath()), '_blank') + +const joinByLink = () => spawnDialog(OpenConversationLinkDialog) - + {{ t('talk_desktop', 'Force reload') }} - + @@ -73,7 +88,7 @@ const openInWeb = () => window.open(generateUrl(getCurrentTalkRoutePath()), '_bl {{ t('talk_desktop', 'Settings') }} - + diff --git a/src/talk/renderer/TitleBar/components/OpenConversationLinkDialog.vue b/src/talk/renderer/TitleBar/components/OpenConversationLinkDialog.vue new file mode 100644 index 00000000..9a17c4d9 --- /dev/null +++ b/src/talk/renderer/TitleBar/components/OpenConversationLinkDialog.vue @@ -0,0 +1,75 @@ + + + + + diff --git a/src/talk/renderer/utils/parseConversationToken.ts b/src/talk/renderer/utils/parseConversationToken.ts new file mode 100644 index 00000000..2e1b6bf9 --- /dev/null +++ b/src/talk/renderer/utils/parseConversationToken.ts @@ -0,0 +1,44 @@ +/*! + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { t } from '@nextcloud/l10n' +import { appData } from '../../../app/AppData.js' +import { createURL } from '../../../shared/utils.ts' + +/** + * Parse URL string to get current server conversation token or error message if invalid + * TODO: currently parsing error has messages for UI in the open conversation link dialog. + * TODO: when it is needed in other places - return error code and translate on the component + * + * @param maybeConversationUrl - URL + */ +export function parseConversationToken(maybeConversationUrl: string): { token: string, error: string } { + // No input - no output + if (!maybeConversationUrl) { + return { error: '', token: '' } + } + + const url = createURL(maybeConversationUrl) + if (!url) { + return { error: t('talk_desktop', 'Invalid URL'), token: '' } + } + + if (!maybeConversationUrl.startsWith(appData.serverUrl!)) { + return { error: t('talk_desktop', 'Opening conversations from other servers is not currently supported'), token: '' } + } + + let pathname = maybeConversationUrl.slice((appData.serverUrl as unknown as string).length) + const indexPhp = pathname.indexOf('/index.php') + if (indexPhp === 0) { + pathname = pathname.slice('/index.php'.length) + } + + const [isMatched, token] = pathname.match(/^\/call\/([a-z0-9]+)\/?$/i) ?? [false, ''] + if (!isMatched) { + return { error: t('talk_desktop', 'Invalid URL'), token: '' } + } + + return { token, error: '' } +}