Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
6181d51
fix(people): hide local client for guests
max-nextcloud Jul 19, 2025
f4bf44b
fix(sessions): use 30px size for avatars
max-nextcloud Aug 19, 2025
8347411
chore(cleanup): outdated code in GuestNameDialog
max-nextcloud Aug 20, 2025
238d47b
fix(ui): nest guest name form in li in session list
max-nextcloud Aug 20, 2025
8cabc9e
fix(ui): use color-max-contrast for guest names
max-nextcloud Aug 20, 2025
348f564
fix(avatars): adopt to box-sizing border-box
max-nextcloud Aug 20, 2025
45c01b3
fix(ui): use NcInputField for guest name
max-nextcloud Aug 20, 2025
e1c66dd
chore(wording): Edit guest name -> Enter your name
max-nextcloud Aug 20, 2025
3d772e5
chore(extract): useSessions composable from Editor.vue
max-nextcloud Aug 20, 2025
a9dee1f
chore(refactor): useSessions directly in SessionList
max-nextcloud Aug 20, 2025
440d57f
fix(colors): use avatar colors for text markers
max-nextcloud Aug 20, 2025
224e392
fix(session): update guest name avatar right away
max-nextcloud Aug 20, 2025
994cc9d
fix(avatars): overwrite width of NcButton
max-nextcloud Aug 20, 2025
0878087
fix(avatar): updateUser after guest name change
max-nextcloud Aug 20, 2025
f5794cb
fix(selection): use clientId arg in render function
max-nextcloud Aug 21, 2025
01c1ce1
fix(sessions): keep session list up to date
max-nextcloud Aug 21, 2025
5c15d75
chore(cleanup): outdated myName data in SessionList
max-nextcloud Aug 22, 2025
460bf79
fix(ui): use Account icon for anonymous guests
max-nextcloud Aug 23, 2025
511ffd1
enh(guests): edit toggle for guest name dialog
max-nextcloud Aug 24, 2025
1f5dee7
chore(migrate): GuestNameDialog to script setup syntax
max-nextcloud Aug 24, 2025
3595547
chore(style): use async function setGuestName
max-nextcloud Aug 24, 2025
c4fbddb
enh(guest): submit button for guest name entry
max-nextcloud Aug 24, 2025
1d6920b
chore(cleanup): reuse AwarenessUser interface
max-nextcloud Aug 25, 2025
21e918e
chore(cleanup): unused $userName in SessionService
max-nextcloud Aug 25, 2025
e837479
fix(guests): always use the same color for the same name
max-nextcloud Aug 25, 2025
bc47dce
fix(props): type validation with `required` not `require`
max-nextcloud Aug 25, 2025
1ac68ac
chore(cleanup): unused size prop of AvatarWrapper
max-nextcloud Aug 25, 2025
c822a04
fix(reactivity): adjust guest name entry when prop changes
max-nextcloud Aug 25, 2025
2a9efc0
fix(guest): handle missing or full localStorage gracefully
max-nextcloud Aug 25, 2025
2e55e88
fix(guest): validate name length and handle long names
max-nextcloud Aug 25, 2025
ac5b3e3
fix(status): handle undefined document gracefully
max-nextcloud Aug 26, 2025
cdbc89d
fix(sessionlist): handle undefined currentSession gracefully.
max-nextcloud Aug 26, 2025
00dafb5
fix(types): Sessions do not include the clientId
max-nextcloud Aug 26, 2025
b1addd7
fix(types): Sessions usually do not have a token
max-nextcloud Aug 26, 2025
a906fb4
enh(types): GuestSession and UserSession
max-nextcloud Aug 26, 2025
9d38e65
fix(types): localStorage always exists
max-nextcloud Aug 26, 2025
adfe915
fix(guest): remove edit text from button
max-nextcloud Aug 28, 2025
1b20f45
chore(cleanup): do not store nick after loading it
max-nextcloud Sep 1, 2025
4aa6c84
fix(guest): show Guest cursor label if no guestname set
max-nextcloud Sep 3, 2025
4f691ed
fix(sessions): use box-sizing for all avatars
max-nextcloud Sep 3, 2025
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
18 changes: 6 additions & 12 deletions cypress/e2e/share.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,22 +116,16 @@ describe('Open test.md in viewer', function () {
})
.then(() => {
cy.getEditor().should('be.visible')
cy.getContent()
.should('contain', 'Hello world')
.find('h2')
.should('contain', 'Hello world')

cy.intercept({
method: 'POST',
url: '**/apps/text/public/session/*/session',
}).as('updateSession')
cy.getContent().find('h2').should('contain', 'Hello world')
cy.get('button.avatar-list').click()
cy.get('.session-menu button').click()
cy.get('.guest-name-dialog input[type="text"]').type(
'someone{enter}',
)
cy.wait('@updateSession')
.its('response.body.guestName')
.should('eq', 'someone')
cy.get('.session-menu .session-label.guest').should(
'contain',
'someone',
)
})
})

Expand Down
17 changes: 13 additions & 4 deletions lib/Service/SessionService.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,9 @@ public function __construct(
public function initSession(int $documentId, ?string $guestName = null): Session {
$session = new Session();
$session->setDocumentId($documentId);
$userName = $this->userId ?? $guestName;
$session->setUserId($this->userId);
$session->setToken($this->secureRandom->generate(64));
$session->setColor($this->getColorForGuestName($guestName ?? ''));
$session->setColor($this->getColor());
if ($this->userId === null) {
$session->setGuestName($guestName ?? '');
}
Expand Down Expand Up @@ -223,9 +222,19 @@ public function updateSessionAwareness(Session $session, string $message): Sessi
return $this->sessionMapper->update($session);
}

private function getColor(string $guestName = ''): string {
if ($this->userId === null) {
return $this->getColorForGuestName($guestName);
}
$name = $this->userManager->getDisplayName($this->userId) ?? $this->userId;
$color = $this->avatarManager->getAvatar($this->userId)->avatarBackgroundColor($name);
return $color->name();
}

private function getColorForGuestName(string $guestName = ''): string {
$guestName = $this->userId !== null ? $guestName : '';
$uniqueGuestId = !empty($guestName) ? $guestName : $this->secureRandom->generate(12);
$uniqueGuestId = !empty($guestName)
? $guestName . '(guest)' // make it harder to impersonate users.
: $this->secureRandom->generate(12);
$color = $this->avatarManager->getGuestAvatar($uniqueGuestId)->avatarBackgroundColor($uniqueGuestId);
return $color->name();
}
Expand Down
4 changes: 2 additions & 2 deletions src/apis/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router'
import type { Connection } from '../composables/useConnection.js'
import type { Document, Session } from '../services/SyncService.js'
import type { Document, GuestSession, Session } from '../services/SyncService.js'

export interface OpenParams {
fileId?: number
Expand Down Expand Up @@ -58,7 +58,7 @@ export async function open(
export async function update(
guestName: string,
connection: Connection,
): Promise<Session> {
): Promise<GuestSession> {
if (!connection.shareToken) {
throw new Error('Cannot set guest name without a share token!')
}
Expand Down
83 changes: 10 additions & 73 deletions src/components/Editor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
<Status
:document="document"
:dirty="dirty"
:sessions="filteredSessions"
:sync-error="syncError"
:has-connection-issue="requireReconnect" />
</ReadonlyBar>
Expand All @@ -48,7 +47,6 @@
<Status
:document="document"
:dirty="dirty"
:sessions="filteredSessions"
:sync-error="syncError"
:has-connection-issue="requireReconnect" />
<slot name="header" />
Expand Down Expand Up @@ -85,7 +83,7 @@ import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
import { File } from '@nextcloud/files'
import { Collaboration } from '@tiptap/extension-collaboration'
import { useElementSize } from '@vueuse/core'
import Vue, { defineComponent, ref, set, shallowRef, watch } from 'vue'
import { defineComponent, ref, shallowRef, watch } from 'vue'
import { Doc } from 'yjs'
import Autofocus from '../extensions/Autofocus.js'

Expand All @@ -94,7 +92,6 @@ import { provideEditorFlags } from '../composables/useEditorFlags.ts'
import { ATTACHMENT_RESOLVER, FILE, IS_MOBILE } from './Editor.provider.ts'
import ReadonlyBar from './Menu/ReadonlyBar.vue'

import { t } from '@nextcloud/l10n'
import { generateRemoteUrl } from '@nextcloud/router'
import { Awareness } from 'y-protocols/awareness.js'
import { provideConnection } from '../composables/useConnection.ts'
Expand Down Expand Up @@ -177,7 +174,6 @@ export default defineComponent({
props: {
richWorkspace: {
type: Boolean,
require: false,
default: false,
},
initialSession: {
Expand Down Expand Up @@ -258,7 +254,7 @@ export default defineComponent({
const { applyEditorWidth } = provideEditorWidth()
applyEditorWidth()

const { setEditable } = useEditorMethods(editor)
const { setEditable, updateUser } = useEditorMethods(editor)

const serialize = isRichEditor
? () =>
Expand All @@ -278,6 +274,7 @@ export default defineComponent({

return {
awareness,
connection,
editor,
el,
hasConnectionIssue,
Expand All @@ -292,8 +289,8 @@ export default defineComponent({
setEditable,
syncProvider,
syncService,
updateUser,
width,
connection,
ydoc,
}
},
Expand All @@ -303,12 +300,8 @@ export default defineComponent({
IDLE_TIMEOUT,

document: null,
sessions: [],
currentSession: null,
fileNode: null,

filteredSessions: {},

idle: false,
dirty: false,
contentLoaded: false,
Expand Down Expand Up @@ -342,7 +335,7 @@ export default defineComponent({
: '/'
},
displayed() {
return (this.currentSession && this.active) || this.syncError
return (this.connection && this.active) || this.syncError
},
showLoadingSkeleton() {
return (!this.contentLoaded || !this.displayed) && !this.syncError
Expand Down Expand Up @@ -493,72 +486,24 @@ export default defineComponent({
this.idle = false
},

updateSessions(sessions) {
this.sessions = sessions.sort((a, b) => b.lastContact - a.lastContact)

// Make sure we get our own session updated
// This should ideally be part of a global store where we can have that updated on the actual name change for guests
const currentUpdatedSession = this.sessions.find(
(session) => session.id === this.currentSession.id,
)
set(this, 'currentSession', currentUpdatedSession)

const currentSessionIds = this.sessions.map((session) => session.userId)
const currentGuestIds = this.sessions.map((session) => session.guestId)

const removedSessions = Object.keys(this.filteredSessions).filter(
(sessionId) =>
!currentSessionIds.includes(sessionId)
&& !currentGuestIds.includes(sessionId),
)

for (const index in removedSessions) {
Vue.delete(this.filteredSessions, removedSessions[index])
}
for (const index in this.sessions) {
const session = this.sessions[index]
const sessionKey = session.displayName ? session.userId : session.id
if (this.filteredSessions[sessionKey]) {
// update timestamp if relevant
if (
this.filteredSessions[sessionKey].lastContact
< session.lastContact
) {
set(
this.filteredSessions[sessionKey],
'lastContact',
session.lastContact,
)
}
} else {
set(this.filteredSessions, sessionKey, session)
}
if (session.id === this.currentSession.id) {
set(this.filteredSessions[sessionKey], 'isCurrent', true)
}
}
},

onOpened({ document, session, content, documentState, readOnly }) {
this.currentSession = session
this.document = document
this.readOnly = readOnly
this.editMode = !readOnly && !this.openReadOnlyEnabled
this.hasConnectionIssue = false

this.setEditable(this.editMode)
localStorage.setItem('nick', this.currentSession.guestName)
this.$attachmentResolver = new AttachmentResolver({
session: this.currentSession,
session,
user: getCurrentUser(),
shareToken: this.shareToken,
currentDirectory: this.currentDirectory,
})
if (this.currentSession?.userId && this.relativePath?.length) {
if (session.userId && this.relativePath?.length) {
const node = new File({
id: this.fileId,
source: generateRemoteUrl(
`dav/files/${this.currentSession.userId}${this.relativePath}`,
`dav/files/${session.userId}${this.relativePath}`,
),
mime: this.mime,
})
Expand All @@ -577,18 +522,10 @@ export default defineComponent({
})
}
})
const user = {
name: session?.userId
? session.displayName
: session?.guestName || t('text', 'Guest'),
color: session?.color,
clientId: this.ydoc.clientID,
}
this.editor.commands.updateUser(user)
this.updateUser(session)
},

onChange({ document, sessions }) {
this.updateSessions.bind(this)(sessions)
onChange({ document }) {
this.document = document

this.syncError = null
Expand Down
37 changes: 21 additions & 16 deletions src/components/Editor/AvatarWrapper.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,51 +7,48 @@
<div class="avatar-wrapper" :style="sessionAvatarStyle">
<NcAvatar
v-if="session.userId"
:user="session.userId ? session.userId : session.guestName"
:is-guest="session.userId === null"
:user="session.userId"
:is-guest="false"
:disable-menu="true"
hide-status
:disable-tooltip="true"
:size="size" />
:disable-tooltip="true" />
<div v-else class="avatar" :style="sessionBackgroundStyle">
{{ guestInitial }}
<template v-if="session.guestName">
{{ guestInitial }}
</template>
<AccountOutlineIcon v-else />
</div>
</div>
</template>

<script>
import NcAvatar from '@nextcloud/vue/components/NcAvatar'
import AccountOutlineIcon from 'vue-material-design-icons/AccountOutline.vue'

export default {
name: 'AvatarWrapper',
components: {
NcAvatar,
AccountOutlineIcon,
},
props: {
session: {
type: Object,
required: true,
},
size: {
type: Number,
default: () => 32,
},
},
computed: {
sessionAvatarStyle() {
return {
...this.sessionBackgroundStyle,
'border-color': this.session.color,
'border-width': '2px',
'border-style': 'solid',
'--size': this.size + 'px',
'--font-size': this.size / 2 + 'px',
}
},
sessionBackgroundStyle() {
return {
'background-color': this.session.userId
? this.session.color + ' !important'
: '#b9b9b9',
: 'var(--color-background-dark)',
}
},
guestInitial() {
Expand All @@ -64,18 +61,26 @@ export default {
</script>

<style lang="scss" scoped>
.avatar-wrapper {
overflow: hidden;
}

.avatar,
.avatar-wrapper {
--size: var(--default-clickable-area);
border-radius: 50%;
width: var(--size);
height: var(--size);
text-align: center;
color: #ffffff;
color: var(--color-text-maxcontrast);
line-height: var(--size);
font-size: var(--font-size);
font-size: calc(var(--size) / 2);
font-weight: normal;
display: flex;
justify-content: center;
align-items: center;
border-width: 2px;
border-style: solid;
box-sizing: border-box;
}
</style>
4 changes: 2 additions & 2 deletions src/components/Editor/DocumentStatus.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export default {
props: {
idle: {
type: Boolean,
require: true,
required: true,
},
lock: {
type: Object,
Expand All @@ -53,7 +53,7 @@ export default {
},
hasConnectionIssue: {
type: Boolean,
require: true,
required: true,
},
},

Expand Down
Loading
Loading