feat(sharing): pending-state invites, notification bell, sidebar polish
Shares used to activate instantly on the owner's side with no notice to
the recipient. Introduce a pending/accepted lifecycle so a recipient
gets a bell notification on login and explicitly Accept or Decline
before the shared item lands in their sidebar.
Backend
- Migration 0014 adds `status` + `accepted_at` to heap_shares and
folder_shares; pre-existing rows are backfilled to 'accepted' so
nothing disappears from anyone's current sidebar. One-migration trick:
server_default 'accepted' during add_column, then strip so new inserts
fall through to the Python model default 'pending'.
- New recipient-only endpoints: POST /sharing/{heaps|folders}/{id}/accept
(idempotent) and /decline (hard delete, so re-invites are clean).
- New GET /sharing/pending returning {heaps, folders} of outstanding
invites with target_name + owner_username + permission.
- list_shared_{heaps,folders} now filter to status='accepted' and carry
share_id so the recipient can Leave without a second lookup.
- ShareResponse exposes status so the owner sees pending invites.
Frontend
- NotificationBell lives in the LeftSidebar user row: a Popover
triggered by Bell with a count badge. Each row shows owner avatar,
"{owner} shared {heap|folder} {name}" with a permission subtitle,
and Accept / Decline inline. Polls /sharing/pending every 60s.
- Shared Avatar helper extracted to sharing/Avatar.tsx — used by
ShareDialog, NotificationBell, and the sidebar shared rows so one
user's identity colour is stable everywhere.
- Sidebar shared-row polish: owner avatar bubble + Eye/Pencil
permission icon (was uppercase pill). Right-click opens a context
menu with Open / Leave; Leave calls the existing recipient-revoke
DELETE and invalidates the shared-{heaps,folders} query.
- ShareDialog shows an amber "Invited" pill next to pending recipients.
- New shadcn context-menu primitive (radix dep already installed).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,12 +24,19 @@ class HeapShare(Base):
|
||||
owner_id = Column(String, ForeignKey("users.id"), nullable=False)
|
||||
shared_with_id = Column(String, ForeignKey("users.id"), nullable=False)
|
||||
permission = Column(String, nullable=False, default="read") # 'read' | 'write'
|
||||
# Lifecycle: 'pending' while the recipient hasn't acted, 'accepted'
|
||||
# once they've Accept'd in the notification bell. Decline deletes the
|
||||
# row outright — see migration 0014 for the backfill of pre-existing
|
||||
# rows to 'accepted' so nothing vanishes from existing sidebars.
|
||||
status = Column(String, nullable=False, default="pending")
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
accepted_at = Column(DateTime, nullable=True)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("heap_id", "shared_with_id", name="uq_heap_share"),
|
||||
Index("ix_heap_shares_shared_with", "shared_with_id"),
|
||||
Index("ix_heap_shares_heap_id", "heap_id"),
|
||||
Index("ix_heap_shares_shared_with_status", "shared_with_id", "status"),
|
||||
)
|
||||
|
||||
|
||||
@@ -43,10 +50,13 @@ class FolderShare(Base):
|
||||
owner_id = Column(String, ForeignKey("users.id"), nullable=False)
|
||||
shared_with_id = Column(String, ForeignKey("users.id"), nullable=False)
|
||||
permission = Column(String, nullable=False, default="read") # 'read' | 'write'
|
||||
status = Column(String, nullable=False, default="pending")
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
accepted_at = Column(DateTime, nullable=True)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("folder_id", "shared_with_id", name="uq_folder_share"),
|
||||
Index("ix_folder_shares_shared_with", "shared_with_id"),
|
||||
Index("ix_folder_shares_folder_id", "folder_id"),
|
||||
Index("ix_folder_shares_shared_with_status", "shared_with_id", "status"),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user