# `PhoenixKit.Mentions`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.13.7/lib/phoenix_kit/mentions.ex#L1)

Cross-module `@` pings and `#` record links.

Typing `@` anywhere a person can write free text offers people; typing `#`
offers records from every installed module. The picked result is stored as
a self-contained token (`PhoenixKit.Mentions.Token`), indexed for reverse
lookup (`PhoenixKit.Mentions.Mention`), and rendered per viewer.

## The three halves

  * **Search** — `search/2` fans out across every module that declares a
    searchable resource type, in parallel, with a hard deadline. A module
    that is slow, raises, or has no index costs its own results and
    nothing else: the popup shows what came back.

  * **Resolve** — reuses `PhoenixKit.ResourceLinks`, which already turns
    `(type, uuid)` into a title and a deep-link for the Activity feed and
    Comments. Batched per type, so a page of mentions costs one call per
    distinct type, not one per mention.

  * **Visibility** — `visible/3` asks each type's handler which of these
    uuids THIS viewer may see. Separate from resolution on purpose: the
    searcher and the later reader are different people, and a record that
    was visible when it was linked may not be when it is read.

## What a viewer sees

| Situation | Rendered as |
|---|---|
| Resolves, viewer may see it | live title, real deep-link |
| Gone, or its module uninstalled | the author's stored label, plain text |
| Exists, viewer may NOT see it | a redacted chip — never the title |

The middle row is why the label lives in the text. The bottom row is the
rule that matters: a mention must never show a viewer a title they are not
allowed to know, and it must never *refresh* one. What it does show is
that something is there — the Discord model — with a way to ask for
access, because "you can't see this" is only useful with a next step.

## Making a module's records mentionable

Add the type to `resource_links/0` pointing at a handler module, then give
that handler two functions beyond the `resolve_comment_resources/1` it
already needs for Activity and Comments:

    defmodule MyApp.Widgets.Links do
      # Already required for deep-linking in Activity/Comments
      def resolve_comment_resources(uuids), do: %{...}

      # New: the `#` typeahead. MUST scope to the searcher.
      def search_resources(query, opts) do
        user_uuid = opts[:user_uuid]
        ...
        [%{type: "widget", uuid: w.uuid, title: w.name, subtitle: "Widget"}]
      end

      # New: which of these may this viewer see? Batched.
      def visible_resource_uuids(uuids, opts), do: [...]
    end

A handler without `search_resources/2` is resolvable but not offerable —
reasonable for a type that should be linkable from an activity row yet
never suggested by the typeahead.

`visible_resource_uuids/2` is what makes a type mentionable at all.
Without it, a mention of that type renders REDACTED, because a token can
be typed by hand into any textarea and "the module didn't implement the
check" must never mean "show it to everyone". The only exception is
`user`: the `@` typeahead already lists every pingable account to anyone
who can reach the admin area, so a name is not something a mention can
leak.

# `claim_for_delivery`

```elixir
@spec claim_for_delivery([PhoenixKit.Mentions.Mention.t()]) :: [
  PhoenixKit.Mentions.Mention.t()
]
```

CLAIMS delivery for these mentions, returning the ones this caller won.

`WHERE notified_at IS NULL` is what makes it a claim rather than a
stamp: two saves of the same field racing (a double submit, two tabs)
both see an unnotified row and would both send. Only the update that
actually changes a row may deliver.

# `context`

```elixir
@spec context(
  String.t() | nil,
  keyword()
) :: map()
```

Everything a renderer needs for the mentions in `text`, in one pass.

Returns `%{{type, uuid} => %{state, title, path, prefixed}}` where `state`
is `:ok`, `:missing`, or `:forbidden`. Batched per type: a page with
thirty mentions across three types costs three resolve calls and three
visibility calls.

# `enabled?`

```elixir
@spec enabled?() :: boolean()
```

Master switch. Defaults ON — the feature is additive and inert until
someone actually types a trigger.

# `enabled_setting_key`

```elixir
@spec enabled_setting_key() :: String.t()
```

# `list_backlinks`

```elixir
@spec list_backlinks(String.t(), String.t(), keyword()) :: [
  PhoenixKit.Mentions.Mention.t()
]
```

What links here: every record mentioning this one, newest first.

The backlink panel on a record's own page. Not permission-filtered — the
caller knows who is asking and what their own module's rules are.

# `list_for_source`

```elixir
@spec list_for_source(String.t(), String.t(), String.t()) :: [
  PhoenixKit.Mentions.Mention.t()
]
```

Every mention recorded for one field of one record.

# `notify`

```elixir
@spec notify(
  list(),
  keyword()
) :: :ok
```

Delivers the `@` pings among `mentions` and marks them sent.

Call it with what `sync/4` returned — those are the ones that are new.
Everything else is filtered here rather than by the caller:

  * `#` links never notify. Telling a record's watchers it was referenced
    is a different product (and a noisy one); the reverse index is enough
    to build a "mentioned in" panel when that is actually wanted.
  * mentioning yourself does nothing.
  * someone who cannot open the containing record is not told about it.
    An email about a comment that 403s on click is worse than silence.

Delivery itself is core's activity → notification bridge, so a ping lands
wherever that person already reads things and obeys their existing
preferences and channels.

# `redact_setting_key`

```elixir
@spec redact_setting_key() :: String.t()
```

# `redact_titles?`

```elixir
@spec redact_titles?() :: boolean()
```

Extra-security mode: withhold the TITLE of a mention the viewer cannot
open, showing the label the author stored instead of a live one.

Off by default. On, a record renamed after being mentioned never shows
its new name to someone who can't open it — worth having where the name
itself is the sensitive part, and unnecessary noise where it isn't.

# `search`

```elixir
@spec search(:user | :resource, String.t(), keyword()) :: [map()]
```

Candidates for the typeahead.

`kind` is `:user` for `@` (core's users only — a ping is always a person)
or `:resource` for `#` (every module that opted in).

Options:

  * `:user_uuid` — who is searching. Handlers MUST scope to this; a
    typeahead that offers records the searcher cannot open is itself the
    leak.
  * `:limit` — max results overall (default 8).

Never raises: a handler that blows up contributes nothing and is logged.

# `searchable_handlers`

```elixir
@spec searchable_handlers() :: %{required(String.t()) =&gt; module()}
```

Handler modules that can be searched, as `%{resource_type => module}`.

Derived from the same `resource_links/0` registry that powers deep-linking
— a type is offerable only if it is already resolvable, so a mention can
never be created that cannot later be rendered.

# `sync`

```elixir
@spec sync(String.t(), String.t(), String.t() | nil, keyword()) ::
  {:ok, [PhoenixKit.Mentions.Mention.t()]} | {:error, term()}
```

Rebuilds the index for one field of one record from its text, and returns
the mentions that are NEW since last time.

Call this from the durable save — creating or updating the comment, the
task, the note. Never from a keystroke or an autosave draft: the return
value is what gets notified, and a debounce would ping on every pause.

The delete-then-insert is scoped to this `source_field`, so a record with
two mentionable fields keeps them independent.

# `to_markdown`

```elixir
@spec to_markdown(
  String.t() | nil,
  keyword()
) :: String.t()
```

Rewrites the mentions in `text` as MARKDOWN, resolved for this viewer.

For surfaces that render markdown rather than HEEx — comments, notes,
anything going through `MDEx`. Same three states as the component, with
one honest limitation: markdown output is sanitised afterwards, so a
redacted mention here is plain text rather than a button. The reader
still learns that something exists and that it isn't theirs; they just
can't ask for access from inside a rendered comment.

    Mentions.to_markdown(comment.content, scope: scope)

Text with no mentions is returned unchanged, so this is safe to put in
front of every markdown render.

# `visible`

```elixir
@spec visible(String.t(), [String.t()], keyword()) :: [String.t()]
```

Of `uuids` for `type`, the ones this viewer may see.

A handler with no `visible_resource_uuids/2` is treated as public — see
the moduledoc. Anything that raises fails CLOSED (nothing visible): a
broken permission check must not become an open door.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
