Chime (Chime v0.1.0-dev)

Copy Markdown View Source

Chime is notifications for Phoenix: something happens in the app, and Chime tells the people it concerns, in the app's feed (the bell), by email and by the app's own channels, as each person wants.

Add Chime to the app's supervision tree, after the repo, the PubSub and Oban (Chime.Config has the options):

{Chime, repo: MyApp.Repo, pubsub: MyApp.PubSub, resolver: MyApp.ChimeResolver}

Write a Chime.Notifier for each kind of notification, and send one:

Chime.notify(MyApp.Notifications.NewComment, %{comment: comment})

The functions of this module read and change a person's feed, by the person's id: feed/2, counts/1, mark_seen/1, mark_read/2, mark_unread/2, archive/2, unarchive/2; their preferences: preferences/1, put_preference/4; and the log of what was decided: explain/1.

Start with the Quickstart.

Live

subscribe/1 subscribes the caller to a person's topic, on the app's PubSub. It receives:

  • {Chime, :notified, counts} - a notification came, or an event joined one of their groups;
  • {Chime, :updated, counts} - they saw, read or archived notifications, in another tab or on another device.

counts is %{unseen: n, unread: n, workspaces: %{id => %{unseen: n, unread: n}}}: the whole feed's, and each workspace's. Chime.Live and the JSON API's stream are subscribers.

Summary

Types

A person's counts: what they have not seen, and not read.

Functions

Takes an action of a notification for its person: one of the :actions of the notifier's feed/2 (%{key: "accept", label: "Accept"}), which calls the notifier's act/3 with the key, the data and the notification. When it says :ok or {:ok, url}, the notification keeps the action and is read, and the person's pages are told. Returns {:ok, %{action: key, url: url | nil}}, or {:error, :not_found | :unknown_action | :already | reason}.

Who did a notification's events, in words: "Ana", "Ana and Rui", "Ana and 4 others", in the app's languages (Chime.Messages). An app can write its own from notification.actors, the latest first, and notification.actor_count.

Archives notifications of a person: out of the inbox. Returns the counts.

Cancels an event, by its idempotency key or the event

Returns a specification to start this module under a supervisor.

A person's counts: %{unseen: n, unread: n}, of the notifications of their feed not archived, or of a workspace's (workspace: id). One row, kept as notifications come and go.

A person's devices, the latest first.

The log of a notification: when it came, what each channel decided and why. See Chime.Explain.

A person's feed, the latest first, each notification with its event: Chime.Feed.render/1 gives what it shows.

A link to a notification, for an email or a channel, that reads it in the feed when it is opened, then goes to url (a path goes on the config's :url). See Chime.Links. Without the config's :mail_url and :secret_key_base, the address itself.

Marks notifications of a person as read (and seen): a list of ids, or :all. Option :workspace. Returns the counts.

Marks a person's feed as seen: the bell's dot goes. Option :workspace, only a workspace's. Returns the counts, of the workspace with one.

Marks notifications of a person as unread. Returns the counts.

Notifies the people of an event, by the notifier's channels.

A person's preferences: for each category of the config, its settings by channel (Chime.Preferences). Option :workspace: the settings in a workspace, its own where the person set them.

Changes a person's setting of a channel for a category: "on", "off", or for email "daily" or "weekly". Option :workspace: for that workspace only, over the person's setting for all. See Chime.Preferences.

Registers a device of a person, for the :push channel: %{kind: "web", token: endpoint, keys: %{"p256dh" => ..., "auth" => ...}} for a browser's subscription, %{kind: "apns" | "fcm", token: token} for a phone's; and :name. A token registered before is the same device, now of this person. Errors: :unknown_kind, :invalid_token, :invalid_subscription.

Removes a person's device.

Sends a delivery again: one that failed or was skipped is scheduled now, its attempts from nought, with "sent again" in the log; what is decided at its time (the preferences, the conditions, the rules) is decided again. {:error, :not_found} for an unknown one, {:error, :waiting} for one that waits already.

Starts Chime with the options of Chime.Config.

Subscribes the caller to a person's live news (see Live, above).

Brings archived notifications of a person back. Returns the counts.

The address to unsubscribe a person from a category's emails, for the footer of a notifier's email; nil without :mail_url and :secret_key_base in the config.

Turns a person's device on or off (enabled:), or names it (name:).

Types

counts()

@type counts() :: %{unseen: non_neg_integer(), unread: non_neg_integer()}

A person's counts: what they have not seen, and not read.

Functions

act(recipient, notification_id, action)

@spec act(String.t(), Ecto.UUID.t(), String.t()) ::
  {:ok, %{action: String.t(), url: String.t() | nil}} | {:error, term()}

Takes an action of a notification for its person: one of the :actions of the notifier's feed/2 (%{key: "accept", label: "Accept"}), which calls the notifier's act/3 with the key, the data and the notification. When it says :ok or {:ok, url}, the notification keeps the action and is read, and the person's pages are told. Returns {:ok, %{action: key, url: url | nil}}, or {:error, :not_found | :unknown_action | :already | reason}.

actors(notification)

@spec actors(Chime.Notification.t()) :: String.t()

Who did a notification's events, in words: "Ana", "Ana and Rui", "Ana and 4 others", in the app's languages (Chime.Messages). An app can write its own from notification.actors, the latest first, and notification.actor_count.

archive(recipient, ids, opts \\ [])

@spec archive(String.t(), [String.t()] | :all, keyword()) :: counts()

Archives notifications of a person: out of the inbox. Returns the counts.

cancel(key_or_event, opts \\ [])

@spec cancel(String.t() | Chime.Event.t(), keyword()) ::
  {:ok, %{deliveries: non_neg_integer(), removed: non_neg_integer()}}
  | {:error, :not_found}

Cancels an event, by its idempotency key or the event:

Chime.cancel("comment:#{comment.id}")

A scheduled event (:at) is not sent. One sent has its deliveries that wait (an email in ten minutes, a digest) skipped, with "the notification was cancelled" in the log. With feed: :remove, its notifications are taken out of the feeds too, but those of a group that holds other events. Returns {:ok, %{deliveries: n, removed: n}}, or {:error, :not_found}.

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

counts(recipient, opts \\ [])

@spec counts(String.t(), keyword()) :: counts()

A person's counts: %{unseen: n, unread: n}, of the notifications of their feed not archived, or of a workspace's (workspace: id). One row, kept as notifications come and go.

devices(recipient)

@spec devices(String.t()) :: [Chime.Device.t()]

A person's devices, the latest first.

explain(notification)

@spec explain(Ecto.UUID.t() | Chime.Notification.t()) :: Chime.Explain.t() | nil

The log of a notification: when it came, what each channel decided and why. See Chime.Explain.

feed(recipient, opts \\ [])

@spec feed(String.t(), keyword()) :: [Chime.Notification.t()]

A person's feed, the latest first, each notification with its event: Chime.Feed.render/1 gives what it shows.

Options:

  • :limit - how many (20, up to 100).
  • :before - the id of a notification: the page after it.
  • :filter - :inbox, those not archived (the default), :unread, :archived or :all.
  • :workspace - only the notifications of a workspace.

link(notification, url)

@spec link(Chime.Notification.t(), String.t()) :: String.t()

A link to a notification, for an email or a channel, that reads it in the feed when it is opened, then goes to url (a path goes on the config's :url). See Chime.Links. Without the config's :mail_url and :secret_key_base, the address itself.

~s(<a href="#{Chime.link(notification, "/stories/42")}">Read it</a>)

mark_read(recipient, ids, opts \\ [])

@spec mark_read(String.t(), [String.t()] | :all, keyword()) :: counts()

Marks notifications of a person as read (and seen): a list of ids, or :all. Option :workspace. Returns the counts.

mark_seen(recipient, opts \\ [])

@spec mark_seen(String.t(), keyword()) :: counts()

Marks a person's feed as seen: the bell's dot goes. Option :workspace, only a workspace's. Returns the counts, of the workspace with one.

mark_unread(recipient, ids, opts \\ [])

@spec mark_unread(String.t(), [String.t()] | :all, keyword()) :: counts()

Marks notifications of a person as unread. Returns the counts.

notify(notifier, params, opts \\ [])

@spec notify(module(), term(), keyword()) ::
  {:ok, Chime.Event.t()} | {:error, :duplicate, Chime.Event.t()}

Notifies the people of an event, by the notifier's channels.

The notifier's recipients/1 says who, unless :to does. Each person gets a notification in their feed at once (unless they turned the feed off for the category), or the event joins their open notification of its group; each other channel is a delivery, which goes at its time as an Oban job, in the same transaction.

Options:

  • :to - the people, instead of the notifier's recipients/1: the app's users, Chime.Recipients or ids.
  • :workspace - the workspace it happens in, instead of the notifier's workspace/1 (see the Workspaces guide).
  • :at - a time to notify at, instead of now: the event is "scheduled", and a job notifies its people then. The guard (notify?/2) and the people are decided now; the feed, the preferences and the channels at its time. cancel/2 stops it.
  • :key - an idempotency key: a second call with the same key does nothing, and returns {:error, :duplicate, event}. Give one when the call may run twice (a retried job): "comment:#{comment.id}".

Returns {:ok, %Chime.Event{}}. For many people, it works a batch of 1000 at a time, each its own transaction: call it from a job of the app for an event to everyone.

preferences(recipient, opts \\ [])

@spec preferences(String.t(), keyword()) :: [Chime.Preferences.t()]

A person's preferences: for each category of the config, its settings by channel (Chime.Preferences). Option :workspace: the settings in a workspace, its own where the person set them.

put_preference(recipient, category, channel, setting, opts \\ [])

@spec put_preference(
  String.t(),
  String.t() | atom(),
  String.t() | atom(),
  String.t() | atom(),
  keyword()
) :: :ok | {:error, atom()}

Changes a person's setting of a channel for a category: "on", "off", or for email "daily" or "weekly". Option :workspace: for that workspace only, over the person's setting for all. See Chime.Preferences.

register_device(recipient, attrs)

@spec register_device(String.t(), map()) :: {:ok, Chime.Device.t()} | {:error, atom()}

Registers a device of a person, for the :push channel: %{kind: "web", token: endpoint, keys: %{"p256dh" => ..., "auth" => ...}} for a browser's subscription, %{kind: "apns" | "fcm", token: token} for a phone's; and :name. A token registered before is the same device, now of this person. Errors: :unknown_kind, :invalid_token, :invalid_subscription.

remove_device(recipient, id)

@spec remove_device(String.t(), Ecto.UUID.t()) :: :ok | {:error, :not_found}

Removes a person's device.

resend(delivery_id)

@spec resend(Ecto.UUID.t()) :: :ok | {:error, :not_found | :waiting}

Sends a delivery again: one that failed or was skipped is scheduled now, its attempts from nought, with "sent again" in the log; what is decided at its time (the preferences, the conditions, the rules) is decided again. {:error, :not_found} for an unknown one, {:error, :waiting} for one that waits already.

start_link(opts)

@spec start_link(keyword()) :: Supervisor.on_start()

Starts Chime with the options of Chime.Config.

subscribe(recipient)

@spec subscribe(String.t()) :: :ok | {:error, term()}

Subscribes the caller to a person's live news (see Live, above).

unarchive(recipient, ids, opts \\ [])

@spec unarchive(String.t(), [String.t()] | :all, keyword()) :: counts()

Brings archived notifications of a person back. Returns the counts.

unsubscribe_url(recipient, category)

@spec unsubscribe_url(
  Chime.Recipient.t() | String.t(),
  Chime.Notification.t() | String.t()
) ::
  String.t() | nil

The address to unsubscribe a person from a category's emails, for the footer of a notifier's email; nil without :mail_url and :secret_key_base in the config.

update_device(recipient, id, changes)

@spec update_device(String.t(), Ecto.UUID.t(), keyword() | map()) ::
  {:ok, Chime.Device.t()} | {:error, :not_found}

Turns a person's device on or off (enabled:), or names it (name:).