Skip to content

Commit 111f2b9

Browse files
committed
feat(talk): add "Join by link" action
Signed-off-by: Grigorii K. Shartsev <[email protected]>
1 parent 52ae9f3 commit 111f2b9

File tree

4 files changed

+151
-4
lines changed

4 files changed

+151
-4
lines changed

src/shared/utils.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,16 @@ export function once<F extends (() => any) | ((...args: any[]) => void)>(func: F
2626
}
2727

2828
/* eslint-enable @typescript-eslint/no-explicit-any */
29+
30+
/**
31+
* Create URL instance or return null if invalid
32+
*
33+
* @param url - URL string
34+
*/
35+
export function createURL(url: string) {
36+
try {
37+
return new URL(url)
38+
} catch {
39+
return null
40+
}
41+
}

src/talk/renderer/TitleBar/components/MainMenu.vue

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type { Ref } from 'vue'
88
99
import { t } from '@nextcloud/l10n'
1010
import { generateUrl } from '@nextcloud/router'
11+
import { spawnDialog } from '@nextcloud/vue/functions/dialog'
1112
import { inject } from 'vue'
1213
import NcActionButton from '@nextcloud/vue/components/NcActionButton'
1314
import NcActionLink from '@nextcloud/vue/components/NcActionLink'
@@ -16,9 +17,11 @@ import NcActionSeparator from '@nextcloud/vue/components/NcActionSeparator'
1617
import IconBugOutline from 'vue-material-design-icons/BugOutline.vue'
1718
import IconCogOutline from 'vue-material-design-icons/CogOutline.vue'
1819
import IconInformationOutline from 'vue-material-design-icons/InformationOutline.vue'
20+
import IconLink from 'vue-material-design-icons/Link.vue'
1921
import IconMenu from 'vue-material-design-icons/Menu.vue'
2022
import IconReload from 'vue-material-design-icons/Reload.vue'
2123
import IconWeb from 'vue-material-design-icons/Web.vue'
24+
import OpenConversationLinkDialog from './OpenConversationLinkDialog.vue'
2225
import { BUILD_CONFIG } from '../../../../shared/build.config.ts'
2326
import { getCurrentTalkRoutePath } from '../../TalkWrapper/talk.service.ts'
2427
@@ -30,6 +33,8 @@ const showHelp = () => window.TALK_DESKTOP.showHelp()
3033
const reload = () => window.location.reload()
3134
const openSettings = () => window.OCA.Talk.Settings.open()
3235
const openInWeb = () => window.open(generateUrl(getCurrentTalkRoutePath()), '_blank')
36+
37+
const joinByLink = () => spawnDialog(OpenConversationLinkDialog)
3338
</script>
3439

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

4449
<template v-if="isTalkInitialized">
45-
<NcActionButton @click="openInWeb">
50+
<NcActionButton close-after-click @click="openInWeb">
4651
<template #icon>
4752
<IconWeb :size="20" />
4853
</template>
4954
{{ t('talk_desktop', 'Open in web browser') }}
5055
</NcActionButton>
56+
<NcActionButton close-after-click @click="joinByLink">
57+
<template #icon>
58+
<IconLink :size="20" />
59+
</template>
60+
{{ t('talk_desktop', 'Join by link') }}
61+
</NcActionButton>
5162
</template>
5263

5364
<NcActionSeparator />
5465

55-
<NcActionButton @click="reload">
66+
<NcActionButton close-after-click @click="reload">
5667
<template #icon>
5768
<IconReload :size="20" />
5869
</template>
5970
{{ t('talk_desktop', 'Force reload') }}
6071
</NcActionButton>
61-
<NcActionLink v-if="!BUILD_CONFIG.isBranded" :href="packageInfo.bugs.create || packageInfo.bugs.url" target="_blank">
72+
<NcActionLink
73+
v-if="!BUILD_CONFIG.isBranded"
74+
:href="packageInfo.bugs.create || packageInfo.bugs.url"
75+
target="_blank"
76+
close-after-click>
6277
<template #icon>
6378
<IconBugOutline :size="20" />
6479
</template>
@@ -73,7 +88,7 @@ const openInWeb = () => window.open(generateUrl(getCurrentTalkRoutePath()), '_bl
7388
</template>
7489
{{ t('talk_desktop', 'Settings') }}
7590
</NcActionButton>
76-
<NcActionButton @click="showHelp">
91+
<NcActionButton close-after-click @click="showHelp">
7792
<template #icon>
7893
<IconInformationOutline :size="20" />
7994
</template>
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
<script setup lang="ts">
7+
import axios from '@nextcloud/axios'
8+
import { t } from '@nextcloud/l10n'
9+
import { generateOcsUrl, generateUrl } from '@nextcloud/router'
10+
import { ref, watch } from 'vue'
11+
import NcDialog from '@nextcloud/vue/components/NcDialog'
12+
import NcTextField from '@nextcloud/vue/components/NcTextField'
13+
import { appData } from '../../../../app/AppData.js'
14+
import { openConversation } from '../../TalkWrapper/talk.service.ts'
15+
import { parseConversationToken } from '../../utils/parseConversationToken.ts'
16+
17+
const emit = defineEmits<{
18+
close: []
19+
}>()
20+
21+
const placeholder = generateUrl('/call/…', {}, { baseURL: appData.serverUrl! })
22+
23+
const url = ref('')
24+
25+
const parsedToken = ref({ error: '', token: '' })
26+
27+
watch(url, () => {
28+
parsedToken.value = parseConversationToken(url.value)
29+
})
30+
31+
/** Handle prompt submit */
32+
async function onSubmit() {
33+
if (!parsedToken.value.token) {
34+
return false
35+
}
36+
37+
try {
38+
await axios.get(generateOcsUrl('/apps/spreed/api/v4/room/{token}', { token: parsedToken.value.token }))
39+
} catch {
40+
parsedToken.value = { error: t('talk_desktop', 'Conversation not found'), token: '' }
41+
return false
42+
}
43+
44+
await openConversation(parsedToken.value.token)
45+
return true
46+
}
47+
</script>
48+
49+
<template>
50+
<NcDialog
51+
open
52+
:name="t('talk_desktop', 'Open conversation link')"
53+
size="normal"
54+
:buttons="[{
55+
label: t('talk_desktop', 'Cancel'),
56+
}, {
57+
label: t('talk_desktop', 'Open'),
58+
variant: 'primary',
59+
type: 'submit',
60+
disabled: !parsedToken.token,
61+
callback: onSubmit,
62+
}]"
63+
close-on-click-outside
64+
@close="emit('close')">
65+
<NcTextField
66+
v-model="url"
67+
:label="t('talk_desktop', 'Conversation URL')"
68+
:placeholder
69+
type="url"
70+
:helper-text="parsedToken.error"
71+
:success="!!parsedToken.token"
72+
:error="!!parsedToken.error"
73+
@keydown.enter="onSubmit" />
74+
</NcDialog>
75+
</template>
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*!
2+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { t } from '@nextcloud/l10n'
7+
import { appData } from '../../../app/AppData.js'
8+
import { createURL } from '../../../shared/utils.ts'
9+
10+
/**
11+
* Parse URL string to get current server conversation token or error message if invalid
12+
* TODO: currently parsing error has messages for UI in the open conversation link dialog.
13+
* TODO: when it is needed in other places - return error code and translate on the component
14+
*
15+
* @param maybeConversationUrl - URL
16+
*/
17+
export function parseConversationToken(maybeConversationUrl: string): { token: string, error: string } {
18+
// No input - no output
19+
if (!maybeConversationUrl) {
20+
return { error: '', token: '' }
21+
}
22+
23+
const url = createURL(maybeConversationUrl)
24+
if (!url) {
25+
return { error: t('talk_desktop', 'Invalid URL'), token: '' }
26+
}
27+
28+
if (!maybeConversationUrl.startsWith(appData.serverUrl!)) {
29+
return { error: t('talk_desktop', 'Opening conversations from other servers is not currently supported'), token: '' }
30+
}
31+
32+
let pathname = maybeConversationUrl.slice((appData.serverUrl as unknown as string).length)
33+
const indexPhp = pathname.indexOf('/index.php')
34+
if (indexPhp === 0) {
35+
pathname = pathname.slice('/index.php'.length)
36+
}
37+
38+
const [isMatched, token] = pathname.match(/^\/call\/([a-z0-9]+)\/?$/i) ?? [false, '']
39+
if (!isMatched) {
40+
return { error: t('talk_desktop', 'Invalid URL'), token: '' }
41+
}
42+
43+
return { token, error: '' }
44+
}

0 commit comments

Comments
 (0)