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
## [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

View File

@@ -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}"

View File

@@ -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()
)