@@ -27,9 +27,13 @@ import { createPageCollabDoc } from "./page-yjs-doc";
2727import { usePageCollabEditor } from " ./usePageCollabEditor" ;
2828import { usePageManagement } from " ./usePageManagement" ;
2929import { usePagePathAndPrefs } from " ./usePagePathAndPrefs" ;
30- import { base64ToBytes } from " @deepnotes/e2ee" ;
31- import { decryptPageRelativeTitle } from " ./page-collab-crypto" ;
30+ import { base64ToBytes , bytesToBase64 } from " @deepnotes/e2ee" ;
31+ import {
32+ decryptPageRelativeTitle ,
33+ decryptPageAbsoluteTitle ,
34+ } from " ./page-collab-crypto" ;
3235import { usePagePathRealtimeTitles } from " ./usePagePathRealtimeTitles" ;
36+ import { buildRealtimeHset , sendRealtimeRequestBatch } from " ../realtime/realtime-user-ws" ;
3337import { usePageSnapshots } from " ./usePageSnapshots" ;
3438import PageLayout from " @/layouts/PageLayout.vue" ;
3539import PageStateScreens from " ./screens/PageStateScreens.vue" ;
@@ -169,31 +173,71 @@ const { pathPageLabels } = usePagePathRealtimeTitles({
169173 cryptoError ,
170174});
171175
172- // Merge realtime labels with the current page's bootstrap-encrypted title.
173- // usePagePathRealtimeTitles clears and refills pathPageLabels asynchronously,
174- // so a simple watcher would be overwritten. A computed always re-evaluates.
175- const pageLabels = computed <Record <string , string >>(() => {
176- const labels = { ... pathPageLabels .value };
176+ // Separate reactive overrides so local edits reflect immediately without
177+ // waiting for the async realtime round-trip.
178+ const editedRelativeTitle = ref <string | null >(null );
179+ const editedAbsoluteTitle = ref <string | null >(null );
180+
181+ watch (pageId , () => {
182+ editedRelativeTitle .value = null ;
183+ editedAbsoluteTitle .value = null ;
184+ });
185+
186+ const currentPageRelativeTitle = computed (() => {
187+ if (editedRelativeTitle .value != null ) return editedRelativeTitle .value ;
177188 const id = pageId .value ;
178- if (labels [id ] && labels [id ].length > 0 ) {
179- return labels ;
180- }
181189 const pk = pageKeyring .value ;
182190 const b64 = pageEncRelTitleB64 .value ;
183- if (! id || ! pk || ! b64 ) {
184- return labels ;
191+ if (! id || ! pk || ! b64 ) return ` [Page ${id }] ` ;
192+ try {
193+ const t = decryptPageRelativeTitle ({
194+ pageKeyring: pk ,
195+ pageId: id ,
196+ ciphertext: base64ToBytes (b64 ),
197+ });
198+ return t && t .length > 0 ? t : ` [Page ${id }] ` ;
199+ } catch {
200+ return ` [Page ${id }] ` ;
185201 }
202+ });
203+
204+ const currentPageAbsoluteTitle = computed (() => {
205+ if (editedAbsoluteTitle .value != null ) return editedAbsoluteTitle .value ;
206+ const id = pageId .value ;
207+ const fromRealtime = pathPageLabels .value [id ];
208+ if (fromRealtime && fromRealtime .length > 0 ) return fromRealtime ;
209+ const pk = pageKeyring .value ;
210+ const b64 = pageEncAbsTitleB64 .value ;
211+ if (! id || ! pk || ! b64 ) return ` [Page ${id }] ` ;
186212 try {
187- const title = decryptPageRelativeTitle ({
213+ const t = decryptPageAbsoluteTitle ({
188214 pageKeyring: pk ,
189215 pageId: id ,
190216 ciphertext: base64ToBytes (b64 ),
191217 });
192- if (title && title .length > 0 ) {
193- labels [id ] = title ;
194- }
218+ return t && t .length > 0 ? t : ` [Page ${id }] ` ;
219+ } catch {
220+ return ` [Page ${id }] ` ;
221+ }
222+ });
223+
224+ // Breadcrumb labels: absolute title from realtime, fallback to decrypted bootstrap.
225+ const pageLabels = computed <Record <string , string >>(() => {
226+ const labels = { ... pathPageLabels .value };
227+ const id = pageId .value ;
228+ if (labels [id ] && labels [id ].length > 0 ) return labels ;
229+ const pk = pageKeyring .value ;
230+ const b64 = pageEncAbsTitleB64 .value ;
231+ if (! id || ! pk || ! b64 ) return labels ;
232+ try {
233+ const t = decryptPageAbsoluteTitle ({
234+ pageKeyring: pk ,
235+ pageId: id ,
236+ ciphertext: base64ToBytes (b64 ),
237+ });
238+ if (t && t .length > 0 ) labels [id ] = t ;
195239 } catch {
196- // ignore decrypt failures
240+ // ignore
197241 }
198242 return labels ;
199243});
@@ -242,6 +286,42 @@ async function onUnlockWithPassword(password: string): Promise<boolean> {
242286 return ok ;
243287}
244288
289+ async function updatePageTitle(type : " relative" | " absolute" , value : string ) {
290+ const id = pageId .value ;
291+ const pk = pageKeyring .value ;
292+ if (! id || ! pk ) return ;
293+ try {
294+ const ciphertext = pk .encrypt (new TextEncoder ().encode (value ), {
295+ padding: true ,
296+ associatedData: {
297+ context:
298+ type === " relative" ? " PageRelativeTitle" : " PageAbsoluteTitle" ,
299+ pageId: id ,
300+ },
301+ });
302+ void sendRealtimeRequestBatch ([
303+ buildRealtimeHset (
304+ " page" ,
305+ id ,
306+ type === " relative"
307+ ? " encrypted-relative-title"
308+ : " encrypted-absolute-title" ,
309+ bytesToBase64 (ciphertext ),
310+ ),
311+ ]);
312+ // Optimistic update so the input stays in sync immediately.
313+ if (type === " relative" ) {
314+ editedRelativeTitle .value = value ;
315+ } else {
316+ editedAbsoluteTitle .value = value ;
317+ // Also update the shared labels map used by breadcrumb/cards.
318+ pathPageLabels .value = { ... pathPageLabels .value , [id ]: value };
319+ }
320+ } catch {
321+ // ignore encrypt failures
322+ }
323+ }
324+
245325onMounted (() => {
246326 if (! isAuthenticated .value ) {
247327 void router .replace ({
@@ -419,12 +499,12 @@ onMounted(() => {
419499 <PagePropertiesCard
420500 v-if =" ! selectedNoteId && ! selectedArrowId "
421501 :page-id =" pageId "
422- :relative-title =" pageLabels [ pageId ] "
423- :absolute-title =" pageLabels [ pageId ] "
502+ :relative-title =" currentPageRelativeTitle "
503+ :absolute-title =" currentPageAbsoluteTitle "
424504 :is-favorite =" isFavorite "
425505 :read-only =" cryptoError !== null "
426- @update :relative-title =" () => {} "
427- @update :absolute-title =" () => {} "
506+ @update :relative-title =" updatePageTitle ( ' relative ' , $event ) "
507+ @update :absolute-title =" updatePageTitle ( ' absolute ' , $event ) "
428508 @toggle-favorite =" toggleFavorite ()"
429509 />
430510
0 commit comments