feat: replace email flows with in-app notifications
Add recipient notification detail and filtering, a queued admin composer, and contextual automatic titles. Retire email delivery, obsolete auth routes, newsletter data, and related templates and settings.
This commit is contained in:
@@ -50,16 +50,22 @@ class RedisNotificationStore:
|
||||
"type": payload.get("type") or "notification",
|
||||
"title": payload.get("title") or "",
|
||||
"message": payload.get("message") or "",
|
||||
"body_markdown": payload.get("body_markdown") or None,
|
||||
"level": payload.get("level") or "info",
|
||||
"created_at": _isoformat_datetime(payload.get("created_at")),
|
||||
"is_seen": bool(payload.get("is_seen", False)),
|
||||
"delete_on_seen": bool(payload.get("delete_on_seen", False)),
|
||||
"action_url": payload.get("action_url"),
|
||||
"entity_type": payload.get("entity_type"),
|
||||
"entity_id": payload.get("entity_id"),
|
||||
"meta": payload.get("meta") or {},
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def summary(data: dict) -> dict:
|
||||
result = {**data, "has_body": bool(data.get("body_markdown"))}
|
||||
result.pop("body_markdown", None)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def _publish_event(cls, user_id: str, event: str, data: dict) -> None:
|
||||
if not settings.NOTIFICATIONS_ENABLED:
|
||||
@@ -96,7 +102,7 @@ class RedisNotificationStore:
|
||||
pipe.execute()
|
||||
|
||||
unread_count = cls.unread_count(user_id)
|
||||
cls._publish_event(user_id, "notification", {"notification": data, "unread_count": unread_count})
|
||||
cls._publish_event(user_id, "notification", {"notification": cls.summary(data), "unread_count": unread_count})
|
||||
cls._publish_event(user_id, "unread_count", {"unread_count": unread_count})
|
||||
return data
|
||||
|
||||
@@ -108,6 +114,7 @@ class RedisNotificationStore:
|
||||
limit: int | None = None,
|
||||
offset: int = 0,
|
||||
type_filter: str | None = None,
|
||||
read_filter: str = "all",
|
||||
paginate: bool = True,
|
||||
) -> tuple[list[dict], int]:
|
||||
ids_key = cls._ids_key(user_id)
|
||||
@@ -134,6 +141,10 @@ class RedisNotificationStore:
|
||||
continue
|
||||
if type_filter and data.get("type") != type_filter:
|
||||
continue
|
||||
if read_filter == "unread" and data["is_seen"]:
|
||||
continue
|
||||
if read_filter == "read" and not data["is_seen"]:
|
||||
continue
|
||||
items.append(data)
|
||||
|
||||
if cleanup_ids:
|
||||
@@ -182,17 +193,6 @@ class RedisNotificationStore:
|
||||
data = cls.get(user_id, notif_id)
|
||||
if not data:
|
||||
return None
|
||||
if data.get("delete_on_seen"):
|
||||
deleted = cls.delete(user_id, notif_id)
|
||||
if deleted:
|
||||
return {
|
||||
"notification_id": notif_id,
|
||||
"deleted": True,
|
||||
"notification": None,
|
||||
"unread_count": cls.unread_count(user_id),
|
||||
}
|
||||
return None
|
||||
|
||||
if not data.get("is_seen"):
|
||||
data["is_seen"] = True
|
||||
redis_client.hset(
|
||||
@@ -204,7 +204,7 @@ class RedisNotificationStore:
|
||||
payload = {
|
||||
"notification_id": notif_id,
|
||||
"deleted": False,
|
||||
"notification": data,
|
||||
"notification": cls.summary(data),
|
||||
"unread_count": unread_count,
|
||||
}
|
||||
cls._publish_event(user_id, "notification_seen", payload)
|
||||
@@ -228,16 +228,12 @@ class RedisNotificationStore:
|
||||
continue
|
||||
if type_filter and data.get("type") != type_filter:
|
||||
continue
|
||||
if data.get("delete_on_seen"):
|
||||
pipe.zrem(cls._ids_key(user_id), notif_id)
|
||||
pipe.hdel(cls._data_key(user_id), notif_id)
|
||||
else:
|
||||
data["is_seen"] = True
|
||||
pipe.hset(
|
||||
cls._data_key(user_id),
|
||||
notif_id,
|
||||
json.dumps(data, ensure_ascii=False, default=str),
|
||||
)
|
||||
data["is_seen"] = True
|
||||
pipe.hset(
|
||||
cls._data_key(user_id),
|
||||
notif_id,
|
||||
json.dumps(data, ensure_ascii=False, default=str),
|
||||
)
|
||||
updated += 1
|
||||
if updated:
|
||||
pipe.execute()
|
||||
|
||||
Reference in New Issue
Block a user