PhoenixKit.Notifications (phoenix_kit v1.7.133)

Copy Markdown View Source

Per-user notifications driven by PhoenixKit.Activity.

When an activity is logged with a target_uuid that differs from the actor_uuid, a row is inserted into phoenix_kit_notifications for the target user. The user sees it in the bell dropdown (count_unread/1, recent_for_user/2) and in the inbox at /notifications (list_for_user/2). Each row carries its own seen_at and dismissed_at — the same activity can be "seen but not dismissed" for one user and "unseen" for another.

The whole feature is gated by the global notifications_enabled setting (default "true"); when "false", maybe_create_from_activity/1 is a no-op.

Registered as a core toggleable module (use PhoenixKit.Module) so it appears on the admin Modules page and contributes the /admin/notifications overview tab. The module enable/disable flips the same notifications_enabled kill-switch enabled?/0 reads.

Summary

Functions

Aggregate counts for the admin overview page: total notifications, unread (neither seen nor dismissed), and dismissed. A single count(...) FILTER (WHERE ...) query — one table scan, not three. Rescues to zeros so the page never crashes on a query hiccup.

Counts undismissed, unseen notifications for a user. Drives the badge.

Create a standalone notification — one not tied to an activity (V126). Use for app-driven notices that don't originate from the activity log (e.g. "your export is ready").

Create a standalone notification for many recipients in one call — the multi-recipient counterpart to create/1. recipient_uuids is a list; attrs is the same shape as create/1 minus :recipient_uuid (it's supplied per recipient).

Dismisses a single notification. Idempotent.

Bulk-dismisses all undismissed notifications. Returns {count, nil}.

Is the notifications feature enabled? Default true.

Fetches one notification scoped to the recipient. Returns nil if missing.

Returns {notifications, total_count} for the given user, newest first.

Bulk-marks all unseen notifications as seen. Returns {count, nil}.

Marks a single notification as seen. Idempotent — already-seen rows return {:ok, notification} unchanged.

Inserts a notification for the activity's target user, if the rules allow it.

Deletes notifications whose underlying activity is older than days.

Returns the N most-recent undismissed notifications for a user.

Retention period in days. Falls back to activity retention if unset.

Functions

admin_stats()

Aggregate counts for the admin overview page: total notifications, unread (neither seen nor dismissed), and dismissed. A single count(...) FILTER (WHERE ...) query — one table scan, not three. Rescues to zeros so the page never crashes on a query hiccup.

count_unread(user_uuid)

Counts undismissed, unseen notifications for a user. Drives the badge.

create(attrs)

Create a standalone notification — one not tied to an activity (V126). Use for app-driven notices that don't originate from the activity log (e.g. "your export is ready").

attrs keys:

  • :recipient_uuid (required) — who receives it

  • :text / :icon / :link — convenience, folded into metadata as notification_text / notification_icon / notification_link (the keys Render reads)

  • :metadata — raw metadata map (merged under the convenience keys)

  • :type — optional notification type key (e.g. "account", "posts", or a module-contributed type). When given, the send is filtered through the recipient's per-type preference (Prefs.user_wants_type?/2, fail-open).

  • :action — optional action string (e.g. "post.commented"). When given, filtered through Prefs.user_wants?/2 (which maps the action to a type). Use :type OR :action, not both.

    Notifications.create(%{ recipient_uuid: user.uuid, text: "Your export is ready.", icon: "hero-arrow-down-tray", link: "/exports/123" })

Honors the global notifications_enabled kill-switch. With neither :type nor :action, it's an unconditional app-driven send (no preference filtering). Returns {:ok, %Notification{}}, {:ok, :skipped} (disabled or filtered out by prefs), or {:error, changeset}. Broadcasts {:notification_created, n} on success.

create_many(recipient_uuids, attrs)

Create a standalone notification for many recipients in one call — the multi-recipient counterpart to create/1. recipient_uuids is a list; attrs is the same shape as create/1 minus :recipient_uuid (it's supplied per recipient).

The recipient list is the caller's responsibility (e.g. the followers of an author) — this is the generic fan-out primitive, not an audience resolver. Duplicate uuids are de-duped. Each recipient is filtered independently through :type / :action prefs when given, so muted users are skipped. Honors the kill-switch once up front.

Notifications.create_many(follower_uuids, %{
  type: "posts",
  text: "Alice published a new post.",
  link: "/posts/#{post.id}"
})

Returns {:ok, created_count} (notifications actually inserted, i.e. excluding disabled / pref-skipped) or {:ok, :skipped} when notifications are globally disabled.

dismiss(user_uuid, uuid)

Dismisses a single notification. Idempotent.

dismiss_all(user_uuid)

Bulk-dismisses all undismissed notifications. Returns {count, nil}.

enabled?()

Is the notifications feature enabled? Default true.

get_notification(user_uuid, uuid)

Fetches one notification scoped to the recipient. Returns nil if missing.

list_for_user(user_uuid, opts \\ [])

Returns {notifications, total_count} for the given user, newest first.

Options:

  • :page (default 1) / :per_page (default 25)
  • :status:unread (seen_at nil) | :all (default)

  • :include_dismissed — include dismissed rows (default false)

mark_all_seen(user_uuid)

Bulk-marks all unseen notifications as seen. Returns {count, nil}.

mark_seen(user_uuid, uuid)

Marks a single notification as seen. Idempotent — already-seen rows return {:ok, notification} unchanged.

maybe_create_from_activity(entry)

Inserts a notification for the activity's target user, if the rules allow it.

Returns one of:

  • {:ok, %Notification{}} — row created; broadcast on the per-user topic
  • {:ok, :skipped} — filtered out (no target, self-action, feature disabled)
  • {:error, changeset} — insert failed (logged, never raised)

prune(days)

Deletes notifications whose underlying activity is older than days.

recent_for_user(user_uuid, limit \\ 10)

Returns the N most-recent undismissed notifications for a user.

Drives the bell dropdown. Activity (and actor) are preloaded.

retention_days()

Retention period in days. Falls back to activity retention if unset.