MishkaCmsCore.Runtime.PageBuilder.Mentions (Mishka CMS Core v0.0.1)

Copy Markdown View Source

Naming somebody in a note: @who in the body, and the notification that follows it.

The handle is the whole address

MishkaCmsCore.Accounts.User has no display name — email is the only thing a person is called — so @sara@example.com is how Sara is named. Nothing stores a nickname; the handle is derived on every read and cannot drift from the account.

The whole address rather than its local part, because a local part is not a name: this CMS is multi-site, and master@one.test and master@two.test are two different people who would both answer to @master. An address is the only handle that names exactly one account, and a mention that reaches the wrong person is worse than a mention that is long.

A bare local part is still UNDERSTOOD — notes written before this, and anyone who types @sara by hand, still reach every Sara the writer can see. It is simply not what the picker writes.

Who can be named, and who the writer is

A TENANT may name their own site's people and nobody else. Not another site's — that would leak one customer's user list into another's editor — and not the masters, whose addresses belong to the platform rather than to any customer. The activity feed draws the same line, and MishkaCmsCoreWeb.Templates.ReviewComments draws it again on the byline and on a mention, where a master reads as "Master" (master_mentions/2).

A MASTER may name anybody the document can reach: the site's people and the other masters. There is nobody a master may not see, so there is nothing to hide from them.

The document matters too, not just the writer: a note on something SHARED has no site of its own, so a master's candidates there are the other masters.

Where each piece runs

segments/1 and handles/1 are pure, so the card can highlight a mention and the notifier can find one without either calling the other. candidates/2 is the autocomplete's one query. notify/2 is called from MishkaCmsCore.Runtime.Resources.Changes.NotifyMentions, after the row exists — a mention that is delivered before the note is saved points at nothing.

Summary

Types

A body cut into the parts a card draws: plain runs and the people named in them.

Functions

Who query could mean, for the autocomplete — at most 6, nearest match first.

The handles a body names, downcased and without repeats.

body with every mention in masked written as @Master. Everything else is kept as written.

The handles in bodies that name a master and nobody on site_id, downcased.

For each of notes, the handles its body names that reader may not see: a master's, when reader is a site user. A master reads every handle. Backs PageComment.master_mentions.

The notification type, and the key its per-site and per-user toggles hang off.

Tell the people a note names that it names them.

A note's body, cut into the runs a card draws.

Types

segment()

@type segment() :: {:text, String.t()} | {:mention, String.t()}

A body cut into the parts a card draws: plain runs and the people named in them.

Functions

candidates(scope, query)

@spec candidates(map(), String.t() | nil) :: [
  %{id: String.t(), handle: String.t(), local: String.t(), domain: String.t()}
]

Who query could mean, for the autocomplete — at most 6, nearest match first.

A blank query offers the first few people rather than nothing, so pressing @ is useful before typing anything. Matching is a prefix of the whole address, so @mas narrows to the masters and @master@two narrows those to one site — which is the point of naming people by address.

scope[:actor] decides how far the list reaches; see "Who can be named" above.

handles(body)

@spec handles(String.t() | nil) :: [String.t()]

The handles a body names, downcased and without repeats.

Case is dropped because an address is matched case-insensitively everywhere else in this codebase — Sara@x.test and sara@x.test are one person, and being notified twice for writing both is not a feature.

mask_masters(body, masked)

@spec mask_masters(String.t() | nil, MapSet.t()) :: String.t() | nil

body with every mention in masked written as @Master. Everything else is kept as written.

iex> alias MishkaCmsCore.Runtime.PageBuilder.Mentions
iex> Mentions.mask_masters("ping @Boss@platform.test, thanks", MapSet.new(["boss@platform.test"]))
"ping @Master, thanks"

master_handles(bodies, site_id)

@spec master_handles([String.t() | nil], String.t() | nil) :: MapSet.t()

The handles in bodies that name a master and nobody on site_id, downcased.

A whole address names a master when a master holds it. A bare local part does when a master's address starts with it and no account of site_id does. One read for every body; with no site, nothing.

master_mentions(notes, arg2)

@spec master_mentions([map()], map() | nil) :: [[String.t()]]

For each of notes, the handles its body names that reader may not see: a master's, when reader is a site user. A master reads every handle. Backs PageComment.master_mentions.

notification_type()

@spec notification_type() :: String.t()

The notification type, and the key its per-site and per-user toggles hang off.

Public because MishkaCmsCore.Application registers the two Settings under it and MishkaCmsCore.Notify.Delivery checks them by the same name.

notify(comment, scope \\ %{})

@spec notify(map(), map()) :: :ok

Tell the people a note names that it names them.

Nothing is delivered to the author: being notified of your own writing is noise, and it is the one mention that is certain to be read already. Nothing is delivered for a handle that matches nobody either — a typo is not an audience.

Returns :ok whatever happens. A note that saved is saved; a notification centre that is down must not turn that into a failed write in front of the person who typed it.

segments(body)

@spec segments(String.t() | nil) :: [segment()]

A note's body, cut into the runs a card draws.

Splitting on whitespace rather than scanning for @ is what keeps a quoted address out of it: write to sara@example.com is one chunk that does not START with @, so it stays text, while @sara@example.com is a chunk that does. Trailing punctuation rides along as text, so @sara@example.com, still names her.

iex> alias MishkaCmsCore.Runtime.PageBuilder.Mentions
iex> Mentions.segments("ping @sara@example.com, thanks")
[{:text, "ping "}, {:mention, "sara@example.com"}, {:text, ", thanks"}]