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})
|
WITH ids AS ({ids})
|
||||||
SELECT ids.chat_id,
|
SELECT ids.chat_id,
|
||||||
COALESCE(cs.message_count, 0) AS message_count,
|
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,
|
COALESCE(named.title,
|
||||||
NULLIF(trim(concat_ws(' ', p.first_name, p.last_name)), ''),
|
NULLIF(trim(concat_ws(' ', p.first_name, p.last_name)), ''),
|
||||||
p.username) AS title,
|
p.username) AS title,
|
||||||
@@ -149,6 +150,8 @@ def _chat_item(row: asyncpg.Record) -> ChatListItem:
|
|||||||
last_date=row["last_date"],
|
last_date=row["last_date"],
|
||||||
last_text=row["last_text"],
|
last_text=row["last_text"],
|
||||||
last_sender_id=row["last_sender_id"],
|
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))
|
views.append(_build_album(members, media_by_key))
|
||||||
index = end
|
index = end
|
||||||
await _apply_read_status(pool, account_id, chat_id, views)
|
await _apply_read_status(pool, account_id, chat_id, views)
|
||||||
|
await _apply_notes(pool, account_id, chat_id, views)
|
||||||
return views
|
return views
|
||||||
|
|
||||||
|
|
||||||
@@ -278,6 +282,7 @@ async def get_message(
|
|||||||
raw = load_raw(row["raw"])
|
raw = load_raw(row["raw"])
|
||||||
view = build_message_view(row, raw, _single_media(row, raw, media_by_key))
|
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_read_status(pool, account_id, chat_id, [view])
|
||||||
|
await _apply_notes(pool, account_id, chat_id, [view])
|
||||||
return 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(
|
def _build_album(
|
||||||
members: list[tuple[asyncpg.Record, dict]],
|
members: list[tuple[asyncpg.Record, dict]],
|
||||||
media_by_key: dict[tuple[int, int], asyncpg.Record],
|
media_by_key: dict[tuple[int, int], asyncpg.Record],
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ class ChatListItem(BaseModel):
|
|||||||
last_date: datetime | None
|
last_date: datetime | None
|
||||||
last_text: str | None
|
last_text: str | None
|
||||||
last_sender_id: int | None
|
last_sender_id: int | None
|
||||||
|
last_message_id: int | None = None
|
||||||
|
last_deleted: bool = False
|
||||||
|
|
||||||
|
|
||||||
class DiscoverItem(BaseModel):
|
class DiscoverItem(BaseModel):
|
||||||
@@ -204,6 +206,7 @@ class MessageView(BaseModel):
|
|||||||
is_animated_emoji: bool = False
|
is_animated_emoji: bool = False
|
||||||
read: bool = False
|
read: bool = False
|
||||||
read_at: datetime | None = None
|
read_at: datetime | None = None
|
||||||
|
has_note: bool = False
|
||||||
|
|
||||||
|
|
||||||
class MessageVersionView(BaseModel):
|
class MessageVersionView(BaseModel):
|
||||||
@@ -611,6 +614,31 @@ class AnnotationView(BaseModel):
|
|||||||
updated_at: datetime
|
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):
|
class WatchView(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
account_id: int
|
account_id: int
|
||||||
|
|||||||
Reference in new issue
Block a user