PhoenixKit.Notifications.Prefs (phoenix_kit v1.7.226)

Copy Markdown View Source

Per-user notification preferences.

Preferences live inside the user's existing custom_fields JSONB column under the "notification_preferences" key — a flat %{type_key => boolean} map. Unset keys default to the type's own :default flag via PhoenixKit.Notifications.Types.default_for/1, so behaviour before a user has opted in anywhere is unchanged.

The filter function user_wants?/2 is called once per notification fan-out from PhoenixKit.Notifications.maybe_create_from_activity/1. It's designed to fail open: any lookup error, unknown action, or malformed prefs map returns true so the system never silently drops a notification due to a bad row.

Summary

Functions

Returns the user's raw preference map.

Merge-preserving write: reads the user's current preferences and overlays only the keys in prefs, so keys this caller doesn't render survive.

Replaces the user's preference map with prefs (a %{key => boolean} map).

Answers "would this user want a notification for this action?"

Like user_wants?/2 but checks a preference key directly — a base type key ("account", "posts") or a dotted sub key ("comments.replies"). Used by Notifications.create/1 when a caller passes :type. Applies the same master switch and the same fail-open contract (unknown key → its default; crash → true).

Functions

get(uuid)

@spec get(PhoenixKit.Users.Auth.User.t() | String.t()) :: %{
  optional(String.t()) => boolean()
}

Returns the user's raw preference map.

Accepts either a loaded %User{} (zero DB work) or a UUID (one lookup). Missing preferences return %{}.

merge(user, prefs)

@spec merge(PhoenixKit.Users.Auth.User.t(), %{optional(String.t()) => boolean()}) ::
  {:ok, PhoenixKit.Users.Auth.User.t()} | {:error, :not_found}

Merge-preserving write: reads the user's current preferences and overlays only the keys in prefs, so keys this caller doesn't render survive.

This is the safe write for any partial surface (the settings page renders all keys but uses this anyway; the deprecated dashboard settings component renders only base keys and MUST use this or it would drop every sub-type entry). Using it by construction closes the whole "full-replace caller drops keys" class.

Note the overlay is computed from the passed user's prefs, so a concurrent write to the same notification_preferences key can still be lost; only sibling custom_fields keys are protected. Callers that must not lose one should pass a freshly-loaded user (same caveat as ChannelConfig.update/3).

Returns {:error, :not_found} if the user row was deleted concurrently.

update(user, prefs)

@spec update(PhoenixKit.Users.Auth.User.t(), %{optional(String.t()) => boolean()}) ::
  {:ok, PhoenixKit.Users.Auth.User.t()} | {:error, :not_found}

Replaces the user's preference map with prefs (a %{key => boolean} map).

Full replace of the notification_preferences value — callers must include every key they intend to keep, so this is for cases that own the whole map (pause/resume, which pass a computed full snapshot). For a partial write that must NOT drop keys it doesn't render, use merge/2. Other custom_fields keys are preserved either way — the write touches the notification_preferences key alone (see write/2).

Returns {:error, :not_found} if the user row was deleted concurrently.

user_wants?(user_uuid, action)

@spec user_wants?(String.t() | PhoenixKit.Users.Auth.User.t(), String.t()) ::
  boolean()

Answers "would this user want a notification for this action?"

Resolves the action to its most-specific preference key (Types.key_for_action/1) and applies the master switch: a base key is checked alone; a dotted sub key is wanted only when BOTH its base master AND the sub itself are enabled. Fail-open on every ambiguity — unknown action, missing pref (→ key default), or any raise → true — so a notification is never silently dropped by a bad row.

user_wants_type?(user_uuid, type_key)

@spec user_wants_type?(String.t() | PhoenixKit.Users.Auth.User.t(), String.t()) ::
  boolean()

Like user_wants?/2 but checks a preference key directly — a base type key ("account", "posts") or a dotted sub key ("comments.replies"). Used by Notifications.create/1 when a caller passes :type. Applies the same master switch and the same fail-open contract (unknown key → its default; crash → true).