sharing: share a note from the web, at view or edit, with anyone on the instance
CI & Build / Python lint (push) Successful in 3s
CI & Build / Build now, or wait for Android? (push) Successful in 4s
Android / Build, or is the channel already serving this? (push) Successful in 4s
Android / Kotlin + Rust (APK) (push) Skipped
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Web typecheck and unit tests (push) Successful in 9s
CI & Build / Python tests (push) Successful in 14s
CI & Build / integration (push) Failing after 54s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 2m6s
Desktop (Tauri) / Update manifest (push) Canceled after 0s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Canceled after 2m25s
Desktop (Tauri) / Tauri desktop (Linux) (push) Canceled after 2m33s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Build now, or wait for Android? (push) Successful in 4s
Android / Build, or is the channel already serving this? (push) Successful in 4s
Android / Kotlin + Rust (APK) (push) Skipped
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Web typecheck and unit tests (push) Successful in 9s
CI & Build / Python tests (push) Successful in 14s
CI & Build / integration (push) Failing after 54s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 2m6s
Desktop (Tauri) / Update manifest (push) Canceled after 0s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Canceled after 2m25s
Desktop (Tauri) / Tauri desktop (Linux) (push) Canceled after 2m33s
The ACL has gated every read since M0, but nothing could write a share. Server: - shares_api: GET /api/users/directory (everyone but you, signed-in only), GET/POST /api/notes/<id>/shares and DELETE …/shares/<share_id>, owner only. Sharing again with the same person changes the permission (ON CONFLICT on the new unique index). - acl.visible_to_user takes permission=; granted_to and shared_ids feed the serializer. - Edit covers body and checklist (_get_editable). Everything else stays _get_owned. A view share's write is a 404 like a stranger's (#1984). An editor's PATCH naming anything but body is a 403. - Serialized notes carry permission, shared and shared_by. A recipient never gets the owner's labels, and a #tag an editor types files under the owner's (it always went to note.owner_id). - ?shared=with_me, also allowed in saved views. Trash and reminders are the owner's. purge_note drops the note's shares. - Migration 0034: one share per note and person (and per group), permission limited to view and edit, an index for "shared with me". Web: - ShareDialog (one, mounted by the shell): pick a member, Can view or Can edit, change or remove existing shares, with loading, error and empty states. - Card: "Shared by X" or "Shared" chip; owner-only actions and reminder buttons hidden for recipients; checkboxes inert at view. - Editor: read-only at view; text and checklist only at edit; Share button for the owner. - FilterBar: Shared with me. Repo seam gains `shares`; the offline desktop shows none of it (#5175 brings sharing there). Tests: owner, recipient and stranger across reads, every write at view and edit, tag filing, unshare, trash, delete and validation; web unit tests for the facet and permission helpers. #5174. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,8 +13,14 @@ export interface NoteFacets {
|
||||
has_attachment?: boolean;
|
||||
created_after?: string;
|
||||
created_before?: string;
|
||||
/** "with_me": only notes other people have shared with you (#5174). */
|
||||
shared?: "with_me";
|
||||
}
|
||||
|
||||
/** How the signed-in person holds a note. A note from a source that knows nothing of
|
||||
* sharing (the offline desktop) has none, and is theirs. */
|
||||
export type NotePermission = "owner" | "edit" | "view";
|
||||
|
||||
export interface NoteLabel {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -86,6 +92,13 @@ export interface Note {
|
||||
previews: LinkPreview[];
|
||||
created_at: string | null;
|
||||
updated_at: string | null;
|
||||
// Sharing (#5174). `edit` may change the body and checklist; everything else is the
|
||||
// owner's. A recipient's copy arrives without the owner's labels.
|
||||
permission?: NotePermission;
|
||||
/** The owner's own note: whether it is shared with anyone. */
|
||||
shared?: boolean;
|
||||
/** Someone else's note: who shared it. */
|
||||
shared_by?: { id: string; display_name: string } | null;
|
||||
}
|
||||
|
||||
export const useNotesStore = defineStore("notes", () => {
|
||||
@@ -108,6 +121,7 @@ export const useNotesStore = defineStore("notes", () => {
|
||||
const inView =
|
||||
v === "trash" ? n.trashed : v === "archived" ? !n.trashed && n.archived : !n.trashed && !n.archived;
|
||||
if (!inView) return false;
|
||||
if (activeFacets.value.shared === "with_me" && (n.permission ?? "owner") === "owner") return false;
|
||||
if (activeLabel.value) return n.labels.some((lb) => lb.id === activeLabel.value);
|
||||
return true;
|
||||
}
|
||||
@@ -262,9 +276,16 @@ export const useNotesStore = defineStore("notes", () => {
|
||||
return note;
|
||||
}
|
||||
|
||||
/** The Share dialog changed who a note is shared with; the card's indicator follows. */
|
||||
function setShared(id: string, shared: boolean): void {
|
||||
const note = items.value.find((n) => n.id === id);
|
||||
if (note) note.shared = shared;
|
||||
}
|
||||
|
||||
return {
|
||||
items,
|
||||
loading,
|
||||
setShared,
|
||||
view,
|
||||
activeLabel,
|
||||
activeFacets,
|
||||
|
||||
@@ -53,7 +53,20 @@ export const useUiStore = defineStore("ui", () => {
|
||||
run?.();
|
||||
}
|
||||
|
||||
// The note whose Share dialog is open (#5174), or null. One dialog for the app,
|
||||
// mounted by the shell, so the card and the editor open the same one.
|
||||
const shareNoteId = ref<string | null>(null);
|
||||
function openShare(id: string) {
|
||||
shareNoteId.value = id;
|
||||
}
|
||||
function closeShare() {
|
||||
shareNoteId.value = null;
|
||||
}
|
||||
|
||||
return {
|
||||
shareNoteId,
|
||||
openShare,
|
||||
closeShare,
|
||||
composeTick,
|
||||
composeSeed,
|
||||
requestCompose,
|
||||
|
||||
Reference in New Issue
Block a user