-module(telega@dialog). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/telega/dialog.gleam"). -export([new/5, string_codec/0, json_codec/2, window/4, window_with_input/5, window_with_widgets/5, subdialog/4, on_sub_result/3, initial/2, on_done/2, with_ttl/2, with_labels/2, build/1, compiled/1, attach/2, attach_on_command/3, start/3, restart/3, widget_store/3, alert/2, toast/2]). -export_type([dialog/4, sub_attachment/3, dialog_builder/4]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. ?MODULEDOC( " # Declarative dialogs\n" "\n" " A dialog is a set of windows; a window is a pure render function plus\n" " event handlers. The engine renders everything into **one live message**\n" " (edit-or-send, including text ↔ media transitions — see\n" " `telega/dialog/render`), parses button callback data itself, keeps a\n" " navigation stack with `Back`, and persists all state in a `FlowStorage` —\n" " the dialog survives restarts with any persistent backend\n" " (Postgres/SQLite/Redis).\n" "\n" " Dialogs compile to `telega/flow` state machines: a window is a step,\n" " navigation is a flow action, delivery of callbacks/text into the active\n" " window is the flow registry's wait-token auto-resume. Nothing here needs\n" " a dedicated router route.\n" "\n" " Full guide (positioning vs conversations/flows/menu builder, widgets,\n" " sub-dialogs, i18n, testing):\n" " [docs/dialogs.md](https://hexdocs.pm/telega/docs/dialogs.html).\n" "\n" " ## Quick start\n" "\n" " ```gleam\n" " import telega/dialog\n" " import telega/dialog/types.{ActionButton, RenderedWindow}\n" " import telega/flow/registry as flow_registry\n" " import telega/flow/storage as flow_storage\n" " import telega/format\n" "\n" " fn render_menu(state: MyState, _ctx) -> RenderedWindow {\n" " RenderedWindow(\n" " text: format.build() |> format.bold_text(\"Settings\") |> format.to_formatted(),\n" " buttons: [[ActionButton(\"Name\", \"name\")], [ActionButton(\"Done\", \"done\")]],\n" " media: None,\n" " )\n" " }\n" "\n" " fn handle_menu(state, event: types.ActionEvent, _ctx) {\n" " case event.action_id {\n" " \"name\" -> Ok(types.Goto(\"name\", state))\n" " \"done\" -> Ok(types.Done(state))\n" " _ -> Ok(types.Stay(state))\n" " }\n" " }\n" "\n" " let assert Ok(settings) =\n" " dialog.new(\n" " id: \"settings\",\n" " storage: flow_storage,\n" " initial_state: fn() { MyState(name: \"\") },\n" " encode_state: encode_my_state,\n" " decode_state: decode_my_state,\n" " )\n" " |> dialog.window(id: \"menu\", render: render_menu, on_action: handle_menu)\n" " |> dialog.window_with_input(id: \"name\", render:, on_action:, on_text:)\n" " |> dialog.initial(\"menu\")\n" " |> dialog.on_done(save_settings)\n" " |> dialog.build()\n" "\n" " let registry =\n" " flow_registry.new_registry()\n" " |> dialog.attach_on_command(\"settings\", settings)\n" " |> flow_registry.register_cancel_command(\"cancel\")\n" "\n" " let router = flow_registry.apply_to_router(router, registry)\n" " ```\n" "\n" " ## Behavior notes\n" "\n" " - **One live instance** per `(dialog, chat, user)`: a repeated start\n" " command resumes (re-renders) the current dialog instead of opening a\n" " second one. Use `restart` for a hard reset.\n" " - **The dialog is modal**: while it waits for a callback, plain text\n" " messages are swallowed (the window is re-rendered). Commands still\n" " reach the router — register a `/cancel` via\n" " `flow_registry.register_cancel_command`.\n" " - **Widgets**: `window_with_widgets` attaches managed keyboards from\n" " `telega/dialog/widget` (pager, select, radio, multiselect,\n" " paged_select) — the engine renders their rows, handles their callbacks\n" " and persists their state; read selections with `widget_store`.\n" " - **Sub-dialogs**: `subdialog` attaches another built dialog (its state\n" " type may differ); any window starts it with `StartSub(sub_id, args,\n" " state)` and receives its exported result in `on_sub_result`. The sub\n" " shares the live message and the parent's storage/TTL/labels; `Back` on\n" " its first window cancels it; nesting is one level deep.\n" " - **Stale buttons**: a press on an outdated dialog message (an old\n" " window, or an old copy of the live message) answers with `labels.stale`\n" " and does nothing. Presses on messages of an already **finished** dialog\n" " are answered the same way by a fallback that `attach` registers\n" " automatically.\n" " - **Errors**: user handler errors are logged, emit\n" " `[\"telega\", \"dialog\", \"error\"]` telemetry and re-render the current\n" " window; failed renders (API errors, over-64-byte callback data) are\n" " logged loudly and keep the dialog alive.\n" ). -opaque dialog(BHHJ, BHHK, BHHL, BHHM) :: {dialog, telega@dialog@engine:compiled_dialog(BHHK, BHHL, BHHM), fun((BHHJ) -> binary()), fun((binary()) -> BHHJ)}. -type sub_attachment(BHHN, BHHO, BHHP) :: {sub_attachment, binary(), list(telega@dialog@types:window(binary(), BHHN, BHHO, BHHP)), binary(), fun((binary(), gleam@dict:dict(binary(), binary())) -> binary()), fun((binary()) -> gleam@dict:dict(binary(), binary())), boolean()}. -opaque dialog_builder(BHHQ, BHHR, BHHS, BHHT) :: {dialog_builder, binary(), list(telega@dialog@types:window(BHHQ, BHHR, BHHS, BHHT)), gleam@option:option(binary()), fun(() -> BHHQ), fun((BHHQ) -> binary()), fun((binary()) -> {ok, BHHQ} | {error, nil}), gleam@option:option(fun((BHHQ, telega@bot:context(BHHR, BHHS, BHHT)) -> {ok, telega@bot:context(BHHR, BHHS, BHHT)} | {error, BHHS})), list(sub_attachment(BHHR, BHHS, BHHT)), list({binary(), fun((BHHQ, gleam@dict:dict(binary(), binary()), telega@bot:context(BHHR, BHHS, BHHT)) -> {ok, telega@dialog@types:dialog_action(BHHQ)} | {error, BHHS})}), telega@flow@types:flow_storage(BHHS), gleam@option:option(integer()), fun((telega@bot:context(BHHR, BHHS, BHHT)) -> telega@dialog@types:labels())}. -file("src/telega/dialog.gleam", 178). ?DOC( " Start building a dialog. `encode_state`/`decode_state` serialize the\n" " user state for persistence (precedent: session serialization); for simple\n" " states see `string_codec` and `json_codec`.\n" ). -spec new( binary(), telega@flow@types:flow_storage(BHHU), fun(() -> BHHW), fun((BHHW) -> binary()), fun((binary()) -> {ok, BHHW} | {error, nil}) ) -> dialog_builder(BHHW, any(), BHHU, any()). new(Id, Storage, Initial_state, Encode_state, Decode_state) -> {dialog_builder, Id, [], none, Initial_state, Encode_state, Decode_state, none, [], [], Storage, none, fun(_) -> telega@dialog@types:default_labels() end}. -file("src/telega/dialog.gleam", 203). ?DOC( " Codec pair for a plain `String` state:\n" " `let #(encode, decode) = dialog.string_codec()`.\n" ). -spec string_codec() -> {fun((binary()) -> binary()), fun((binary()) -> {ok, binary()} | {error, nil})}. string_codec() -> {fun(State) -> State end, fun(Raw) -> {ok, Raw} end}. -file("src/telega/dialog.gleam", 212). ?DOC( " Codec pair from a JSON encoder + decoder:\n" " `let #(encode, decode) = dialog.json_codec(encode_settings, settings_decoder())`.\n" ). -spec json_codec( fun((BHIH) -> gleam@json:json()), gleam@dynamic@decode:decoder(BHIH) ) -> {fun((BHIH) -> binary()), fun((binary()) -> {ok, BHIH} | {error, nil})}. json_codec(Encoder, Decoder) -> {fun(State) -> gleam@json:to_string(Encoder(State)) end, fun(Raw) -> _pipe = gleam@json:parse(Raw, Decoder), gleam@result:replace_error(_pipe, nil) end}. -file("src/telega/dialog.gleam", 311). -spec add_window( dialog_builder(BHLH, BHLI, BHLJ, BHLK), telega@dialog@types:window(BHLH, BHLI, BHLJ, BHLK) ) -> dialog_builder(BHLH, BHLI, BHLJ, BHLK). add_window(Builder, Window) -> {dialog_builder, erlang:element(2, Builder), [Window | erlang:element(3, Builder)], erlang:element(4, Builder), erlang:element(5, Builder), erlang:element(6, Builder), erlang:element(7, Builder), erlang:element(8, Builder), erlang:element(9, Builder), erlang:element(10, Builder), erlang:element(11, Builder), erlang:element(12, Builder), erlang:element(13, Builder)}. -file("src/telega/dialog.gleam", 223). ?DOC( " Add a window that only reacts to button presses. Text sent to it is\n" " swallowed with a re-render.\n" ). -spec window( dialog_builder(BHIL, BHIM, BHIN, BHIO), binary(), fun((BHIL, telega@bot:context(BHIM, BHIN, BHIO)) -> telega@dialog@types:rendered_window()), fun((BHIL, telega@dialog@types:action_event(), telega@bot:context(BHIM, BHIN, BHIO)) -> {ok, telega@dialog@types:dialog_action(BHIL)} | {error, BHIN}) ) -> dialog_builder(BHIL, BHIM, BHIN, BHIO). window(Builder, Id, Render, On_action) -> add_window(Builder, {window, Id, Render, On_action, none, [], none}). -file("src/telega/dialog.gleam", 248). ?DOC(" Add a window that also accepts text input (e.g. \"enter your name\").\n"). -spec window_with_input( dialog_builder(BHJG, BHJH, BHJI, BHJJ), binary(), fun((BHJG, telega@bot:context(BHJH, BHJI, BHJJ)) -> telega@dialog@types:rendered_window()), fun((BHJG, telega@dialog@types:action_event(), telega@bot:context(BHJH, BHJI, BHJJ)) -> {ok, telega@dialog@types:dialog_action(BHJG)} | {error, BHJI}), fun((BHJG, binary(), telega@bot:context(BHJH, BHJI, BHJJ)) -> {ok, telega@dialog@types:dialog_action(BHJG)} | {error, BHJI}) ) -> dialog_builder(BHJG, BHJH, BHJI, BHJJ). window_with_input(Builder, Id, Render, On_action, On_text) -> add_window( Builder, {window, Id, Render, On_action, {some, On_text}, [], none} ). -file("src/telega/dialog.gleam", 284). ?DOC( " Add a window with managed keyboard widgets (see `telega/dialog/widget`).\n" " Widget button rows are appended after the window's own buttons and their\n" " events are handled by the widgets themselves, bypassing `on_action`.\n" "\n" " ```gleam\n" " |> dialog.window_with_widgets(id: \"fruits\", render:, on_action:, widgets: [\n" " widget.multiselect(id: \"f\", items: fruit_items, min: 1, max: 3,\n" " done: \"confirm\"),\n" " ])\n" " ```\n" ). -spec window_with_widgets( dialog_builder(BHKH, BHKI, BHKJ, BHKK), binary(), fun((BHKH, telega@bot:context(BHKI, BHKJ, BHKK)) -> telega@dialog@types:rendered_window()), fun((BHKH, telega@dialog@types:action_event(), telega@bot:context(BHKI, BHKJ, BHKK)) -> {ok, telega@dialog@types:dialog_action(BHKH)} | {error, BHKJ}), list(telega@dialog@types:keyboard_widget(BHKH, BHKI, BHKJ, BHKK)) ) -> dialog_builder(BHKH, BHKI, BHKJ, BHKK). window_with_widgets(Builder, Id, Render, On_action, Widgets) -> add_window(Builder, {window, Id, Render, On_action, none, Widgets, none}). -file("src/telega/dialog.gleam", 494). ?DOC( " Decode with a logged fallback to the initial state — a codec mismatch\n" " (e.g. a state shape change after a deploy) must not kill the dialog.\n" ). -spec decode_or_initial(dialog_builder(BHQH, any(), any(), any())) -> fun((binary()) -> BHQH). decode_or_initial(Builder) -> {dialog_builder, Id, _, _, Initial_state, _, Decode_state, _, _, _, _, _, _} = Builder, fun(Raw) -> case Decode_state(Raw) of {ok, State} -> State; {error, nil} -> logging:log( warning, <<<<"[dialog:"/utf8, Id/binary>>/binary, "] failed to decode state, falling back to initial"/utf8>> ), Initial_state() end end. -file("src/telega/dialog.gleam", 334). ?DOC( " Attach a built dialog as a **sub-dialog**, startable from any window via\n" " `StartSub(sub_id, args, state)` (the sub id is the attached dialog's id).\n" " The sub takes over the live dialog message; its `Done` hands control back\n" " to the window that started it (see `on_sub_result`). Nesting is one level\n" " deep: a dialog with sub-dialogs of its own cannot be attached\n" " (`NestedSubDialog`).\n" "\n" " - `init` builds the sub's starting state from the parent state and the\n" " `StartSub` args.\n" " - `result` exports the sub's final state as the dict handed to the parent\n" " window's `on_sub_result`; prefix the keys with the sub id by convention\n" " (`\"address.city\"`) to keep them collision-free.\n" "\n" " The attached dialog's own `storage`, `ttl`, `labels` and `on_done` are\n" " ignored while it runs as a sub — the parent's apply. A `Back` on the\n" " sub's first window cancels the sub (returns without a result).\n" ). -spec subdialog( dialog_builder(BHLX, BHLY, BHLZ, BHMA), dialog(BHMF, BHLY, BHLZ, BHMA), fun((BHLX, gleam@dict:dict(binary(), binary())) -> BHMF), fun((BHMF) -> gleam@dict:dict(binary(), binary())) ) -> dialog_builder(BHLX, BHLY, BHLZ, BHMA). subdialog(Builder, Sub, Init, Result) -> Parent_decode = decode_or_initial(Builder), Attachment = {sub_attachment, erlang:element(2, erlang:element(2, Sub)), maps:values(erlang:element(3, erlang:element(2, Sub))), erlang:element(4, erlang:element(2, Sub)), fun(Parent_raw, Args) -> (erlang:element(3, Sub))(Init(Parent_decode(Parent_raw), Args)) end, fun(Sub_raw) -> Result((erlang:element(4, Sub))(Sub_raw)) end, maps:size(erlang:element(7, erlang:element(2, Sub))) > 0}, {dialog_builder, erlang:element(2, Builder), erlang:element(3, Builder), erlang:element(4, Builder), erlang:element(5, Builder), erlang:element(6, Builder), erlang:element(7, Builder), erlang:element(8, Builder), [Attachment | erlang:element(9, Builder)], erlang:element(10, Builder), erlang:element(11, Builder), erlang:element(12, Builder), erlang:element(13, Builder)}. -file("src/telega/dialog.gleam", 360). ?DOC( " Handle the result of a sub-dialog started from `window`: the handler\n" " receives the window's state and the result dict exported by the\n" " `subdialog` attachment, and returns the next action (`Stay` re-renders\n" " the window with the updated state). Without a handler the window is\n" " simply re-rendered.\n" ). -spec on_sub_result( dialog_builder(BHMS, BHMT, BHMU, BHMV), binary(), fun((BHMS, gleam@dict:dict(binary(), binary()), telega@bot:context(BHMT, BHMU, BHMV)) -> {ok, telega@dialog@types:dialog_action(BHMS)} | {error, BHMU}) ) -> dialog_builder(BHMS, BHMT, BHMU, BHMV). on_sub_result(Builder, Window, Handler) -> {dialog_builder, erlang:element(2, Builder), erlang:element(3, Builder), erlang:element(4, Builder), erlang:element(5, Builder), erlang:element(6, Builder), erlang:element(7, Builder), erlang:element(8, Builder), erlang:element(9, Builder), [{Window, Handler} | erlang:element(10, Builder)], erlang:element(11, Builder), erlang:element(12, Builder), erlang:element(13, Builder)}. -file("src/telega/dialog.gleam", 376). ?DOC(" Set the window the dialog opens with.\n"). -spec initial(dialog_builder(BHNM, BHNN, BHNO, BHNP), binary()) -> dialog_builder(BHNM, BHNN, BHNO, BHNP). initial(Builder, Window_id) -> {dialog_builder, erlang:element(2, Builder), erlang:element(3, Builder), {some, Window_id}, erlang:element(5, Builder), erlang:element(6, Builder), erlang:element(7, Builder), erlang:element(8, Builder), erlang:element(9, Builder), erlang:element(10, Builder), erlang:element(11, Builder), erlang:element(12, Builder), erlang:element(13, Builder)}. -file("src/telega/dialog.gleam", 385). ?DOC( " Called when a window returns `Done`: receives the final state. The live\n" " message keeps its text but loses the keyboard.\n" ). -spec on_done( dialog_builder(BHNY, BHNZ, BHOA, BHOB), fun((BHNY, telega@bot:context(BHNZ, BHOA, BHOB)) -> {ok, telega@bot:context(BHNZ, BHOA, BHOB)} | {error, BHOA}) ) -> dialog_builder(BHNY, BHNZ, BHOA, BHOB). on_done(Builder, Handler) -> {dialog_builder, erlang:element(2, Builder), erlang:element(3, Builder), erlang:element(4, Builder), erlang:element(5, Builder), erlang:element(6, Builder), erlang:element(7, Builder), {some, Handler}, erlang:element(9, Builder), erlang:element(10, Builder), erlang:element(11, Builder), erlang:element(12, Builder), erlang:element(13, Builder)}. -file("src/telega/dialog.gleam", 394). ?DOC(" Expire the dialog after `ms` milliseconds (lazy check on next event).\n"). -spec with_ttl(dialog_builder(BHOS, BHOT, BHOU, BHOV), integer()) -> dialog_builder(BHOS, BHOT, BHOU, BHOV). with_ttl(Builder, Ms) -> {dialog_builder, erlang:element(2, Builder), erlang:element(3, Builder), erlang:element(4, Builder), erlang:element(5, Builder), erlang:element(6, Builder), erlang:element(7, Builder), erlang:element(8, Builder), erlang:element(9, Builder), erlang:element(10, Builder), erlang:element(11, Builder), {some, Ms}, erlang:element(13, Builder)}. -file("src/telega/dialog.gleam", 404). ?DOC( " Localize engine-generated texts (stale-button notice, widget labels).\n" " The factory receives the update's `Context`, so `telega_i18n.t` works:\n" " `dialog.with_labels(builder, fn(ctx) { labels_from_i18n(ctx) })`.\n" ). -spec with_labels( dialog_builder(BHPE, BHPF, BHPG, BHPH), fun((telega@bot:context(BHPF, BHPG, BHPH)) -> telega@dialog@types:labels()) ) -> dialog_builder(BHPE, BHPF, BHPG, BHPH). with_labels(Builder, Labels) -> {dialog_builder, erlang:element(2, Builder), erlang:element(3, Builder), erlang:element(4, Builder), erlang:element(5, Builder), erlang:element(6, Builder), erlang:element(7, Builder), erlang:element(8, Builder), erlang:element(9, Builder), erlang:element(10, Builder), erlang:element(11, Builder), erlang:element(12, Builder), Labels}. -file("src/telega/dialog.gleam", 765). -spec namespaced_id(binary(), binary()) -> binary(). namespaced_id(Sub_id, Window_id) -> <<<>)/binary>>/binary, Window_id/binary>>. -file("src/telega/dialog.gleam", 771). ?DOC( " Re-key an (already erased) sub-dialog window under the sub namespace and\n" " map its navigation targets into it.\n" ). -spec namespace_window( binary(), telega@dialog@types:window(binary(), BHUX, BHUY, BHUZ) ) -> telega@dialog@types:window(binary(), BHUX, BHUY, BHUZ). namespace_window(Sub_id, Window) -> Map_action = fun(Action) -> case Action of {goto, Window_id, State} -> {goto, namespaced_id(Sub_id, Window_id), State}; Other -> Other end end, {window, namespaced_id(Sub_id, erlang:element(2, Window)), erlang:element(3, Window), fun(Raw, Event, Ctx) -> _pipe = (erlang:element(4, Window))(Raw, Event, Ctx), gleam@result:map(_pipe, Map_action) end, gleam@option:map( erlang:element(5, Window), fun(On_text) -> fun(Raw@1, Text, Ctx@1) -> _pipe@1 = On_text(Raw@1, Text, Ctx@1), gleam@result:map(_pipe@1, Map_action) end end ), gleam@list:map( erlang:element(6, Window), fun(Widget_item) -> {keyboard_widget, erlang:element(2, Widget_item), erlang:element(3, Widget_item), fun(Wctx, Cmd, Arg) -> _pipe@2 = (erlang:element(4, Widget_item))( Wctx, Cmd, Arg ), gleam@result:map( _pipe@2, fun(Widget_result) -> case Widget_result of {emit, Action@1} -> {emit, Map_action(Action@1)}; Other@1 -> Other@1 end end ) end, gleam@list:map( erlang:element(5, Widget_item), fun(_capture) -> namespaced_id(Sub_id, _capture) end ), erlang:element(6, Widget_item)} end ), gleam@option:map( erlang:element(7, Window), fun(Handler) -> fun(Raw@2, Sub_result, Ctx@2) -> _pipe@3 = Handler(Raw@2, Sub_result, Ctx@2), gleam@result:map(_pipe@3, Map_action) end end )}. -file("src/telega/dialog.gleam", 714). -spec erase_action( telega@dialog@types:dialog_action(BHTW), fun((BHTW) -> binary()) ) -> telega@dialog@types:dialog_action(binary()). erase_action(Action, Encode) -> case Action of {stay, State} -> {stay, Encode(State)}; {goto, Window_id, State@1} -> {goto, Window_id, Encode(State@1)}; {back, State@2} -> {back, Encode(State@2)}; {done, State@3} -> {done, Encode(State@3)}; {start_sub, Sub_id, Args, State@4} -> {start_sub, Sub_id, Args, Encode(State@4)} end. -file("src/telega/dialog.gleam", 753). -spec typed_widget_ctx( telega@dialog@types:widget_ctx(binary(), BHUL, BHUM, BHUN), fun((binary()) -> BHUS) ) -> telega@dialog@types:widget_ctx(BHUS, BHUL, BHUM, BHUN). typed_widget_ctx(Wctx, Decode) -> {widget_ctx, Decode(erlang:element(2, Wctx)), erlang:element(3, Wctx), erlang:element(4, Wctx), erlang:element(5, Wctx)}. -file("src/telega/dialog.gleam", 729). -spec erase_widget( telega@dialog@types:keyboard_widget(BHTZ, BHUA, BHUB, BHUC), fun((BHTZ) -> binary()), fun((binary()) -> BHTZ) ) -> telega@dialog@types:keyboard_widget(binary(), BHUA, BHUB, BHUC). erase_widget(Widget_item, Encode, Decode) -> {keyboard_widget, erlang:element(2, Widget_item), fun(Wctx) -> (erlang:element(3, Widget_item))(typed_widget_ctx(Wctx, Decode)) end, fun(Wctx@1, Cmd, Arg) -> _pipe = (erlang:element(4, Widget_item))( typed_widget_ctx(Wctx@1, Decode), Cmd, Arg ), gleam@result:map(_pipe, fun(Widget_result) -> case Widget_result of {store_updated, Store} -> {store_updated, Store}; {emit, Action} -> {emit, erase_action(Action, Encode)} end end) end, erlang:element(5, Widget_item), erlang:element(6, Widget_item)}. -file("src/telega/dialog.gleam", 687). -spec erase_window( telega@dialog@types:window(BHTK, BHTL, BHTM, BHTN), fun((BHTK) -> binary()), fun((binary()) -> BHTK) ) -> telega@dialog@types:window(binary(), BHTL, BHTM, BHTN). erase_window(Window, Encode, Decode) -> {window, erlang:element(2, Window), fun(Raw, Ctx) -> (erlang:element(3, Window))(Decode(Raw), Ctx) end, fun(Raw@1, Event, Ctx@1) -> _pipe = (erlang:element(4, Window))(Decode(Raw@1), Event, Ctx@1), gleam@result:map( _pipe, fun(_capture) -> erase_action(_capture, Encode) end ) end, gleam@option:map( erlang:element(5, Window), fun(On_text) -> fun(Raw@2, Text, Ctx@2) -> _pipe@1 = On_text(Decode(Raw@2), Text, Ctx@2), gleam@result:map( _pipe@1, fun(_capture@1) -> erase_action(_capture@1, Encode) end ) end end ), gleam@list:map( erlang:element(6, Window), fun(_capture@2) -> erase_widget(_capture@2, Encode, Decode) end ), gleam@option:map( erlang:element(7, Window), fun(Handler) -> fun(Raw@3, Sub_result, Ctx@3) -> _pipe@2 = Handler(Decode(Raw@3), Sub_result, Ctx@3), gleam@result:map( _pipe@2, fun(_capture@3) -> erase_action(_capture@3, Encode) end ) end end )}. -file("src/telega/dialog.gleam", 671). -spec require( boolean(), telega@dialog@types:dialog_build_error(), fun(() -> {ok, BHTF} | {error, telega@dialog@types:dialog_build_error()}) ) -> {ok, BHTF} | {error, telega@dialog@types:dialog_build_error()}. require(Condition, Build_error, Continue) -> gleam@bool:guard(not Condition, {error, Build_error}, Continue). -file("src/telega/dialog.gleam", 547). ?DOC( " Measure the callback-data budget through the very function that packs it\n" " at render time (`render.callback_data`), so build-time validation and\n" " runtime packing can never diverge.\n" ). -spec validate_budget(binary(), binary(), binary()) -> {ok, nil} | {error, telega@dialog@types:dialog_build_error()}. validate_budget(Dialog_id, Window_id, Action_id) -> Bytes = erlang:byte_size( telega@dialog@render:callback_data( Dialog_id, Window_id, Action_id, none ) ), require( Bytes =< 64, {callback_data_too_long, Window_id, Action_id, Bytes}, fun() -> {ok, nil} end ). -file("src/telega/dialog.gleam", 641). ?DOC( " Sub-dialog attachments: unique ids, one nesting level, and the re-checked\n" " 64-byte budget — the sub was validated against its own dialog id, but at\n" " runtime its buttons carry the parent id plus the `.` prefix.\n" ). -spec validate_subs(binary(), list(sub_attachment(any(), any(), any()))) -> {ok, nil} | {error, telega@dialog@types:dialog_build_error()}. validate_subs(Dialog_id, Subs) -> gleam@result:'try'( gleam@list:try_fold( Subs, gleam@set:new(), fun(Seen, Sub) -> require( not gleam@set:contains(Seen, erlang:element(2, Sub)), {duplicate_sub_dialog_id, erlang:element(2, Sub)}, fun() -> require( not erlang:element(7, Sub), {nested_sub_dialog, erlang:element(2, Sub)}, fun() -> gleam@result:'try'( gleam@list:try_each( erlang:element(3, Sub), fun(Window) -> Namespaced = namespaced_id( erlang:element(2, Sub), erlang:element(2, Window) ), gleam@result:'try'( validate_budget( Dialog_id, Namespaced, <<"x"/utf8>> ), fun(_use0) -> nil = _use0, gleam@list:try_each( erlang:element( 6, Window ), fun(Widget) -> gleam@list:try_each( erlang:element( 6, Widget ), fun(_capture) -> validate_budget( Dialog_id, Namespaced, _capture ) end ) end ) end ) end ), fun(_use0@1) -> nil = _use0@1, {ok, gleam@set:insert( Seen, erlang:element(2, Sub) )} end ) end ) end ) end ), fun(_) -> {ok, nil} end ). -file("src/telega/dialog.gleam", 621). ?DOC( " Widget `Goto` targets (e.g. a multiselect's `done` window) must reference\n" " existing windows; checked once all windows are known.\n" ). -spec validate_widget_targets( list(telega@dialog@types:window(any(), any(), any(), any())) ) -> {ok, nil} | {error, telega@dialog@types:dialog_build_error()}. validate_widget_targets(Windows) -> Window_ids = gleam@set:from_list( gleam@list:map(Windows, fun(Window) -> erlang:element(2, Window) end) ), gleam@list:try_each( Windows, fun(Window@1) -> gleam@list:try_each( erlang:element(6, Window@1), fun(Widget) -> gleam@list:try_each( erlang:element(5, Widget), fun(Target) -> require( gleam@set:contains(Window_ids, Target), {unknown_window_reference, erlang:element(2, Window@1), Target}, fun() -> {ok, nil} end ) end ) end ) end ). -file("src/telega/dialog.gleam", 591). -spec validate_widgets( binary(), binary(), telega@dialog@types:window(any(), any(), any(), any()) ) -> {ok, nil} | {error, telega@dialog@types:dialog_build_error()}. validate_widgets(Dialog_id, Window_id, Window) -> gleam@result:'try'( gleam@list:try_fold( erlang:element(6, Window), gleam@set:new(), fun(Seen, Widget) -> require( not gleam_stdlib:contains_string( erlang:element(2, Widget), <<":"/utf8>> ), {reserved_id_character, <<"widget"/utf8>>, erlang:element(2, Widget)}, fun() -> require( not gleam@set:contains( Seen, erlang:element(2, Widget) ), {duplicate_widget_id, erlang:element(2, Window), erlang:element(2, Widget)}, fun() -> gleam@result:'try'( gleam@list:try_each( erlang:element(6, Widget), fun(_capture) -> validate_budget( Dialog_id, Window_id, _capture ) end ), fun(_use0) -> nil = _use0, {ok, gleam@set:insert( Seen, erlang:element(2, Widget) )} end ) end ) end ) end ), fun(_) -> {ok, nil} end ). -file("src/telega/dialog.gleam", 566). -spec validate_windows( binary(), list(telega@dialog@types:window(any(), any(), any(), any())) ) -> {ok, nil} | {error, telega@dialog@types:dialog_build_error()}. validate_windows(Dialog_id, Windows) -> gleam@result:'try'( gleam@list:try_fold( Windows, gleam@set:new(), fun(Seen, Window) -> require( not gleam_stdlib:contains_string( erlang:element(2, Window), <<":"/utf8>> ) andalso not gleam_stdlib:contains_string( erlang:element(2, Window), <<"."/utf8>> ), {reserved_id_character, <<"window"/utf8>>, erlang:element(2, Window)}, fun() -> require( not gleam@set:contains( Seen, erlang:element(2, Window) ), {duplicate_window_id, erlang:element(2, Window)}, fun() -> gleam@result:'try'( validate_budget( Dialog_id, erlang:element(2, Window), <<"x"/utf8>> ), fun(_use0) -> nil = _use0, gleam@result:'try'( validate_widgets( Dialog_id, erlang:element(2, Window), Window ), fun(_use0@1) -> nil = _use0@1, {ok, gleam@set:insert( Seen, erlang:element( 2, Window ) )} end ) end ) end ) end ) end ), fun(_) -> {ok, nil} end ). -file("src/telega/dialog.gleam", 514). -spec attach_sub_result_hooks( list(telega@dialog@types:window(BHQP, BHQQ, BHQR, BHQS)), list({binary(), fun((BHQP, gleam@dict:dict(binary(), binary()), telega@bot:context(BHQQ, BHQR, BHQS)) -> {ok, telega@dialog@types:dialog_action(BHQP)} | {error, BHQR})}) ) -> {ok, list(telega@dialog@types:window(BHQP, BHQQ, BHQR, BHQS))} | {error, telega@dialog@types:dialog_build_error()}. attach_sub_result_hooks(Windows, Hooks) -> gleam@list:try_fold( Hooks, Windows, fun(Windows@1, Hook) -> {Window_id, Handler} = Hook, require( gleam@list:any( Windows@1, fun(Window) -> erlang:element(2, Window) =:= Window_id end ), {unknown_window_reference, <<"on_sub_result"/utf8>>, Window_id}, fun() -> {ok, gleam@list:map( Windows@1, fun(Window@1) -> case erlang:element(2, Window@1) =:= Window_id of true -> {window, erlang:element(2, Window@1), erlang:element(3, Window@1), erlang:element(4, Window@1), erlang:element(5, Window@1), erlang:element(6, Window@1), {some, Handler}}; false -> Window@1 end end )} end ) end ). -file("src/telega/dialog.gleam", 420). ?DOC( " Validate and build the dialog. Checks window ids for duplicates and the\n" " reserved `:`/`.` characters, the initial window, `on_sub_result` and\n" " widget window references, sub-dialog attachments, and that all\n" " callback-data prefixes (including the `.` namespace of sub\n" " windows) leave room within Telegram's 64-byte limit.\n" "\n" " Building also **erases** the state type: every window is wrapped so its\n" " closures decode/encode the state with this dialog's codec, which is what\n" " lets sub-dialogs with different state types share one flow.\n" ). -spec build(dialog_builder(BHPT, BHPU, BHPV, BHPW)) -> {ok, dialog(BHPT, BHPU, BHPV, BHPW)} | {error, telega@dialog@types:dialog_build_error()}. build(Builder) -> Windows = lists:reverse(erlang:element(3, Builder)), Subs = lists:reverse(erlang:element(9, Builder)), require( Windows /= [], no_windows, fun() -> require( not gleam_stdlib:contains_string( erlang:element(2, Builder), <<":"/utf8>> ) andalso not gleam_stdlib:contains_string( erlang:element(2, Builder), <<"."/utf8>> ), {reserved_id_character, <<"dialog"/utf8>>, erlang:element(2, Builder)}, fun() -> gleam@result:'try'( attach_sub_result_hooks( Windows, lists:reverse(erlang:element(10, Builder)) ), fun(Windows@1) -> gleam@result:'try'( validate_windows( erlang:element(2, Builder), Windows@1 ), fun(_use0) -> nil = _use0, gleam@result:'try'( validate_widget_targets(Windows@1), fun(_use0@1) -> nil = _use0@1, gleam@result:'try'( validate_subs( erlang:element(2, Builder), Subs ), fun(_use0@2) -> nil = _use0@2, gleam@result:'try'( gleam@option:to_result( erlang:element( 4, Builder ), {unknown_initial_window, <<""/utf8>>} ), fun(Initial) -> require( gleam@list:any( Windows@1, fun(Window) -> erlang:element( 2, Window ) =:= Initial end ), {unknown_initial_window, Initial}, fun() -> Encode = erlang:element( 6, Builder ), Decode = decode_or_initial( Builder ), Erased_own = gleam@list:map( Windows@1, fun( Window@1 ) -> {erlang:element( 2, Window@1 ), erase_window( Window@1, Encode, Decode )} end ), Erased_subs = gleam@list:flat_map( Subs, fun(Sub) -> gleam@list:map( erlang:element( 3, Sub ), fun( Window@2 ) -> Namespaced = namespace_window( erlang:element( 2, Sub ), Window@2 ), {erlang:element( 2, Namespaced ), Namespaced} end ) end ), {ok, {dialog, {compiled_dialog, erlang:element( 2, Builder ), maps:from_list( lists:append( Erased_own, Erased_subs ) ), Initial, fun( ) -> Encode( (erlang:element( 5, Builder ))( ) ) end, gleam@option:map( erlang:element( 8, Builder ), fun( On_done ) -> fun( Raw, Ctx ) -> On_done( Decode( Raw ), Ctx ) end end ), maps:from_list( gleam@list:map( Subs, fun( Sub@1 ) -> {erlang:element( 2, Sub@1 ), {compiled_sub, erlang:element( 2, Sub@1 ), namespaced_id( erlang:element( 2, Sub@1 ), erlang:element( 4, Sub@1 ) ), erlang:element( 5, Sub@1 ), erlang:element( 6, Sub@1 )}} end ) ), erlang:element( 11, Builder ), erlang:element( 12, Builder ), erlang:element( 13, Builder )}, Encode, Decode}} end ) end ) end ) end ) end ) end ) end ) end ). -file("src/telega/dialog.gleam", 819). ?DOC(false). -spec compiled(dialog(any(), BHVJ, BHVK, BHVL)) -> telega@dialog@engine:compiled_dialog(BHVJ, BHVK, BHVL). compiled(Dialog) -> erlang:element(2, Dialog). -file("src/telega/dialog.gleam", 860). -spec with_dialog_routing( telega@flow@registry:flow_registry(BHWV, BHWW, BHWX), dialog(any(), BHWV, BHWW, BHWX) ) -> telega@flow@registry:flow_registry(BHWV, BHWW, BHWX). with_dialog_routing(Registry, Dialog) -> Flow_name = <<(<<"__dialog:"/utf8>>)/binary, (erlang:element(2, erlang:element(2, Dialog)))/binary>>, Prefix = <<<<"dlg:"/utf8, (erlang:element(2, erlang:element(2, Dialog)))/binary>>/binary, ":"/utf8>>, Labels = erlang:element(10, erlang:element(2, Dialog)), _pipe = Registry, _pipe@1 = telega@flow@registry:with_callback_filter( _pipe, Flow_name, fun(_capture) -> gleam_stdlib:string_starts_with(_capture, Prefix) end ), telega@flow@registry:with_orphan_callback_handler( _pipe@1, fun(_capture@1) -> gleam_stdlib:string_starts_with(_capture@1, Prefix) end, fun(Ctx, _) -> telega@dialog@render:answer_quietly( Ctx, {some, erlang:element(10, Labels(Ctx))} ), {ok, Ctx} end ). -file("src/telega/dialog.gleam", 836). ?DOC( " Register the dialog in a flow registry without a trigger — start it\n" " programmatically with `start`. Event delivery into the active window is\n" " the registry's standard auto-resume, so remember to finish with\n" " `flow_registry.apply_to_router`.\n" "\n" " Attaching also wires two routing guards for free: the dialog only\n" " auto-resumes on its own `dlg::` callbacks (so several waiting\n" " flows/dialogs can coexist), and presses on messages of an already\n" " finished dialog are answered with `labels.stale` instead of hanging.\n" ). -spec attach( telega@flow@registry:flow_registry(BHVT, BHVU, BHVV), dialog(any(), BHVT, BHVU, BHVV) ) -> telega@flow@registry:flow_registry(BHVT, BHVU, BHVV). attach(Registry, Dialog) -> _pipe = telega@flow@registry:register_callable( Registry, telega@dialog@engine:compile(erlang:element(2, Dialog)) ), with_dialog_routing(_pipe, Dialog). -file("src/telega/dialog.gleam", 847). ?DOC( " Register the dialog and start it on a command (e.g. `\"settings\"` for\n" " `/settings`). A repeated command while the dialog is active resumes it.\n" " Wires the same routing guards as `attach`.\n" ). -spec attach_on_command( telega@flow@registry:flow_registry(BHWH, BHWI, BHWJ), binary(), dialog(any(), BHWH, BHWI, BHWJ) ) -> telega@flow@registry:flow_registry(BHWH, BHWI, BHWJ). attach_on_command(Registry, Command, Dialog) -> _pipe = telega@flow@registry:register( Registry, {on_command, Command}, telega@dialog@engine:compile(erlang:element(2, Dialog)) ), with_dialog_routing(_pipe, Dialog). -file("src/telega/dialog.gleam", 882). ?DOC(" Start (or resume) an attached dialog from any handler.\n"). -spec start( telega@bot:context(BHXJ, BHXK, BHXL), telega@flow@registry:flow_registry(BHXJ, BHXK, BHXL), binary() ) -> {ok, telega@bot:context(BHXJ, BHXK, BHXL)} | {error, BHXK}. start(Ctx, Registry, Dialog_id) -> telega@flow@registry:call_flow( Ctx, Registry, <<(<<"__dialog:"/utf8>>)/binary, Dialog_id/binary>>, maps:new() ). -file("src/telega/dialog.gleam", 897). ?DOC( " Delete the current instance and start the dialog from scratch (a\n" " repeated start command only resumes — this is the hard reset).\n" ). -spec restart( telega@bot:context(BHXX, BHXY, BHXZ), telega@flow@registry:flow_registry(BHXX, BHXY, BHXZ), binary() ) -> {ok, telega@bot:context(BHXX, BHXY, BHXZ)} | {error, BHXY}. restart(Ctx, Registry, Dialog_id) -> {User_id, Chat_id} = telega@flow@engine:extract_ids_from_context(Ctx), Instance_id = telega@flow@storage:generate_id( User_id, Chat_id, <<(<<"__dialog:"/utf8>>)/binary, Dialog_id/binary>> ), _ = telega@flow@registry:cancel_flow_instance(Registry, Instance_id), start(Ctx, Registry, Dialog_id). -file("src/telega/dialog.gleam", 926). ?DOC( " Read a widget's persistent store from inside a window render or handler\n" " (`on_action`, `on_text`, `on_done`). Combine with the typed readers from\n" " `telega/dialog/widget`:\n" "\n" " ```gleam\n" " let zone =\n" " dialog.widget_store(ctx, window_id: \"prefs\", widget_id: \"zone\")\n" " |> widget.radio_value\n" " |> option.unwrap(\"hall\")\n" " ```\n" "\n" " Returns an empty store when the widget has no state yet. In pure render\n" " tests seed the store first with `widget.seed_store`.\n" ). -spec widget_store(telega@bot:context(any(), any(), any()), binary(), binary()) -> telega@dialog@types:widget_store(). widget_store(Ctx, Window_id, Widget_id) -> telega@dialog@widget:widget_store(Ctx, Window_id, Widget_id). -file("src/telega/dialog.gleam", 939). ?DOC( " Show a modal alert to the user who pressed the button. Call inside\n" " `on_action` before returning an action; the engine will skip its\n" " automatic spinner-removing answer for this event.\n" ). -spec alert(telega@bot:context(any(), any(), any()), binary()) -> {ok, nil} | {error, telega@error:telega_error()}. alert(Ctx, Text) -> telega@dialog@render:alert(Ctx, Text). -file("src/telega/dialog.gleam", 948). ?DOC( " Show a toast notification at the top of the chat. Same contract as\n" " `alert`.\n" ). -spec toast(telega@bot:context(any(), any(), any()), binary()) -> {ok, nil} | {error, telega@error:telega_error()}. toast(Ctx, Text) -> telega@dialog@render:toast(Ctx, Text).