sync: recipients keep their own pin, archive and place on shared notes
A note_user_state row per (note, recipient) holds what used to be the owner's columns as far as anyone else could tell. The board filters and orders through the viewer's own state; PATCH and reorder write it for a note shared at any level; the feed's revision for a shared note is the later of the note's and the caller's row, so a recipient's pin reaches their devices and no one else's. Push takes the three with their own `state_at` stamp (protocol 7, `shared_state`), so pinning a copy whose text is behind never makes that text win over the owner's edit. A body is only stamped as an edit when it changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
"""note_user_state: a recipient's own pin, archive and place for a shared note
|
||||
|
||||
Revision ID: 0036
|
||||
Revises: 0035
|
||||
Create Date: 2026-10-07
|
||||
|
||||
Pin, archive and board order are personal organization, and until now they were
|
||||
columns on `notes`, so on a shared note they could only ever mean the owner's
|
||||
(#5176). This table holds them for everyone else: one row per (note, person it is
|
||||
shared with) who has pinned, archived or moved it. The owner keeps the note's own
|
||||
columns; a recipient with no row sees the note unpinned, unarchived, in the owner's
|
||||
order.
|
||||
|
||||
`sync_revision` is stamped by the same `ts_set_sync_revision()` trigger the notes
|
||||
and labels use (0015), so a recipient's change reaches their own devices on the one
|
||||
cursor they already page by, and never moves the note on anyone else's.
|
||||
|
||||
## Downgrade
|
||||
|
||||
Drops the table. Recipients lose their own pins and archives; the notes are
|
||||
untouched.
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
|
||||
revision = "0036"
|
||||
down_revision = "0035"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"note_user_state",
|
||||
sa.Column("note_id", UUID(as_uuid=True), sa.ForeignKey("notes.id", ondelete="CASCADE"), nullable=False),
|
||||
sa.Column("user_id", UUID(as_uuid=True), sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=False),
|
||||
sa.Column("pinned", sa.Boolean(), nullable=False, server_default=sa.false()),
|
||||
sa.Column("archived", sa.Boolean(), nullable=False, server_default=sa.false()),
|
||||
sa.Column("position", sa.Integer(), nullable=True),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
|
||||
sa.Column("sync_revision", sa.BigInteger(), nullable=True),
|
||||
sa.PrimaryKeyConstraint("note_id", "user_id"),
|
||||
)
|
||||
op.create_index("ix_note_user_state_user_revision", "note_user_state", ["user_id", "sync_revision"])
|
||||
op.execute(
|
||||
"CREATE TRIGGER trg_note_user_state_sync_revision BEFORE INSERT OR UPDATE ON note_user_state "
|
||||
"FOR EACH ROW EXECUTE PROCEDURE ts_set_sync_revision()"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute("DROP TRIGGER IF EXISTS trg_note_user_state_sync_revision ON note_user_state")
|
||||
op.drop_index("ix_note_user_state_user_revision", table_name="note_user_state")
|
||||
op.drop_table("note_user_state")
|
||||
Reference in New Issue
Block a user