Skip to content
Merged
Show file tree
Hide file tree
Changes from 60 commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
7d8327d
docs: define stable list collaboration design
absolutepraya Aug 15, 2026
fa87b82
test: cover recursive list collaboration permissions
absolutepraya Aug 15, 2026
978b88b
feat: store recursive collaboration scope
absolutepraya Aug 15, 2026
a45fb5c
feat: add recursive list collaboration permissions
absolutepraya Aug 15, 2026
8584058
test(web): cover stable collaboration UI semantics
absolutepraya Aug 15, 2026
5e8aa36
feat(web): graduate list collaboration UI
absolutepraya Aug 15, 2026
3ef8cb3
feat(mobile): add stable list collaboration management
absolutepraya Aug 15, 2026
6311652
fix: integrate stable collaboration stack
absolutepraya Aug 15, 2026
f1888c9
test: cover stable list invitation lifecycle
absolutepraya Aug 15, 2026
5d779d9
fix: support older TypeScript targets in invitation email
absolutepraya Aug 15, 2026
b389917
chore: temporarily print React Doctor diagnostics
absolutepraya Aug 15, 2026
5ac6fdb
chore: temporarily print formatter diff
absolutepraya Aug 15, 2026
9d46a19
chore: compact React Doctor diagnostics
absolutepraya Aug 15, 2026
d692e6d
fix: apply collaboration formatter output
absolutepraya Aug 15, 2026
206cf8c
fix: apply collaboration formatter output
absolutepraya Aug 15, 2026
9a0f869
fix(web): format invitation dates deterministically
absolutepraya Aug 15, 2026
d59f3b7
fix(web): harden stable collaborator management
absolutepraya Aug 15, 2026
6b116b9
fix(web): stabilize pending invitation display
absolutepraya Aug 15, 2026
8b21e8c
chore: restore normal format gate
absolutepraya Aug 15, 2026
da178e2
chore: restore React Doctor score gate
absolutepraya Aug 15, 2026
e25db4f
docs: document stable list collaboration
absolutepraya Aug 15, 2026
026cdf6
fix(mobile): align collaborator lifecycle UI
absolutepraya Aug 15, 2026
ab8529b
fix(mobile): stabilize invitation display
absolutepraya Aug 15, 2026
0a738dc
merge: update stable collaboration branch from main
absolutepraya Aug 15, 2026
95621ad
ci: expose formatter diff for collaboration branch
absolutepraya Aug 15, 2026
655754f
ci: capture formatter output for collaboration branch
absolutepraya Aug 15, 2026
6b3353b
ci: apply formatter output to collaboration branch
absolutepraya Aug 15, 2026
48721b6
fix(web): preserve list header collaboration API
absolutepraya Aug 15, 2026
639620b
ci: skip duplicate hook for formatter commit
absolutepraya Aug 15, 2026
da04a8d
style: apply formatter
github-actions[bot] Aug 15, 2026
b144274
ci: restore standard validation workflow
absolutepraya Aug 15, 2026
a0e1dbb
fix(db): preserve migration journal history
absolutepraya Aug 15, 2026
474beb8
test: cover recursive collaboration offline revocation
absolutepraya Aug 15, 2026
e5ccf06
test: cover future recursive child offline sync
absolutepraya Aug 15, 2026
4093255
perf: batch recursive collaboration access resolution
absolutepraya Aug 15, 2026
3629902
fix(email): bound SMTP delivery time
absolutepraya Aug 15, 2026
c824a5d
test: throttle repeated invitation resend
absolutepraya Aug 15, 2026
f938263
fix: throttle list invitation resend
absolutepraya Aug 15, 2026
35017bb
fix(mobile): gate collaborator management by ownership
absolutepraya Aug 15, 2026
5c71d38
chore: stage collaboration backend review fixes
absolutepraya Aug 15, 2026
1ed3ed7
ci: apply collaboration backend review patch
absolutepraya Aug 15, 2026
7a234e1
fix: reconcile recursive collaboration state
github-actions[bot] Aug 15, 2026
e86acc8
chore: remove one-shot collaboration patch workflow
absolutepraya Aug 15, 2026
29f2f2b
chore: remove one-shot collaboration patch script
absolutepraya Aug 15, 2026
355e7d8
test: cover collaboration tree and metadata safety
absolutepraya Aug 15, 2026
1417c1a
ci: stage collaboration i18n cleanup
absolutepraya Aug 15, 2026
1e68024
ci: replace failed one-shot i18n helper
absolutepraya Aug 15, 2026
d136399
ci: apply stable collaboration i18n cleanup
absolutepraya Aug 15, 2026
d1494ec
chore: remove one-shot collaboration i18n helper
absolutepraya Aug 15, 2026
9d6adcc
ci: apply collaboration i18n patch
absolutepraya Aug 15, 2026
ee1dc20
ci: restore standard validation workflow
absolutepraya Aug 15, 2026
d6bf564
i18n: add stable collaboration namespace
absolutepraya Aug 15, 2026
5a91eda
fix(web): localize collaborator management copy
absolutepraya Aug 15, 2026
5b0f411
fix(web): localize invitation inbox copy
absolutepraya Aug 15, 2026
fa02fbc
refactor(web): keep collaboration UI helpers locale-neutral
absolutepraya Aug 15, 2026
e412102
test(web): keep collaboration helper tests locale-neutral
absolutepraya Aug 15, 2026
27afe67
test: preserve direct parent during recursive revocation
absolutepraya Aug 15, 2026
a6a663c
test: define accessible shared-list hierarchy
absolutepraya Aug 15, 2026
62f0a01
chore: apply accessible hierarchy patch
absolutepraya Aug 15, 2026
58e4830
fix: preserve accessible shared-list hierarchy
github-actions[bot] Aug 15, 2026
82ff718
docs: define Drive-style accessible shared hierarchy
absolutepraya Aug 15, 2026
dc5d051
docs: explain accessible shared-list navigation
absolutepraya Aug 15, 2026
1f6a25e
feat: add accessible list breadcrumbs
absolutepraya Aug 15, 2026
3189705
feat: add mobile shared-list up navigation
absolutepraya Aug 15, 2026
d9b9f75
docs: define accessible shared hierarchy
absolutepraya Aug 15, 2026
d3dcab6
chore: run formatter once
absolutepraya Aug 15, 2026
f264103
style: apply formatter
github-actions[bot] Aug 15, 2026
194bdf6
docs: record shared-list up navigation
absolutepraya Aug 15, 2026
3413d03
chore: apply final collaboration review fixes
absolutepraya Aug 15, 2026
f3145b0
fix: batch collaboration access snapshots
github-actions[bot] Aug 15, 2026
01d4605
chore: format final collaboration fixes
absolutepraya Aug 15, 2026
ea898ff
style: format final collaboration fixes
github-actions[bot] Aug 15, 2026
6988f92
docs: document shared-list up navigation
absolutepraya Aug 15, 2026
b13a63d
fix: improve shared-list navigation accessibility
absolutepraya Aug 15, 2026
511e476
fix: add semantic shared-list breadcrumbs
absolutepraya Aug 15, 2026
80e5dff
fix: enlarge mobile shared-list up target
absolutepraya Aug 15, 2026
8107630
chore: harden invitation lifecycle
absolutepraya Aug 15, 2026
8af307c
fix: harden invitation lifecycle
github-actions[bot] Aug 15, 2026
5cbdf93
docs: align stable collaboration design with implementation
absolutepraya Aug 15, 2026
c4f87c7
docs: align collaboration implementation plan
absolutepraya Aug 15, 2026
bd2e1c4
chore: align declined invitation tests
absolutepraya Aug 15, 2026
2c73edf
chore: fix final collaboration CI blockers
absolutepraya Aug 15, 2026
5a08730
fix: align collaboration tests and access types
github-actions[bot] Aug 15, 2026
b6de1d6
docs: clarify declined invitation domain state
absolutepraya Aug 15, 2026
c2f9f99
fix: register collaboration i18n namespace
absolutepraya Aug 15, 2026
5bab6a4
chore: fold collaboration translations into typed namespace
absolutepraya Aug 15, 2026
d94d98e
fix: use typed collaboration translations
github-actions[bot] Aug 15, 2026
669222a
docs: record typed collaboration translation namespace
absolutepraya Aug 15, 2026
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
24 changes: 24 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Domain Context

## List collaboration

- **List owner**: the user who owns a list. Ownership is not a collaborator membership and is never inherited.
- **Collaborator**: a non-owner user with accepted access to a manual list as `viewer` or `editor`.
- **Direct grant**: a collaborator membership attached to one specific list.
- **Recursive grant**: a direct grant whose role may be inherited by descendants of that list, including descendants created or moved into the subtree later.
- **Inherited access**: effective access to a list obtained from the nearest ancestor direct grant for the same user whose recursive flag is enabled.
- **Effective role**: the role used for authorization on a list. A direct grant on the list wins. Otherwise the nearest recursive ancestor grant wins. Otherwise the user has no collaborator access.
- **Invitation**: a pending offer for a direct grant. Invitations may request viewer/editor access and may optionally request recursive sharing.
- **Invitation expiry**: a pending invitation is valid for 30 days from `invitedAt`. Resending renews `invitedAt` for another 30 days.
- **Public access**: anonymous read-only access to a list through its public-list mechanism. Public access never creates collaborator membership and is independent from collaboration.
- **Contributed bookmark membership**: a bookmark-to-list association created through a collaborator membership. Removing that direct membership removes associations tied to it, but never deletes the underlying bookmark.

## Permission rules

- Collaboration applies only to manual lists.
- Viewers are read-only. Editors can add/remove bookmark memberships. Only owners manage list metadata and collaborators.
- Recursive sharing is opt-in per direct grant and defaults off.
- Current and future descendants can inherit a recursive grant.
- Moving a descendant out of a recursively shared subtree removes access that existed only through that inheritance.
- Explicit direct access on a descendant overrides an inherited role on that descendant. Descendants inherit from the nearest recursive direct grant available on their own ancestor chain.
- Public sharing and collaborator sharing can coexist on the same list.
22 changes: 16 additions & 6 deletions apps/mobile/app/dashboard/(tabs)/(lists)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,14 @@ export default function Lists() {
const api = useTRPC();
const queryClient = useQueryClient();
const { data: listStats } = useQuery(api.lists.stats.queryOptions());
const { data: pendingInvitations } = useQuery(
api.lists.getPendingInvitations.queryOptions(),
);

// Check if there are any shared lists
const hasSharedLists = useMemo(() => {
return lists?.data.some((list) => list.userRole !== "owner") ?? false;
}, [lists?.data]);

// Check if any list has children to determine if we need chevron spacing
const hasAnyListsWithChildren = useMemo(() => {
const checkForChildren = (node: ZBookmarkListTreeNode): boolean => {
if (node.children && node.children.length > 0) return true;
Expand All @@ -111,6 +112,7 @@ export default function Lists() {
const onRefresh = () => {
queryClient.invalidateQueries(api.lists.list.pathFilter());
queryClient.invalidateQueries(api.lists.stats.pathFilter());
queryClient.invalidateQueries(api.lists.getPendingInvitations.pathFilter());
};

const links: ListLink[] = [
Expand All @@ -134,9 +136,19 @@ export default function Lists() {
},
];

// Add shared lists section if there are any
if (pendingInvitations && pendingInvitations.length > 0) {
links.push({
id: "pending-invitations",
logo: "✉️",
name: `List Invitations (${pendingInvitations.length})`,
href: "/dashboard/lists/invitations",
level: 0,
numChildren: 0,
collapsed: false,
});
}

if (hasSharedLists) {
// Count shared lists to determine if section has children
const sharedListsCount = Object.values(lists.root).filter(
(list) => list.item.userRole !== "owner",
).length;
Expand All @@ -152,7 +164,6 @@ export default function Lists() {
isSharedSection: true,
});

// Add shared lists as children if section is expanded
if (showChildrenOf["shared-section"]) {
Object.values(lists.root).forEach((list) => {
if (list.item.userRole !== "owner") {
Expand All @@ -169,7 +180,6 @@ export default function Lists() {
}
}

// Add owned lists only
Object.values(lists.root).forEach((list) => {
if (list.item.userRole === "owner") {
traverseTree(list, links, showChildrenOf, listStats?.stats);
Expand Down
Loading