Skip to content

Commit 68c5388

Browse files
authored
Merge pull request #11551 from nextcloud/perf/noid/reduce-initial-sidebar-bundle
2 parents 55b7a77 + ac21ca6 commit 68c5388

15 files changed

Lines changed: 317 additions & 334 deletions

rspack.config.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,7 @@ module.exports = defineConfig((env) => {
4444
collections: path.join(__dirname, 'src', 'collections.js'),
4545
main: path.join(__dirname, 'src', 'main.js'),
4646
recording: path.join(__dirname, 'src', 'mainRecording.js'),
47-
'files-sidebar': [
48-
path.join(__dirname, 'src', 'mainFilesSidebar.js'),
49-
path.join(__dirname, 'src', 'mainFilesSidebarLoader.js'),
50-
],
47+
'files-sidebar': path.join(__dirname, 'src', 'mainFilesSidebarLoader.js'),
5148
'public-share-auth-form': path.join(__dirname, 'src', 'publicShareAuthForm.ts'),
5249
'public-share-auth-sidebar': path.join(__dirname, 'src', 'mainPublicShareAuthSidebar.js'),
5350
'public-share-sidebar': path.join(__dirname, 'src', 'mainPublicShareSidebar.js'),

src/FilesSidebarLoaderApp.vue

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
<script setup lang="ts">
7+
import { showWarning } from '@nextcloud/dialogs'
8+
import { type INode } from '@nextcloud/files'
9+
import { FileType, getSidebar } from '@nextcloud/files'
10+
import { t } from '@nextcloud/l10n'
11+
import { ShareType } from '@nextcloud/sharing'
12+
import { ref, useTemplateRef, watch } from 'vue'
13+
import NcButton from '@nextcloud/vue/components/NcButton'
14+
import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
15+
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
16+
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
17+
import IconMessageOutline from 'vue-material-design-icons/MessageOutline.vue'
18+
import IconShareVariant from 'vue-material-design-icons/ShareVariant.vue'
19+
import IconTalk from '../img/app-dark.svg?raw'
20+
import { getFileConversation } from './services/filesIntegrationServices.ts'
21+
22+
const props = defineProps<{
23+
node: INode
24+
folder: Record<string, unknown>
25+
view: Record<string, unknown>
26+
active: boolean
27+
}>()
28+
29+
const appContainer = useTemplateRef<HTMLDivElement>('appContainer')
30+
const isTalkSidebarSupportedForFile = ref<boolean | undefined>(undefined)
31+
const token = ref<string>()
32+
const isTalkSidebarMounted = ref<boolean>(false)
33+
34+
const isOtherTalkInstanceMounted = ref<boolean>(false)
35+
checkOtherTalkInstanceMounted()
36+
37+
watch(() => props.active, (active) => {
38+
if (active) {
39+
setTalkSidebarSupportedForFile()
40+
} else if (isTalkSidebarMounted.value) {
41+
window.OCA.Talk.unmountInstance?.()
42+
isTalkSidebarMounted.value = false
43+
}
44+
}, { immediate: true })
45+
46+
/**
47+
* Check if file is shared and Talk integration can be opened
48+
*/
49+
async function setTalkSidebarSupportedForFile() {
50+
isTalkSidebarSupportedForFile.value = undefined
51+
token.value = ''
52+
53+
if (!props.node || !props.node.fileid) {
54+
isTalkSidebarSupportedForFile.value = false
55+
return
56+
}
57+
58+
if (props.node.type === FileType.Folder) {
59+
isTalkSidebarSupportedForFile.value = false
60+
return
61+
}
62+
63+
if (props.node.attributes?.['share-owner-id']) {
64+
// Shared with me
65+
isTalkSidebarSupportedForFile.value = true
66+
return
67+
}
68+
69+
if (!props.node.attributes?.['share-types']) {
70+
try {
71+
token.value = (await getFileConversation(props.node.fileid)).data.ocs.data.token || ''
72+
isTalkSidebarSupportedForFile.value = !!token.value
73+
} catch (error) {
74+
isTalkSidebarSupportedForFile.value = false
75+
}
76+
return
77+
}
78+
79+
const shareTypes = Object.values(props.node.attributes?.['share-types'] || {}).flat().filter(function(shareType) {
80+
const type = parseInt(shareType as unknown as string)
81+
return type === ShareType.User
82+
|| type === ShareType.Group
83+
|| type === ShareType.Team
84+
|| type === ShareType.Room
85+
|| type === ShareType.Link
86+
|| type === ShareType.Email
87+
})
88+
89+
if (shareTypes.length === 0) {
90+
try {
91+
token.value = (await getFileConversation(props.node.fileid)).data.ocs.data.token || ''
92+
isTalkSidebarSupportedForFile.value = !!token.value
93+
} catch (error) {
94+
isTalkSidebarSupportedForFile.value = false
95+
}
96+
return
97+
}
98+
99+
isTalkSidebarSupportedForFile.value = true
100+
}
101+
102+
/**
103+
* Mount a Talk integration app
104+
*/
105+
async function joinConversation() {
106+
try {
107+
if (checkOtherTalkInstanceMounted()) {
108+
// Fallback, should be prevented by isOtherTalkInstanceMounted
109+
showWarning(t('spreed', 'Duplicate session'))
110+
return
111+
}
112+
113+
if (!token.value) {
114+
token.value = (await getFileConversation(props.node.fileid!)).data.ocs.data.token || ''
115+
}
116+
117+
import('./mainFilesSidebar.js')
118+
.then((module) => {
119+
module.mountApp(appContainer.value, props, token.value)
120+
isTalkSidebarMounted.value = true
121+
})
122+
} catch (error) {
123+
console.error('Failed to load Talk integration:', error)
124+
}
125+
}
126+
127+
/**
128+
* Check if other Talk instance is active on a page (e.g. floating call)
129+
*/
130+
function checkOtherTalkInstanceMounted() {
131+
isOtherTalkInstanceMounted.value = !!window.OCA.Talk
132+
return isOtherTalkInstanceMounted.value
133+
}
134+
135+
/**
136+
* Open a Sidebar Sharing tab
137+
*/
138+
function openSharingTab() {
139+
getSidebar().setActiveTab('sharing')
140+
isTalkSidebarMounted.value = false
141+
}
142+
</script>
143+
144+
<template>
145+
<div class="talkChatTab">
146+
<NcEmptyContent
147+
v-if="!isTalkSidebarMounted"
148+
class="empty-content">
149+
<template #icon>
150+
<NcLoadingIcon
151+
v-if="isTalkSidebarSupportedForFile === undefined"
152+
class="empty-content__icon"
153+
:size="64" />
154+
<NcIconSvgWrapper
155+
v-else
156+
class="empty-content__icon"
157+
:svg="IconTalk"
158+
:size="64" />
159+
</template>
160+
<template v-if="isTalkSidebarSupportedForFile !== undefined" #name>
161+
<h4>{{ t('spreed', 'Discuss this file') }}</h4>
162+
</template>
163+
<template #description>
164+
<p v-if="isTalkSidebarSupportedForFile === undefined">
165+
{{ t('spreed', 'Loading …') }}
166+
</p>
167+
<p v-else-if="isTalkSidebarSupportedForFile === false">
168+
{{ t('spreed', 'Share this file with others to discuss it') }}
169+
</p>
170+
</template>
171+
<template #action>
172+
<NcButton
173+
v-if="isTalkSidebarSupportedForFile === true"
174+
variant="primary"
175+
:disabled="isOtherTalkInstanceMounted"
176+
@click="joinConversation">
177+
<template #icon>
178+
<IconMessageOutline :size="20" />
179+
</template>
180+
{{ t('spreed', 'Join conversation') }}
181+
</NcButton>
182+
<NcButton
183+
v-if="isTalkSidebarSupportedForFile === false"
184+
variant="primary"
185+
@click="openSharingTab">
186+
<template #icon>
187+
<IconShareVariant :size="20" />
188+
</template>
189+
{{ t('spreed', 'Share this file') }}
190+
</NcButton>
191+
</template>
192+
</NcEmptyContent>
193+
<!-- Full app mounted here after joining -->
194+
<div ref="appContainer" class="app-container" />
195+
</div>
196+
</template>
197+
198+
<style scoped lang="scss">
199+
.talkChatTab {
200+
height: 100%;
201+
202+
display: flex;
203+
flex-grow: 1;
204+
flex-direction: column;
205+
}
206+
207+
.empty-content {
208+
/* Override default top margin set in server and center vertically instead. */
209+
margin-top: unset;
210+
height: 100%;
211+
212+
&__icon {
213+
opacity: 1;
214+
}
215+
}
216+
217+
.app-container {
218+
display: contents;
219+
}
220+
</style>

0 commit comments

Comments
 (0)