feat(backend,frontend): feed gets a bots tab so private is people only, a folder's context menu opens the feed of its chats

This commit is contained in:
hh committed 2026-10-26 01:50:44 +00:00
1 parent b9e8a96774
commit c2180e80ad
11 files changed
+128 -16

No files matched your search

+41 -4
View File
@@ -5,6 +5,7 @@ from enum import StrEnum
import asyncpg
from utils.policy import repository as policy_repository
from utils.read import chats, peers
from utils.read.accounts import self_user_id
from utils.read.models import (
@@ -45,6 +46,13 @@ LEFT JOIN LATERAL (
) p ON true
"""
_IS_BOT = """EXISTS (
SELECT 1 FROM peers p
WHERE p.account_id = feed_items.account_id AND p.peer_id = feed_items.chat_id
AND COALESCE((p.raw->>'is_bot')::bool, (p.raw->>'bot')::bool,
p.raw->>'type' = 'ChatType.BOT', false)
)"""
_UNSEEN = """
SELECT count(*) FILTER (WHERE kind = 'deleted') AS deleted,
count(*) FILTER (WHERE kind = 'edited') AS edited
@@ -69,15 +77,25 @@ class FeedKind(StrEnum):
class FeedChatKind(StrEnum):
DM = "dm"
BOT = "bot"
GROUP = "group"
CHANNEL = "channel"
_CHAT_KIND_CLAUSES = {
FeedChatKind.DM: f"chat_kind = 'dm' AND NOT {_IS_BOT}",
FeedChatKind.BOT: f"chat_kind = 'dm' AND {_IS_BOT}",
FeedChatKind.GROUP: "chat_kind = 'group'",
FeedChatKind.CHANNEL: "chat_kind = 'channel'",
}
@dataclass(slots=True)
class FeedFilter:
kind: FeedKind = FeedKind.ALL
chat_id: int | None = None
chat_kind: FeedChatKind | None = None
folder_id: int | None = None
since: datetime | None = None
@@ -90,18 +108,34 @@ def decode_cursor(cursor: str) -> tuple[datetime, int]:
return _EPOCH + int(micros) * _MICROSECOND, int(key)
async def _folder_chat_ids(
pool: asyncpg.Pool, account_id: int, folder_id: int | None
) -> list[int] | None:
if folder_id is None:
return None
folder = await policy_repository.get_folder(pool, account_id, folder_id)
if folder is None:
return []
return await chats.folder_chat_ids(pool, account_id, folder)
def _where(
account_id: int, feed: FeedFilter, cursor: str | None
account_id: int,
feed: FeedFilter,
folder_chat_ids: list[int] | None,
cursor: str | None,
) -> tuple[str, list[object]]:
params: list[object] = [account_id]
clauses = ["account_id = $1"]
if feed.kind is FeedKind.MESSAGES:
clauses.append("kind <> 'profile'")
if feed.chat_kind is not None:
clauses.append(_CHAT_KIND_CLAUSES[feed.chat_kind])
exact = feed.kind not in {FeedKind.ALL, FeedKind.MESSAGES}
conditions = (
("kind = {}", feed.kind.value if exact else None),
("chat_id = {}", feed.chat_id),
("chat_kind = {}", feed.chat_kind.value if feed.chat_kind else None),
("chat_id = ANY({}::bigint[])", folder_chat_ids),
("at >= {}", feed.since),
)
for template, value in conditions:
@@ -191,7 +225,9 @@ async def _build_items(
chat=FeedChat(
chat_id=row["chat_id"],
title=chat.title if chat else None,
kind=row["chat_kind"],
kind=FeedChatKind.BOT
if chat and chat.is_bot and row["chat_kind"] == FeedChatKind.DM
else row["chat_kind"],
has_avatar=chat.has_avatar if chat else False,
),
message_id=row["message_id"],
@@ -218,7 +254,8 @@ async def list_feed(
cursor: str | None = None,
) -> FeedPage:
size = max(1, min(limit, MAX_LIMIT))
where, params = _where(account_id, feed, cursor)
folder_chat_ids = await _folder_chat_ids(pool, account_id, feed.folder_id)
where, params = _where(account_id, feed, folder_chat_ids, cursor)
params.append(size + 1)
rows = await pool.fetch(
f"SELECT {_ITEM_COLS} FROM feed_items WHERE {where} " # noqa: S608