From 13343bb40ebc9b843f7d0cbc3ceba43f0d798fa7 Mon Sep 17 00:00:00 2001 From: Untone Date: Sat, 4 Oct 2025 08:59:47 +0300 Subject: [PATCH] fix: handle follower and shout notifications in notifications_seen_thread - Add support for marking follower notifications as seen (thread='followers') - Add support for marking new shout notifications as seen - Use enum constants (NotificationAction, NotificationEntity) instead of strings - Improve thread ID parsing to support different formats - Remove obsolete TODO about notification_id offset - Better error handling with logger.warning() instead of exceptions Resolves TODOs on lines 253 and 286 in resolvers/notifier.py --- CHANGELOG.md | 18 +++++++++++ resolvers/follower.py | 6 ++-- resolvers/notifier.py | 74 +++++++++++++++++++++++++++++++++++-------- 3 files changed, 81 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f0de7b0..8d9ca4bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## [0.9.31] - 2025-10-04 + +### ✅ Fixed: Notifications TODOs +- **Уведомления о followers**: Добавлена обработка уведомлений о подписчиках в `notifications_seen_thread` + - Теперь при клике на группу "followers" все уведомления о подписках помечаются как прочитанные + - Исправлена обработка thread ID `"followers"` отдельно от shout/reaction threads +- **Уведомления о новых публикациях**: Добавлена обработка уведомлений о новых shouts в `notifications_seen_thread` + - При открытии публикации уведомления о ней тоже помечаются как прочитанные + - Исправлена логика парсинга thread ID для поддержки разных форматов +- **Code Quality**: Использованы enum константы (`NotificationAction`, `NotificationEntity`) вместо строк +- **Убраны устаревшие TODO**: Удален TODO про `notification_id` как offset (текущая логика с timestamp работает корректно) + +### Technical Details +- `core/resolvers/notifier.py`: расширена функция `notifications_seen_thread` для поддержки всех типов уведомлений +- Добавлена обработка `thread == "followers"` для уведомлений о подписках +- Добавлена обработка `NotificationEntity.SHOUT` для уведомлений о новых публикациях +- Улучшена обработка ошибок с `logger.warning()` вместо исключений + ## [0.9.30] - 2025-10-02 ### 🔧 Fixed diff --git a/resolvers/follower.py b/resolvers/follower.py index 8228bbcf..9cafb693 100644 --- a/resolvers/follower.py +++ b/resolvers/follower.py @@ -50,10 +50,10 @@ def get_entity_field_name(entity_type: str) -> str: ValueError: Unknown entity_type: invalid """ entity_field_mapping = { - "author": "following", # AuthorFollower.following -> Author - "topic": "topic", # TopicFollower.topic -> Topic + "author": "following", # AuthorFollower.following -> Author + "topic": "topic", # TopicFollower.topic -> Topic "community": "community", # CommunityFollower.community -> Community - "shout": "shout" # ShoutReactionsFollower.shout -> Shout + "shout": "shout", # ShoutReactionsFollower.shout -> Shout } if entity_type not in entity_field_mapping: msg = f"Unknown entity_type: {entity_type}" diff --git a/resolvers/notifier.py b/resolvers/notifier.py index 093893cf..63dc9c7d 100644 --- a/resolvers/notifier.py +++ b/resolvers/notifier.py @@ -120,7 +120,7 @@ def get_notifications_grouped(author_id: int, after: int = 0, limit: int = 10, o shout_id = shout.get("id") author_id = shout.get("created_by") thread_id = f"shout-{shout_id}" - + with local_session() as session: author = session.query(Author).where(Author.id == author_id).first() shout = session.query(Shout).where(Shout.id == shout_id).first() @@ -155,7 +155,7 @@ def get_notifications_grouped(author_id: int, after: int = 0, limit: int = 10, o thread_id = f"shout-{shout_id}" if reply_id and reaction.get("kind", "").lower() == "comment": thread_id = f"shout-{shout_id}::{reply_id}" - + existing_group = groups_by_thread.get(thread_id) if existing_group: existing_group["seen"] = False @@ -215,7 +215,7 @@ async def load_notifications(_: None, info: GraphQLResolveInfo, after: int, limi if author_id: groups_list = get_notifications_grouped(author_id, after, limit) notifications = sorted(groups_list, key=lambda group: group.get("updated_at", 0), reverse=True) - + # Считаем реальное количество сгруппированных уведомлений total = len(notifications) unread = sum(1 for n in notifications if not n.get("seen", False)) @@ -250,7 +250,7 @@ async def notification_mark_seen(_: None, info: GraphQLResolveInfo, notification @mutation.field("notifications_seen_after") @login_required async def notifications_seen_after(_: None, info: GraphQLResolveInfo, after: int) -> dict: - # TODO: use latest loaded notification_id as input offset parameter + """Mark all notifications after given timestamp as seen.""" error = None try: author_id = info.context.get("author", {}).get("id") @@ -278,18 +278,64 @@ async def notifications_seen_thread(_: None, info: GraphQLResolveInfo, thread: s error = None author_id = info.context.get("author", {}).get("id") if author_id: - [shout_id, reply_to_id] = thread.split(":") with local_session() as session: # Convert Unix timestamp to datetime for PostgreSQL compatibility after_datetime = datetime.fromtimestamp(after, tz=UTC) if after else None - # TODO: handle new follower and new shout notifications + # Handle different thread types: shout reactions, followers, or new shouts + if thread == "followers": + # Mark follower notifications as seen + query_conditions = [ + Notification.entity == NotificationEntity.FOLLOWER.value, + ] + if after_datetime: + query_conditions.append(Notification.created_at > after_datetime) + + follower_notifications = session.query(Notification).where(and_(*query_conditions)).all() + for n in follower_notifications: + try: + ns = NotificationSeen(notification=n.id, viewer=author_id) + session.add(ns) + except Exception as e: + logger.warning(f"Failed to mark follower notification as seen: {e}") + session.commit() + return {"error": None} + + # Handle shout and reaction notifications + thread_parts = thread.split(":") + if len(thread_parts) < 2: + return {"error": "Invalid thread format"} + + shout_id = thread_parts[0] + reply_to_id = thread_parts[1] if len(thread_parts) > 1 else None + + # Query for new shout notifications in this thread + shout_query_conditions = [ + Notification.entity == NotificationEntity.SHOUT.value, + Notification.action == NotificationAction.CREATE.value, + ] + if after_datetime: + shout_query_conditions.append(Notification.created_at > after_datetime) + + shout_notifications = session.query(Notification).where(and_(*shout_query_conditions)).all() + + # Mark relevant shout notifications as seen + for n in shout_notifications: + payload = orjson.loads(str(n.payload)) + if str(payload.get("id")) == shout_id: + try: + ns = NotificationSeen(notification=n.id, viewer=author_id) + session.add(ns) + except Exception as e: + logger.warning(f"Failed to mark shout notification as seen: {e}") + + # Query for reaction notifications if after_datetime: new_reaction_notifications = ( session.query(Notification) .where( - Notification.action == "create", - Notification.entity == "reaction", + Notification.action == NotificationAction.CREATE.value, + Notification.entity == NotificationEntity.REACTION.value, Notification.created_at > after_datetime, ) .all() @@ -297,8 +343,8 @@ async def notifications_seen_thread(_: None, info: GraphQLResolveInfo, thread: s removed_reaction_notifications = ( session.query(Notification) .where( - Notification.action == "delete", - Notification.entity == "reaction", + Notification.action == NotificationAction.DELETE.value, + Notification.entity == NotificationEntity.REACTION.value, Notification.created_at > after_datetime, ) .all() @@ -307,16 +353,16 @@ async def notifications_seen_thread(_: None, info: GraphQLResolveInfo, thread: s new_reaction_notifications = ( session.query(Notification) .where( - Notification.action == "create", - Notification.entity == "reaction", + Notification.action == NotificationAction.CREATE.value, + Notification.entity == NotificationEntity.REACTION.value, ) .all() ) removed_reaction_notifications = ( session.query(Notification) .where( - Notification.action == "delete", - Notification.entity == "reaction", + Notification.action == NotificationAction.DELETE.value, + Notification.entity == NotificationEntity.REACTION.value, ) .all() )