android: pin and archive a note someone shared with you
CI & Build / Python lint (push) Successful in 2s
CI & Build / Build now, or wait for Android? (push) Successful in 2s
Android / Build, or is the channel already serving this? (push) Successful in 2s
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 11s
CI & Build / integration (push) Successful in 1m24s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Failing after 1m30s
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
Android / Kotlin + Rust (APK) (push) Successful in 7m17s

The card's long-press menu and the editor's overflow now open on shared notes
with Pin and Archive; Labels, Share and Move to trash stay the owner's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-07 17:13:58 -04:00
co-authored by Claude Opus 5.5
parent 89cd1c76bb
commit 4f5459cb94
3 changed files with 28 additions and 18 deletions
@@ -85,8 +85,9 @@ fun EditorTopBar(
onConfirmDelete: () -> Unit, onConfirmDelete: () -> Unit,
onAction: (EditorAction) -> Unit, onAction: (EditorAction) -> Unit,
) { ) {
// Someone else's note (#5175): its text is all that may change here, so the bar // Someone else's note (#5175): its text, at `edit`, and this account's own pin
// keeps the checklist button and nothing that is the owner's to do. // and archive (#5176) are all that may change here, so the bar keeps the
// checklist button and an overflow of those two, and nothing that is the owner's.
val owner = access == NoteAccess.OWNER val owner = access == NoteAccess.OWNER
val dark = isSystemInDarkTheme() val dark = isSystemInDarkTheme()
TopAppBar( TopAppBar(
@@ -133,10 +134,10 @@ fun EditorTopBar(
) )
} }
} }
if (owner) { if (owner || !note.trashed) {
OverflowMenu( OverflowMenu(
note = note, note = note,
readOnly = readOnly, owner = owner,
onPicker = onPicker, onPicker = onPicker,
onShare = onShare, onShare = onShare,
onConfirmDelete = onConfirmDelete, onConfirmDelete = onConfirmDelete,
@@ -278,7 +279,7 @@ private fun savedLabel(
@Composable @Composable
private fun OverflowMenu( private fun OverflowMenu(
note: Note, note: Note,
readOnly: Boolean, owner: Boolean,
onPicker: (Picker) -> Unit, onPicker: (Picker) -> Unit,
onShare: () -> Unit, onShare: () -> Unit,
onConfirmDelete: () -> Unit, onConfirmDelete: () -> Unit,
@@ -291,7 +292,7 @@ private fun OverflowMenu(
Icon(Icons.Filled.MoreVert, contentDescription = stringResource(R.string.editor_more)) Icon(Icons.Filled.MoreVert, contentDescription = stringResource(R.string.editor_more))
} }
DropdownMenu(expanded = open, onDismissRequest = close) { DropdownMenu(expanded = open, onDismissRequest = close) {
if (readOnly) { if (note.trashed) {
MenuItem(R.string.editor_restore, close) { onAction(EditorAction.Restore) } MenuItem(R.string.editor_restore, close) { onAction(EditorAction.Restore) }
MenuItem(R.string.editor_delete_forever, close, onConfirmDelete) MenuItem(R.string.editor_delete_forever, close, onConfirmDelete)
} else { } else {
@@ -299,19 +300,23 @@ private fun OverflowMenu(
if (note.pinned) R.string.editor_unpin else R.string.editor_pin, if (note.pinned) R.string.editor_unpin else R.string.editor_pin,
close, close,
) { onAction(EditorAction.SetPinned(!note.pinned)) } ) { onAction(EditorAction.SetPinned(!note.pinned)) }
if (owner) {
MenuItem(R.string.editor_labels, close) { onPicker(Picker.LABELS) } MenuItem(R.string.editor_labels, close) { onPicker(Picker.LABELS) }
}
// Not on a draft: the server has nothing to share until it is saved. // Not on a draft: the server has nothing to share until it is saved.
if (note.id != DRAFT_ID) { if (owner && note.id != DRAFT_ID) {
MenuItem(R.string.editor_share, close, onShare) MenuItem(R.string.editor_share, close, onShare)
} }
MenuItem( MenuItem(
if (note.archived) R.string.editor_unarchive else R.string.editor_archive, if (note.archived) R.string.editor_unarchive else R.string.editor_archive,
close, close,
) { onAction(EditorAction.SetArchived(!note.archived)) } ) { onAction(EditorAction.SetArchived(!note.archived)) }
if (owner) {
MenuItem(R.string.editor_trash, close) { onAction(EditorAction.Trash) } MenuItem(R.string.editor_trash, close) { onAction(EditorAction.Trash) }
} }
} }
} }
}
} }
/** /**
@@ -5,9 +5,10 @@ import com.fabledsword.inkwell.core.Note
/** /**
* How this account holds a note (#5175), read from the core's `permission`. * How this account holds a note (#5175), read from the core's `permission`.
* *
* Someone else's note at [EDIT] is theirs to have its TEXT changed here and nothing * Someone else's note at [EDIT] may have its TEXT changed here; at [VIEW] it may
* else; at [VIEW] nothing changes here at all. The core refuses the rest anyway — * not. Either way its pin and archive are this account's own (#5176). The core
* this only keeps the editor from offering what would be refused. * refuses the rest anyway — this only keeps the editor from offering what would be
* refused.
*/ */
enum class NoteAccess { OWNER, EDIT, VIEW } enum class NoteAccess { OWNER, EDIT, VIEW }
@@ -19,10 +20,10 @@ val Note.access: NoteAccess
else -> NoteAccess.OWNER else -> NoteAccess.OWNER
} }
/** Nothing changes here: the note is in the trash, or shared with us to view. */ /** Its content can't change here: it is in the trash, or shared with us to view. */
val Note.readOnlyHere: Boolean val Note.readOnlyHere: Boolean
get() = trashed || access == NoteAccess.VIEW get() = trashed || access == NoteAccess.VIEW
/** The owner's own controls apply: tags, reminder, files, previews, the menu. */ /** The owner's own controls apply: tags, reminder, files, previews, share, trash. */
val Note.ownerControlsHere: Boolean val Note.ownerControlsHere: Boolean
get() = !readOnlyHere && access == NoteAccess.OWNER get() = !readOnlyHere && access == NoteAccess.OWNER
@@ -229,9 +229,11 @@ private fun NoteMenu(
onAction: (EditorAction) -> Unit, onAction: (EditorAction) -> Unit,
onConfirmDelete: () -> Unit, onConfirmDelete: () -> Unit,
) { ) {
// Every item here is the owner's to do, so someone else's note has no menu at all // Pin and archive are this account's own on someone else's note too (#5176);
// rather than an empty one (#5175). // trash is the owner's. A note shared with us never reaches our Trash, so the
DropdownMenu(expanded = expanded && note.access == NoteAccess.OWNER, onDismissRequest = onDismiss) { // trashed set is only ever the owner's.
val owner = note.access == NoteAccess.OWNER
DropdownMenu(expanded = expanded && (owner || !note.trashed), onDismissRequest = onDismiss) {
if (note.trashed) { if (note.trashed) {
MenuItem(R.string.editor_restore, onDismiss) { onAction(EditorAction.Restore) } MenuItem(R.string.editor_restore, onDismiss) { onAction(EditorAction.Restore) }
MenuItem(R.string.editor_delete_forever, onDismiss, onConfirmDelete) MenuItem(R.string.editor_delete_forever, onDismiss, onConfirmDelete)
@@ -244,9 +246,11 @@ private fun NoteMenu(
if (note.archived) R.string.editor_unarchive else R.string.editor_archive, if (note.archived) R.string.editor_unarchive else R.string.editor_archive,
onDismiss, onDismiss,
) { onAction(EditorAction.SetArchived(!note.archived)) } ) { onAction(EditorAction.SetArchived(!note.archived)) }
if (owner) {
MenuItem(R.string.editor_trash, onDismiss) { onAction(EditorAction.Trash) } MenuItem(R.string.editor_trash, onDismiss) { onAction(EditorAction.Trash) }
} }
} }
}
} }
/** /**