Skip to content
Draft
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
13 changes: 13 additions & 0 deletions src/shared/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,16 @@ export function once<F extends (() => 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
}
}
23 changes: 19 additions & 4 deletions src/talk/renderer/TitleBar/components/MainMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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'

Expand All @@ -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)
</script>

<template>
Expand All @@ -42,23 +47,33 @@ const openInWeb = () => window.open(generateUrl(getCurrentTalkRoutePath()), '_bl
</template>

<template v-if="isTalkInitialized">
<NcActionButton @click="openInWeb">
<NcActionButton close-after-click @click="openInWeb">
<template #icon>
<IconWeb :size="20" />
</template>
{{ t('talk_desktop', 'Open in web browser') }}
</NcActionButton>
<NcActionButton close-after-click @click="joinByLink">
<template #icon>
<IconLink :size="20" />
</template>
{{ t('talk_desktop', 'Join by link') }}
</NcActionButton>
</template>

<NcActionSeparator />

<NcActionButton @click="reload">
<NcActionButton close-after-click @click="reload">
<template #icon>
<IconReload :size="20" />
</template>
{{ t('talk_desktop', 'Force reload') }}
</NcActionButton>
<NcActionLink v-if="!BUILD_CONFIG.isBranded" :href="packageInfo.bugs.create || packageInfo.bugs.url" target="_blank">
<NcActionLink
v-if="!BUILD_CONFIG.isBranded"
:href="packageInfo.bugs.create || packageInfo.bugs.url"
target="_blank"
close-after-click>
<template #icon>
<IconBugOutline :size="20" />
</template>
Expand All @@ -73,7 +88,7 @@ const openInWeb = () => window.open(generateUrl(getCurrentTalkRoutePath()), '_bl
</template>
{{ t('talk_desktop', 'Settings') }}
</NcActionButton>
<NcActionButton @click="showHelp">
<NcActionButton close-after-click @click="showHelp">
<template #icon>
<IconInformationOutline :size="20" />
</template>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<!--
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->

<script setup lang="ts">
import axios from '@nextcloud/axios'
import { t } from '@nextcloud/l10n'
import { generateOcsUrl, generateUrl } from '@nextcloud/router'
import { ref, watch } from 'vue'
import NcDialog from '@nextcloud/vue/components/NcDialog'
import NcTextField from '@nextcloud/vue/components/NcTextField'
import { appData } from '../../../../app/AppData.js'
import { openConversation } from '../../TalkWrapper/talk.service.ts'
import { parseConversationToken } from '../../utils/parseConversationToken.ts'

const emit = defineEmits<{
close: []
}>()

const placeholder = generateUrl('/call/…', {}, { baseURL: appData.serverUrl! })

const url = ref('')

const parsedToken = ref({ error: '', token: '' })

watch(url, () => {
parsedToken.value = parseConversationToken(url.value)
})

/** Handle prompt submit */
async function onSubmit() {
if (!parsedToken.value.token) {
return false
}

try {
await axios.get(generateOcsUrl('/apps/spreed/api/v4/room/{token}', { token: parsedToken.value.token }))
} catch {
parsedToken.value = { error: t('talk_desktop', 'Conversation not found'), token: '' }
return false
}

await openConversation(parsedToken.value.token)
return true
}
</script>

<template>
<NcDialog
open
:name="t('talk_desktop', 'Open conversation link')"
size="normal"
:buttons="[{
label: t('talk_desktop', 'Cancel'),
}, {
label: t('talk_desktop', 'Open'),
variant: 'primary',
type: 'submit',
disabled: !parsedToken.token,
callback: onSubmit,
}]"
close-on-click-outside
@close="emit('close')">
<NcTextField
v-model="url"
:label="t('talk_desktop', 'Conversation URL')"
:placeholder
type="url"
:helper-text="parsedToken.error"
:success="!!parsedToken.token"
:error="!!parsedToken.error"
@keydown.enter="onSubmit" />
</NcDialog>
</template>
44 changes: 44 additions & 0 deletions src/talk/renderer/utils/parseConversationToken.ts
Original file line number Diff line number Diff line change
@@ -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: '' }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return { error: t('talk_desktop', 'Opening conversations from other servers is not currently supported'), token: '' }
return { error: t('talk_desktop', 'Opening conversations from other servers is currently not 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: '' }
}