diff --git a/ios/Keep/Services/KeepAPI.swift b/ios/Keep/Services/KeepAPI.swift index f38fde4..3e66891 100644 --- a/ios/Keep/Services/KeepAPI.swift +++ b/ios/Keep/Services/KeepAPI.swift @@ -56,6 +56,16 @@ actor KeepAPI { _ = try await request("/api/notes/\(id)", method: "DELETE", as: OkResponse.self) } + /// Mints a public share token (or returns the existing one — the server + /// COALESCEs) for the note. + func share(id: String) async throws -> Note { + try await request("/api/notes/\(id)/share", method: "POST", as: NoteResponse.self).note + } + + func unshare(id: String) async throws -> Note { + try await request("/api/notes/\(id)/share", method: "DELETE", as: NoteResponse.self).note + } + // MARK: - Plumbing private func request( diff --git a/ios/Keep/Services/NotesStore.swift b/ios/Keep/Services/NotesStore.swift index 4806caf..3d7b76a 100644 --- a/ios/Keep/Services/NotesStore.swift +++ b/ios/Keep/Services/NotesStore.swift @@ -82,6 +82,26 @@ final class NotesStore { await update(note.id, patch: ["color": key ?? NSNull()]) } + /// Ensures the note has a public share link; returns the updated note. + @discardableResult + func share(_ note: Note) async -> Note? { + do { + let updated = try await api.share(id: note.id) + if let i = notes.firstIndex(where: { $0.id == note.id }) { notes[i] = updated } + return updated + } catch { + handle(error) + return nil + } + } + + func unshare(_ note: Note) async { + do { + let updated = try await api.unshare(id: note.id) + if let i = notes.firstIndex(where: { $0.id == note.id }) { notes[i] = updated } + } catch { handle(error) } + } + // MARK: - Auth /// Signs in, then reloads. Returns an error message to show, or nil on success. diff --git a/ios/KeepMac/KeepMacApp.swift b/ios/KeepMac/KeepMacApp.swift index c277cac..0f41029 100644 --- a/ios/KeepMac/KeepMacApp.swift +++ b/ios/KeepMac/KeepMacApp.swift @@ -68,6 +68,18 @@ struct NoteCommands: Commands { Divider() + Button("Copy Share Link") { + if let note { Task { await store.copyShareLink(note) } } + } + .disabled(note == nil || note?.trashed == true) + + Button("Stop Sharing") { + if let note { Task { await store.unshare(note) } } + } + .disabled(note?.shareToken == nil) + + Divider() + Button("Move to Trash") { if let note { Task { await store.trash(note) } } } diff --git a/ios/KeepMac/MacRootView.swift b/ios/KeepMac/MacRootView.swift index c899897..29fdbd1 100644 --- a/ios/KeepMac/MacRootView.swift +++ b/ios/KeepMac/MacRootView.swift @@ -177,6 +177,11 @@ struct MacRootView: View { } } Divider() + Button("Copy Share Link") { Task { await store.copyShareLink(note) } } + if note.shareToken != nil { + Button("Stop Sharing") { Task { await store.unshare(note) } } + } + Divider() Button("Move to Trash", role: .destructive) { Task { await store.trash(note) } } } } @@ -233,6 +238,11 @@ private struct MacNoteRow: View { } } Spacer() + if note.shareToken != nil { + Image(systemName: "link") + .font(.caption2) + .foregroundStyle(.secondary) + } if note.pinned { Image(systemName: "pin.fill") .font(.caption2) diff --git a/ios/KeepMac/ShareLink.swift b/ios/KeepMac/ShareLink.swift new file mode 100644 index 0000000..f1f80dd --- /dev/null +++ b/ios/KeepMac/ShareLink.swift @@ -0,0 +1,13 @@ +import AppKit + +/// Mac-side share helper: makes sure the public link exists, then puts it on +/// the pasteboard. Lives in the Mac target because of NSPasteboard. +extension NotesStore { + func copyShareLink(_ note: Note) async { + guard let token = (await share(note))?.shareToken else { return } + let url = Config.baseURL.appendingPathComponent("p/\(token)") + let pasteboard = NSPasteboard.general + pasteboard.clearContents() + pasteboard.setString(url.absoluteString, forType: .string) + } +} diff --git a/ios/screenshots/keepmac-share-links-20260709-204934.png b/ios/screenshots/keepmac-share-links-20260709-204934.png new file mode 100644 index 0000000..42e7408 Binary files /dev/null and b/ios/screenshots/keepmac-share-links-20260709-204934.png differ