feat(backend): chat list marks a deleted last message, history pages mark messages with a note
This commit is contained in:
1 parent
90b37ec708
commit
97f9082f5e
3 files changed
+148
-1
No files matched your search
@@ -0,0 +1,97 @@
|
||||
"""chat stats last deleted
|
||||
|
||||
Revision ID: f1c4a8e2d6b3
|
||||
Revises: e8b3f1a6c2d4
|
||||
Create Date: 2026-10-19 04:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "f1c4a8e2d6b3"
|
||||
down_revision: str | None = "e8b3f1a6c2d4"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
_APPLY = """
|
||||
CREATE OR REPLACE FUNCTION chat_stats_apply() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
IF TG_OP = 'INSERT' THEN
|
||||
INSERT INTO chat_stats AS cs (account_id, chat_id, message_count,
|
||||
last_date, last_message_id,
|
||||
last_text, last_sender_id{column})
|
||||
VALUES (NEW.account_id, NEW.chat_id, 1,
|
||||
CASE WHEN NEW.date <= now() + interval '1 day'
|
||||
THEN NEW.date END,
|
||||
CASE WHEN NEW.date <= now() + interval '1 day'
|
||||
THEN NEW.message_id END,
|
||||
NEW.text, NEW.sender_id{value})
|
||||
ON CONFLICT (account_id, chat_id) DO UPDATE SET
|
||||
message_count = cs.message_count + 1,
|
||||
last_date = CASE WHEN chat_stats_newer(cs, EXCLUDED)
|
||||
THEN EXCLUDED.last_date ELSE cs.last_date END,
|
||||
last_message_id = CASE WHEN chat_stats_newer(cs, EXCLUDED)
|
||||
THEN EXCLUDED.last_message_id
|
||||
ELSE cs.last_message_id END,
|
||||
last_text = CASE WHEN chat_stats_newer(cs, EXCLUDED)
|
||||
THEN EXCLUDED.last_text ELSE cs.last_text END,
|
||||
last_sender_id = CASE WHEN chat_stats_newer(cs, EXCLUDED)
|
||||
THEN EXCLUDED.last_sender_id
|
||||
ELSE cs.last_sender_id END{merge};
|
||||
ELSE
|
||||
UPDATE chat_stats
|
||||
SET last_text = NEW.text, last_sender_id = NEW.sender_id{update}
|
||||
WHERE account_id = NEW.account_id
|
||||
AND chat_id = NEW.chat_id
|
||||
AND last_message_id = NEW.message_id;
|
||||
END IF;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$
|
||||
"""
|
||||
|
||||
_DELETED_PARTS = {
|
||||
"column": ", last_deleted",
|
||||
"value": ", NEW.deleted_at IS NOT NULL",
|
||||
"merge": (
|
||||
",\n last_deleted = CASE WHEN chat_stats_newer(cs, EXCLUDED)"
|
||||
" THEN EXCLUDED.last_deleted ELSE cs.last_deleted END"
|
||||
),
|
||||
"update": ", last_deleted = NEW.deleted_at IS NOT NULL",
|
||||
"deleted": " OR OLD.deleted_at IS DISTINCT FROM NEW.deleted_at",
|
||||
}
|
||||
_PLAIN_PARTS = dict.fromkeys(_DELETED_PARTS, "")
|
||||
|
||||
_UPDATE_TRIGGER = (
|
||||
"CREATE TRIGGER messages_chat_stats_update AFTER UPDATE ON messages "
|
||||
"FOR EACH ROW WHEN (OLD.text IS DISTINCT FROM NEW.text "
|
||||
"OR OLD.sender_id IS DISTINCT FROM NEW.sender_id{deleted}) "
|
||||
"EXECUTE FUNCTION chat_stats_apply()"
|
||||
)
|
||||
|
||||
_BACKFILL = """
|
||||
UPDATE chat_stats cs SET last_deleted = true
|
||||
FROM messages m
|
||||
WHERE m.account_id = cs.account_id AND m.chat_id = cs.chat_id
|
||||
AND m.message_id = cs.last_message_id AND m.date = cs.last_date
|
||||
AND m.deleted_at IS NOT NULL
|
||||
"""
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute(
|
||||
"ALTER TABLE chat_stats ADD COLUMN last_deleted boolean NOT NULL DEFAULT false"
|
||||
)
|
||||
op.execute(_BACKFILL)
|
||||
op.execute(_APPLY.format(**_DELETED_PARTS))
|
||||
op.execute("DROP TRIGGER IF EXISTS messages_chat_stats_update ON messages")
|
||||
op.execute(_UPDATE_TRIGGER.format(**_DELETED_PARTS))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute("DROP TRIGGER IF EXISTS messages_chat_stats_update ON messages")
|
||||
op.execute(_UPDATE_TRIGGER.format(**_PLAIN_PARTS))
|
||||
op.execute(_APPLY.format(**_PLAIN_PARTS))
|
||||
op.execute("ALTER TABLE chat_stats DROP COLUMN last_deleted")
|
||||
@@ -80,7 +80,8 @@ _CHAT_ROWS = """
|
||||
WITH ids AS ({ids})
|
||||
SELECT ids.chat_id,
|
||||
COALESCE(cs.message_count, 0) AS message_count,
|
||||
cs.last_date, cs.last_text, cs.last_sender_id,
|
||||
cs.last_date, cs.last_text, cs.last_sender_id, cs.last_message_id,
|
||||
COALESCE(cs.last_deleted, false) AS last_deleted,
|
||||
COALESCE(named.title,
|
||||
NULLIF(trim(concat_ws(' ', p.first_name, p.last_name)), ''),
|
||||
p.username) AS title,
|
||||
@@ -149,6 +150,8 @@ def _chat_item(row: asyncpg.Record) -> ChatListItem:
|
||||
last_date=row["last_date"],
|
||||
last_text=row["last_text"],
|
||||
last_sender_id=row["last_sender_id"],
|
||||
last_message_id=row["last_message_id"],
|
||||
last_deleted=row["last_deleted"],
|
||||
)
|
||||
|
||||
|
||||
@@ -259,6 +262,7 @@ async def get_chat_history( # noqa: PLR0913
|
||||
views.append(_build_album(members, media_by_key))
|
||||
index = end
|
||||
await _apply_read_status(pool, account_id, chat_id, views)
|
||||
await _apply_notes(pool, account_id, chat_id, views)
|
||||
return views
|
||||
|
||||
|
||||
@@ -278,6 +282,7 @@ async def get_message(
|
||||
raw = load_raw(row["raw"])
|
||||
view = build_message_view(row, raw, _single_media(row, raw, media_by_key))
|
||||
await _apply_read_status(pool, account_id, chat_id, [view])
|
||||
await _apply_notes(pool, account_id, chat_id, [view])
|
||||
return view
|
||||
|
||||
|
||||
@@ -301,6 +306,23 @@ async def _apply_read_status(
|
||||
)
|
||||
|
||||
|
||||
async def _apply_notes(
|
||||
pool: asyncpg.Pool, account_id: int, chat_id: int, views: list[MessageView]
|
||||
) -> None:
|
||||
if not views:
|
||||
return
|
||||
rows = await pool.fetch(
|
||||
"SELECT DISTINCT message_id FROM annotations "
|
||||
"WHERE account_id = $1 AND chat_id = $2 AND message_id = ANY($3::bigint[])",
|
||||
account_id,
|
||||
chat_id,
|
||||
[view.message_id for view in views],
|
||||
)
|
||||
noted = {row["message_id"] for row in rows}
|
||||
for view in views:
|
||||
view.has_note = view.message_id in noted
|
||||
|
||||
|
||||
def _build_album(
|
||||
members: list[tuple[asyncpg.Record, dict]],
|
||||
media_by_key: dict[tuple[int, int], asyncpg.Record],
|
||||
|
||||
@@ -38,6 +38,8 @@ class ChatListItem(BaseModel):
|
||||
last_date: datetime | None
|
||||
last_text: str | None
|
||||
last_sender_id: int | None
|
||||
last_message_id: int | None = None
|
||||
last_deleted: bool = False
|
||||
|
||||
|
||||
class DiscoverItem(BaseModel):
|
||||
@@ -204,6 +206,7 @@ class MessageView(BaseModel):
|
||||
is_animated_emoji: bool = False
|
||||
read: bool = False
|
||||
read_at: datetime | None = None
|
||||
has_note: bool = False
|
||||
|
||||
|
||||
class MessageVersionView(BaseModel):
|
||||
@@ -611,6 +614,31 @@ class AnnotationView(BaseModel):
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class NoteView(AnnotationView):
|
||||
chat_title: str | None
|
||||
chat_has_avatar: bool
|
||||
message_text: str | None
|
||||
message_date: datetime | None
|
||||
message_has_media: bool
|
||||
message_deleted: bool
|
||||
|
||||
|
||||
class NoteTag(BaseModel):
|
||||
tag: str
|
||||
count: int
|
||||
|
||||
|
||||
class CaptureStatus(BaseModel):
|
||||
live: bool
|
||||
since: datetime | None
|
||||
last_seen: datetime | None
|
||||
|
||||
|
||||
class CaptureGap(BaseModel):
|
||||
start: datetime
|
||||
end: datetime
|
||||
|
||||
|
||||
class WatchView(BaseModel):
|
||||
id: int
|
||||
account_id: int
|
||||
|
||||
Reference in new issue
Block a user