Skip to content

Commit

Permalink
Sse enhancements (#10709)
Browse files Browse the repository at this point in the history
* Early return in sse events, if current client did the request, add folder created event handler

* Prevent double fetch resources

* add file touched event

* Fix text file thumbnails are hidden on purpose (too short) in table view but shown in tiles view
  • Loading branch information
AlexAndBear authored Apr 8, 2024
1 parent 08754db commit e76186d
Show file tree
Hide file tree
Showing 14 changed files with 305 additions and 61 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Enhancement: Implement Server-Sent Events (SSE) for File Creation

We've implemented Server-Sent Events (SSE) to notify users in real-time when a file is uploaded,
a new folder is created, or a file is created (e.g., a text file).
With this enhancement, users will see new files automatically appear in another browser tab if they have one open or
when collaborating with others in the same space.

https://github.com/owncloud/web/pull/10709
https://github.com/owncloud/web/issues/9782
4 changes: 3 additions & 1 deletion packages/web-client/src/sse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ export enum MESSAGE_TYPE {
POSTPROCESSING_FINISHED = 'postprocessing-finished',
FILE_LOCKED = 'file-locked',
FILE_UNLOCKED = 'file-unlocked',
FILE_TOUCHED = 'file-touched',
ITEM_RENAMED = 'item-renamed',
ITEM_TRASHED = 'item-trashed',
ITEM_RESTORED = 'item-restored'
ITEM_RESTORED = 'item-restored',
FOLDER_CREATED = 'folder-created'
}

export class RetriableError extends Error {
Expand Down
6 changes: 5 additions & 1 deletion packages/web-client/src/webdav/client/dav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ interface DAVOptions {
accessToken: Ref<string>
baseUrl: string
language: Ref<string>
clientInitiatorId: Ref<string>
}

interface DavResult {
Expand All @@ -31,12 +32,14 @@ export class DAV {
private client: WebDAVClient
private davPath: string
private language: Ref<string>
private clientInitiatorId: Ref<string>

constructor({ accessToken, baseUrl, language }: DAVOptions) {
constructor({ accessToken, baseUrl, language, clientInitiatorId }: DAVOptions) {
this.davPath = urlJoin(baseUrl, 'remote.php/dav')
this.accessToken = accessToken
this.client = createClient(this.davPath, {})
this.language = language
this.clientInitiatorId = clientInitiatorId
}

public mkcol(path: string, { headers = {} }: { headers?: Headers } = {}) {
Expand Down Expand Up @@ -166,6 +169,7 @@ export class DAV {
return {
'Accept-Language': unref(this.language),
'Content-Type': 'application/xml; charset=utf-8',
'Initiator-ID': unref(this.clientInitiatorId),
'X-Requested-With': 'XMLHttpRequest',
'X-Request-ID': uuidV4(),
...headers
Expand Down
3 changes: 2 additions & 1 deletion packages/web-client/src/webdav/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ export const webdav = (options: WebDavOptions): WebDAV => {
const dav = new DAV({
accessToken: options.accessToken,
baseUrl: options.baseUrl,
language: options.language
language: options.language,
clientInitiatorId: options.clientInitiatorId
})

const pathForFileIdFactory = GetPathForFileIdFactory(dav, options)
Expand Down
1 change: 1 addition & 0 deletions packages/web-client/src/webdav/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface WebDavOptions {
clientService: any
language: Ref<string>
user: Ref<User>
clientInitiatorId: Ref<string>
}

export interface WebDAV {
Expand Down
14 changes: 12 additions & 2 deletions packages/web-pkg/src/components/FilesList/ResourceTile.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@
>
<div class="oc-tile-card-hover"></div>
<slot name="imageField" :item="resource">
<oc-img v-if="resource.thumbnail" class="tile-preview" :src="resource.thumbnail" />
<oc-img
v-if="shouldDisplayThumbnails(resource)"
class="tile-preview"
:src="resource.thumbnail"
/>
<resource-icon
v-else
:resource="resource"
Expand Down Expand Up @@ -82,6 +86,7 @@ import ResourceLink from './ResourceLink.vue'
import { Resource } from '@ownclouders/web-client'
import { useGettext } from 'vue3-gettext'
import { isSpaceResource } from '@ownclouders/web-client/src/helpers'
import { isResourceTxtFileAlmostEmpty } from '../../helpers'
export default defineComponent({
name: 'ResourceTile',
Expand Down Expand Up @@ -158,12 +163,17 @@ export default defineComponent({
return ''
})
const shouldDisplayThumbnails = (resource: Resource) => {
return resource.thumbnail && !isResourceTxtFileAlmostEmpty(resource)
}
return {
statusIconAttrs,
showStatusIcon,
tooltipLabelIcon,
resourceDisabled,
resourceDescription
resourceDescription,
shouldDisplayThumbnails
}
}
})
Expand Down
16 changes: 16 additions & 0 deletions packages/web-pkg/src/composables/piniaStores/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useClientStore = defineStore('client', () => {
const clientInitiatorId = ref<string>()
const setClientInitiatorId = (id: string) => {
clientInitiatorId.value = id
}

return {
clientInitiatorId,
setClientInitiatorId
}
})

export type ClientStore = ReturnType<typeof useClientStore>
1 change: 1 addition & 0 deletions packages/web-pkg/src/composables/piniaStores/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export * from './shares'
export * from './spaces'
export * from './theme'
export * from './user'
export * from './client'
9 changes: 7 additions & 2 deletions packages/web-pkg/src/composables/upload/useUpload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { computed, unref, watch } from 'vue'
import { UppyService } from '../../services/uppy/uppyService'
import { v4 as uuidV4 } from 'uuid'
import { useGettext } from 'vue3-gettext'
import { useAuthStore, useCapabilityStore, useConfigStore } from '../piniaStores'
import { useAuthStore, useCapabilityStore, useClientStore, useConfigStore } from '../piniaStores'

interface UploadOptions {
uppyService: UppyService
Expand All @@ -11,11 +11,15 @@ interface UploadOptions {
export function useUpload(options: UploadOptions) {
const configStore = useConfigStore()
const capabilityStore = useCapabilityStore()
const clientStore = useClientStore()
const { current: currentLanguage } = useGettext()
const authStore = useAuthStore()

const headers = computed((): { [key: string]: string } => {
const headers = { 'Accept-Language': currentLanguage }
const headers = {
'Accept-Language': currentLanguage,
'Initiator-ID': clientStore.clientInitiatorId
}
if (authStore.publicLinkContextReady) {
const password = authStore.publicLinkPassword
if (password) {
Expand All @@ -42,6 +46,7 @@ export function useUpload(options: UploadOptions) {
req.setHeader('Authorization', unref(headers).Authorization)
req.setHeader('X-Request-ID', uuidV4())
req.setHeader('Accept-Language', unref(headers)['Accept-Language'])
req.setHeader('Initiator-ID', unref(headers)['Initiator-ID'])
},
headers: (file) =>
!!file.xhrUpload || file?.isRemote
Expand Down
8 changes: 6 additions & 2 deletions packages/web-pkg/src/services/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { WebDAV } from '@ownclouders/web-client/src/webdav'
import { Language } from 'vue3-gettext'
import { FetchEventSourceInit } from '@microsoft/fetch-event-source'
import { sse } from '@ownclouders/web-client/src/sse'
import { AuthStore, ConfigStore, UserStore } from '../../composables'
import { AuthStore, ClientStore, ConfigStore, UserStore } from '../../composables'
import { computed } from 'vue'

interface OcClient {
Expand Down Expand Up @@ -54,13 +54,15 @@ export interface ClientServiceOptions {
language: Language
authStore: AuthStore
userStore: UserStore
clientStore: ClientStore
}

export class ClientService {
private configStore: ConfigStore
private language: Language
private authStore: AuthStore
private userStore: UserStore
private clientStore: ClientStore

private httpAuthenticatedClient: HttpClient
private httpUnAuthenticatedClient: HttpClient
Expand All @@ -75,6 +77,7 @@ export class ClientService {
this.language = options.language
this.authStore = options.authStore
this.userStore = options.userStore
this.clientStore = options.clientStore
}

public get httpAuthenticated(): _HttpClient {
Expand Down Expand Up @@ -132,7 +135,8 @@ export class ClientService {
'Accept-Language': this.currentLanguage,
...(!!authenticated && { Authorization: 'Bearer ' + this.authStore.accessToken }),
'X-Requested-With': 'XMLHttpRequest',
'X-Request-ID': uuidV4()
'X-Request-ID': uuidV4(),
'Initiator-ID': this.clientStore.clientInitiatorId
}
})
}
Expand Down
12 changes: 10 additions & 2 deletions packages/web-pkg/tests/unit/services/client.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { HttpClient } from '../../../src/http'
import { ClientService, useAuthStore, useConfigStore, useUserStore } from '../../../src/'
import {
ClientService,
useAuthStore,
useClientStore,
useConfigStore,
useUserStore
} from '../../../src/'
import { Language } from 'vue3-gettext'
import { Graph, OCS, client as _client } from '@ownclouders/web-client'
import { createTestingPinia, writable } from 'web-test-helpers'
Expand All @@ -13,14 +19,16 @@ const getClientServiceMock = () => {
const authStore = useAuthStore()
const configStore = useConfigStore()
const userStore = useUserStore()
const clientStore = useClientStore()
writable(configStore).serverUrl = serverUrl

return {
clientService: new ClientService({
configStore,
language: language as Language,
authStore,
userStore
userStore,
clientStore
}),
authStore
}
Expand Down
52 changes: 46 additions & 6 deletions packages/web-runtime/src/container/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,17 @@ import {
useResourcesStore,
ResourcesStore,
SpacesStore,
MessageStore
MessageStore,
useClientStore
} from '@ownclouders/web-pkg'
import { authService } from '../services/auth'
import {
ClientService,
LoadingService,
PasswordPolicyService,
PreviewService,
UppyService
UppyService,
ClientStore
} from '@ownclouders/web-pkg'
import { init as sentryInit } from '@sentry/vue'
import { webdav } from '@ownclouders/web-client/src/webdav'
Expand All @@ -60,7 +62,9 @@ import {
onSSEItemRenamedEvent,
onSSEProcessingFinishedEvent,
onSSEItemRestoredEvent,
onSSEItemTrashedEvent
onSSEItemTrashedEvent,
onSSEFolderCreatedEvent,
onSSEFileTouchedEvent
} from './sse'

const getEmbedConfigFromQuery = (
Expand Down Expand Up @@ -331,6 +335,7 @@ export const announcePiniaStores = () => {
const sharesStore = useSharesStore()
const spacesStore = useSpacesStore()
const userStore = useUserStore()
const clientStore = useClientStore()

return {
appsStore,
Expand All @@ -343,7 +348,8 @@ export const announcePiniaStores = () => {
modalStore,
sharesStore,
spacesStore,
userStore
userStore,
clientStore
}
}

Expand Down Expand Up @@ -388,19 +394,24 @@ export const announceClientService = ({
configStore,
userStore,
authStore,
capabilityStore
capabilityStore,
clientStore
}: {
app: App
configStore: ConfigStore
userStore: UserStore
authStore: AuthStore
capabilityStore: CapabilityStore
clientStore: ClientStore
}): void => {
clientStore.setClientInitiatorId(uuidV4())

const clientService = new ClientService({
configStore,
language: app.config.globalProperties.$language,
authStore,
userStore
userStore,
clientStore
})
app.config.globalProperties.$clientService = clientService
app.config.globalProperties.$clientService.webdav = webdav({
Expand All @@ -409,6 +420,7 @@ export const announceClientService = ({
capabilities: computed(() => capabilityStore.capabilities),
clientService: app.config.globalProperties.$clientService,
language: computed(() => app.config.globalProperties.$language.current),
clientInitiatorId: computed(() => clientStore.clientInitiatorId),
user: computed(() => userStore.user)
})

Expand Down Expand Up @@ -644,6 +656,7 @@ export const registerSSEEventListeners = ({
language,
resourcesStore,
spacesStore,
clientStore,
messageStore,
clientService,
previewService,
Expand All @@ -653,6 +666,7 @@ export const registerSSEEventListeners = ({
language: Language
resourcesStore: ResourcesStore
spacesStore: SpacesStore
clientStore: ClientStore
messageStore: MessageStore
clientService: ClientService
previewService: PreviewService
Expand All @@ -675,6 +689,7 @@ export const registerSSEEventListeners = ({
topic: MESSAGE_TYPE.ITEM_RENAMED,
resourcesStore,
spacesStore,
clientStore,
msg,
clientService,
router
Expand All @@ -686,6 +701,7 @@ export const registerSSEEventListeners = ({
topic: MESSAGE_TYPE.POSTPROCESSING_FINISHED,
resourcesStore,
spacesStore,
clientStore,
msg,
clientService,
previewService,
Expand Down Expand Up @@ -718,6 +734,7 @@ export const registerSSEEventListeners = ({
topic: MESSAGE_TYPE.ITEM_TRASHED,
language,
resourcesStore,
clientStore,
messageStore,
msg
})
Expand All @@ -728,6 +745,29 @@ export const registerSSEEventListeners = ({
topic: MESSAGE_TYPE.ITEM_RESTORED,
resourcesStore,
spacesStore,
clientStore,
msg,
clientService
})
)

clientService.sseAuthenticated.addEventListener(MESSAGE_TYPE.FOLDER_CREATED, (msg) =>
onSSEFolderCreatedEvent({
topic: MESSAGE_TYPE.FOLDER_CREATED,
resourcesStore,
spacesStore,
clientStore,
msg,
clientService
})
)

clientService.sseAuthenticated.addEventListener(MESSAGE_TYPE.FILE_TOUCHED, (msg) =>
onSSEFileTouchedEvent({
topic: MESSAGE_TYPE.FILE_TOUCHED,
resourcesStore,
spacesStore,
clientStore,
msg,
clientService
})
Expand Down
Loading

0 comments on commit e76186d

Please sign in to comment.