fix: handle follower and shout notifications in notifications_seen_thread
All checks were successful
Deploy on push / deploy (push) Successful in 3m13s

- 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
This commit is contained in:
2025-10-04 08:59:47 +03:00
parent 163c0732d4
commit 13343bb40e
3 changed files with 81 additions and 17 deletions

View File

@@ -1,5 +1,23 @@
# Changelog # 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 ## [0.9.30] - 2025-10-02
### 🔧 Fixed ### 🔧 Fixed

View File

@@ -53,7 +53,7 @@ def get_entity_field_name(entity_type: str) -> str:
"author": "following", # AuthorFollower.following -> Author "author": "following", # AuthorFollower.following -> Author
"topic": "topic", # TopicFollower.topic -> Topic "topic": "topic", # TopicFollower.topic -> Topic
"community": "community", # CommunityFollower.community -> Community "community": "community", # CommunityFollower.community -> Community
"shout": "shout" # ShoutReactionsFollower.shout -> Shout "shout": "shout", # ShoutReactionsFollower.shout -> Shout
} }
if entity_type not in entity_field_mapping: if entity_type not in entity_field_mapping:
msg = f"Unknown entity_type: {entity_type}" msg = f"Unknown entity_type: {entity_type}"

View File

@@ -250,7 +250,7 @@ async def notification_mark_seen(_: None, info: GraphQLResolveInfo, notification
@mutation.field("notifications_seen_after") @mutation.field("notifications_seen_after")
@login_required @login_required
async def notifications_seen_after(_: None, info: GraphQLResolveInfo, after: int) -> dict: 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 error = None
try: try:
author_id = info.context.get("author", {}).get("id") author_id = info.context.get("author", {}).get("id")
@@ -278,18 +278,64 @@ async def notifications_seen_thread(_: None, info: GraphQLResolveInfo, thread: s
error = None error = None
author_id = info.context.get("author", {}).get("id") author_id = info.context.get("author", {}).get("id")
if author_id: if author_id:
[shout_id, reply_to_id] = thread.split(":")
with local_session() as session: with local_session() as session:
# Convert Unix timestamp to datetime for PostgreSQL compatibility # Convert Unix timestamp to datetime for PostgreSQL compatibility
after_datetime = datetime.fromtimestamp(after, tz=UTC) if after else None 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: if after_datetime:
new_reaction_notifications = ( new_reaction_notifications = (
session.query(Notification) session.query(Notification)
.where( .where(
Notification.action == "create", Notification.action == NotificationAction.CREATE.value,
Notification.entity == "reaction", Notification.entity == NotificationEntity.REACTION.value,
Notification.created_at > after_datetime, Notification.created_at > after_datetime,
) )
.all() .all()
@@ -297,8 +343,8 @@ async def notifications_seen_thread(_: None, info: GraphQLResolveInfo, thread: s
removed_reaction_notifications = ( removed_reaction_notifications = (
session.query(Notification) session.query(Notification)
.where( .where(
Notification.action == "delete", Notification.action == NotificationAction.DELETE.value,
Notification.entity == "reaction", Notification.entity == NotificationEntity.REACTION.value,
Notification.created_at > after_datetime, Notification.created_at > after_datetime,
) )
.all() .all()
@@ -307,16 +353,16 @@ async def notifications_seen_thread(_: None, info: GraphQLResolveInfo, thread: s
new_reaction_notifications = ( new_reaction_notifications = (
session.query(Notification) session.query(Notification)
.where( .where(
Notification.action == "create", Notification.action == NotificationAction.CREATE.value,
Notification.entity == "reaction", Notification.entity == NotificationEntity.REACTION.value,
) )
.all() .all()
) )
removed_reaction_notifications = ( removed_reaction_notifications = (
session.query(Notification) session.query(Notification)
.where( .where(
Notification.action == "delete", Notification.action == NotificationAction.DELETE.value,
Notification.entity == "reaction", Notification.entity == NotificationEntity.REACTION.value,
) )
.all() .all()
) )