UI description format
A plugin never draws widgets itself. It sends a page (a plain map, CBOR on the wire, a dict in Python) and the app draws it with native widgets. The host validates the page strictly before the app sees it: every unknown field, wrong type or out-of-range value rejects the whole call with the error code invalid (freshgram.Invalid in Python) and a message from the list below. The app receives a normalised copy in which all defaults are filled in.
This page is the reference. For an introduction see UI: menus, commands, windows; for error codes see Errors and limits; for the call names see Call protocol (ops); for event names see Events.
Where a page is used
Section titled “Where a page is used”| Python | Operation | Permission | Notes |
|---|---|---|---|
ui.settings_page(title, items, buttons=None) |
ui.settings.set |
ui.settings_page |
The plugin’s settings page. Needs "settingsPage": true in contributes. Send it in response to settings.open. |
ui.open_settings() |
ui.open_settings |
ui.settings_page |
Opens that page. Only within 10 s after a user action (otherwise denied). |
ui.dialog(title, items, buttons=None) |
ui.dialog.show |
ui.dialog |
One dialog (a box over the chat) per plugin and account. |
ui.close_dialog() |
ui.dialog.close |
ui.dialog |
Closes it; no dialog.close event comes back. |
ui.window(window_id, title, items, buttons=None, mode="layer") |
ui.window.show |
ui.dialog |
Opens a window, or updates it if id is already open. |
ui.update_window(window_id, title, items, buttons=None) |
ui.window.update |
ui.dialog |
Updates an open window; not_found if it is not open. |
ui.close_window(window_id) |
ui.window.close |
ui.dialog |
Closes it; no window.close event comes back. |
Rust and C send the same operations with the arguments page (and id, mode for windows); see Call protocol (ops). The Python helpers pass only title, items and buttons; to send closable (see below) call the operation ui.dialog.show directly.
The page
Section titled “The page”{ "title": "My plugin", # required "items": [ ... ], # required, 0..64 items "buttons": [ ... ], # optional, 0..3 "closable": True, # optional}| Field | Type | Required | Limit / default | Meaning |
|---|---|---|---|---|
title |
text | yes | up to 120 bytes, one line | The title of the settings box, dialog, layer or window. An empty title is shown as the plugin name. |
items |
list of items | yes | at most 64 on every level, 256 items in the whole page, nesting depth 4 | The content. May be empty. |
buttons |
list | no | at most 3 | Page buttons, see below. |
closable |
bool | no | default true |
false stops the user from closing a dialog or layer with Esc, a click outside or the Close button. It is allowed only together with at least one entry in buttons (otherwise a page that cannot be closed needs a button). An uncloseable dialog is closed by the app after 2 minutes. Windows with mode="window" ignore it. |
Unknown keys on the page are rejected (unknown field …).
Text rules
Section titled “Text rules”“Text” in this reference is a UTF-8 string, limited in bytes, without control characters (below U+0020 except \n where newlines are allowed, and U+007F) and without bidirectional controls (U+200E, U+200F, U+202A to U+202E, U+2066 to U+2069, U+061C). A field that breaks this rule fails with <field> is not valid text.
Identifiers
Section titled “Identifiers”id: 1 to 64 characters from a-z 0-9 _ - .. Every id must be unique across the whole page: items, nested items, tab ids, list entry ids, sub-page ids and page button ids share one namespace. A missing, malformed or repeated id gives an item has a bad or repeated id. Window ids (ui.window.*) are limited to 32 characters from the same set.
alias (see common fields): up to 32 characters from the same set, unique per page.
Page buttons
Section titled “Page buttons”"buttons": [ {"id": "ok", "label": "Save", "style": "primary"}, {"id": "cancel", "label": "Cancel"},]| Field | Type | Required | Limit / default |
|---|---|---|---|
id |
id | yes | shares the page’s id namespace |
label |
text | yes | up to 40 bytes, one line |
style |
text | no | up to 16 bytes: default (default), primary or danger |
In a dialog and in a mode="layer" window the buttons are drawn at the bottom of the box (a standard Close button is added after them unless closable is false). On the settings page and in a mode="window" window they are drawn as rows at the end of the page. A press sends <scope>.click with the button id.
Item types
Section titled “Item types”Every item is a map with type and the fields of that type. Any item type can be used on the settings page, in a dialog and in a window; containers can hold any items up to the depth limit.
type |
Interactive | Event it sends |
|---|---|---|
text |
no | none |
header |
no | none |
code |
no | none |
divider |
no | none |
link |
opens a URL | none |
progress |
no | none |
toggle |
yes | change, value is a bool |
button |
yes | click, optionally long_click |
input |
yes | change, value is text |
slider |
yes | change, value is an int |
select |
yes | change, value is an option value |
chat |
yes | change, value is a chat id |
page |
opens a sub-page | long_click optionally |
row, column, card |
containers | none |
tabs |
yes | change, value is a tab id |
image |
no | none |
list |
yes | click / long_click of an entry |
table |
no | none |
The event names are described in Events sent back.
Limits for the whole tree
Section titled “Limits for the whole tree”| Limit | Value | Error |
|---|---|---|
items in any one list (items, a container’s items, a tab’s items) |
64 | items must be a list of at most 64 entries |
| items in the whole page (containers count, tab/list entries and buttons do not) | 256 | the page is too large |
nesting depth (top-level items are level 1; row, column, card, page and a tab add a level) |
4 | the page is too large |
peer: and emoji: icons per page |
32 | a page may use at most 32 peer and emoji icons |
Common fields
Section titled “Common fields”Several item types share these optional fields.
| Field | Type | Applies to | Meaning |
|---|---|---|---|
hint |
text, up to 300 bytes, newlines allowed | toggle, button, input, slider, chat, select, page |
A small grey line under the item; empty or absent hides it. |
disabled |
bool, default false |
same as hint |
Greys the item out. |
icon |
text | link, button, toggle, chat, select, page, list entries |
An icon name, see Icons. |
alias |
id, up to 32 chars, unique | toggle, button, chat, select, page |
A stable name of the item on the settings page. The context menu of the item gets “Copy link”, the link tg://ayuplugin?id=<plugin id>&alias=<alias> opens the plugin’s settings scrolled to the item. |
long_click |
bool, default false |
button, page, list entries |
A right click (long press) on the item sends <scope>.long_click. With the value false nothing is sent. |
A paragraph. Selectable.
| Field | Type | Required | Limit / default |
|---|---|---|---|
text |
text | yes | up to 1000 bytes, newlines allowed |
style |
text | no | up to 16 bytes: title, subtitle, muted, accent, danger; absent means plain. Another value gives style must be title, subtitle, muted, accent or danger |
{"type": "text", "text": "Connected", "style": "accent"}header
Section titled “header”A section title.
| Field | Type | Required | Limit |
|---|---|---|---|
text |
text | yes | up to 120 bytes, one line |
A monospace block, selectable (for logs, JSON, commands).
| Field | Type | Required | Limit |
|---|---|---|---|
text |
text | yes | up to 1000 bytes, newlines allowed |
divider
Section titled “divider”A separator. Takes no fields besides type.
A row that opens a URL in the browser. The host name is shown on the right.
| Field | Type | Required | Limit / default |
|---|---|---|---|
label |
text | yes | up to 120 bytes |
url |
text | yes | up to 512 bytes, must start with https:// and be longer than 8 bytes; spaces and the characters " < > \ are not allowed. Nothing else is checked: the host is not parsed by the host |
icon |
text | no | default link |
Error: a link needs an https:// url.
progress
Section titled “progress”A label with a progress bar. Not interactive; update it by sending the page again (see Updating a page).
| Field | Type | Required | Limit / default |
|---|---|---|---|
label |
text | no | up to 120 bytes, default empty |
value |
int | no | -1 to 100, default 0. -1 means unknown (no percent is shown). A float is rejected. |
toggle
Section titled “toggle”A switch row.
| Field | Type | Required | Limit / default |
|---|---|---|---|
id |
id | yes | |
label |
text | yes | up to 120 bytes |
value |
bool | no | default false |
hint, disabled, icon, alias |
no | common fields |
{"type": "toggle", "id": "loud", "label": "Loud", "value": False, "hint": "Plays a sound"}button
Section titled “button”A row that acts as a button.
| Field | Type | Required | Limit / default |
|---|---|---|---|
id |
id | yes | |
label |
text | yes | up to 120 bytes |
style |
text | no | up to 16 bytes: default (default), primary, danger |
hint, disabled, icon, alias, long_click |
no | common fields |
Error for a bad style: style must be default, primary or danger.
A text field. The value is sent 600 ms after the last edit, when the field loses focus and when the page closes. While the field has focus or an edit is pending, a new page does not overwrite what the user typed.
| Field | Type | Required | Limit / default |
|---|---|---|---|
id |
id | yes | |
label |
text | yes | up to 120 bytes. Used as the placeholder if placeholder is empty; otherwise shown above the field. |
value |
text | no | at most max bytes; newlines only with multiline: true. Default empty. |
placeholder |
text | no | up to 120 bytes |
max |
int | no | 1 to 4096, default 1000 (the mask length if there is a mask) |
multiline |
bool | no | multi-line field, default false |
password |
bool | no | hides the characters, default false |
mask |
text | no | up to 64 bytes; see below |
hint, disabled |
no | common fields |
A mask describes the format: # is a digit, A is a Latin letter, * is any character, any other character stands for itself and is inserted automatically. A mask must contain at least one of # A * and cannot be combined with multiline or password (the keys must be absent, even set to false). The initial value must match the mask exactly (or be empty). Error: mask needs # (a digit), A (a letter) or *, and no multiline or password and the value does not fit the mask.
{"type": "input", "id": "phone", "label": "Phone", "mask": "+# (###) ###-##-##"}slider
Section titled “slider”An integer slider with a visible value.
| Field | Type | Required | Limit / default |
|---|---|---|---|
id |
id | yes | |
label |
text | yes | up to 120 bytes |
min |
int | no | -10^9 to 10^9, default 0 |
max |
int | no | -10^9 to 10^9, default 100 |
step |
int | no | 1 to 10^9, default 1 |
value |
int | no | between min and max, default min; rounded down to a multiple of step from min |
hint, disabled |
no | common fields |
At least two positions are required (max - min >= step) and at most 1000 steps ((max - min) / step <= 1000): a slider needs at least two positions and at most 1000 steps.
select
Section titled “select”A row that opens a list of options to choose from.
| Field | Type | Required | Limit / default |
|---|---|---|---|
id |
id | yes | |
label |
text | yes | up to 120 bytes |
options |
list | yes | 1 to 16 options |
value |
text | no | up to 64 bytes, one of the option values; default is the first option |
hint, disabled, icon, alias |
no | common fields |
An option is a map with exactly value (text, required, up to 64 bytes, unique in the list) and label (text, required, up to 120 bytes). Errors: a select needs 1 to 16 options, repeated option, the value is not one of the options.
{"type": "select", "id": "lang", "label": "Language", "value": "en", "options": [{"value": "en", "label": "English"}, {"value": "ru", "label": "Russian"}]}A row that opens the chat picker; shows the chosen chat’s name on the right.
| Field | Type | Required | Limit / default |
|---|---|---|---|
id |
id | yes | |
label |
text | yes | up to 120 bytes |
value |
int | no | chat id, 0 or more; 0 (default) means nothing chosen |
icon |
text | no | default chats |
hint, disabled, alias |
no | common fields |
The name of the chosen chat is shown only for chats the app has loaded, and never for hidden chats. A hidden chat cannot be picked.
page (sub-page)
Section titled “page (sub-page)”A row that opens a box with its own items. Events from items inside carry the same window field as the parent.
| Field | Type | Required | Limit / default |
|---|---|---|---|
id |
id | yes | |
label |
text | yes | up to 120 bytes; also the box title |
items |
list of items | yes | 1 to 64, counts as one nesting level |
hint, disabled, icon, alias, long_click |
no | common fields |
Error: a page needs items. Sub-pages can be nested up to the depth limit.
row and column
Section titled “row and column”| Field | Type | Required | Limit |
|---|---|---|---|
items |
list of items | yes | column: 1 to 64. row: 1 to 4; each item gets an equal-width cell. |
Error: a row holds 1 to 4 items, a column at least one. Containers take no id.
A framed group with an optional title.
| Field | Type | Required | Limit / default |
|---|---|---|---|
title |
text | no | up to 120 bytes, default empty |
items |
list of items | yes | 1 to 64 |
Error: a card needs items.
| Field | Type | Required | Limit / default |
|---|---|---|---|
id |
id | yes | |
tabs |
list | yes | 1 to 8 tabs |
value |
text | no | up to 64 bytes, id of the selected tab; default is the first tab |
A tab is a map with exactly id (id, required, unique on the page), label (text, required, up to 40 bytes) and items (list, required, up to 64 items, may be empty; one nesting level). Errors: tabs needs 1 to 8 tabs, the value is not one of the tabs.
{"type": "tabs", "id": "view", "tabs": [ {"id": "a", "label": "Main", "items": [{"type": "text", "text": "Hello"}]}, {"id": "b", "label": "More", "items": []},]}A picture. Not interactive.
| Field | Type | Required | Limit / default |
|---|---|---|---|
data |
bytes | yes | 1 byte to 256 KiB |
width |
int | no | 0 to 512, default 0 |
height |
int | no | 0 to 512, default 0 |
alt |
text | no | up to 120 bytes; shown as a tooltip |
The format is detected from the contents:
- PNG up to 2048 x 2048, or JPEG (the app also refuses images over 4 million pixels);
- a plain SVG: it must start with
<svg(an<?xml ?>prolog is allowed) and must not contain<script,<foreignobject,<image,<iframe,<embed,<audio,<video,<animate,<set,<!entity,<!doctype,javascript:,@import,<?xml-stylesheet,data:,file:, event attributes (onload=and so on), or anyhref/url(...)that does not point to a local#id; - a Lottie animation (JSON):
layersnot empty,wandh1 to 1024,fr(frame rate) 1 to 120, andip/op(first and last frame) all required, withopgreater thanipand at most 600 frames (op - ip); no assets that reference files (p,u,e).
width and height of 0 mean the natural size for PNG and JPEG (the image is scaled down to fit the width). They are required (non-zero) for SVG and Lottie. Errors: an image needs data of up to 256 KiB (missing, empty, not bytes or too large), an image must be a PNG up to 2048x2048, a JPEG, a plain SVG or a bounded Lottie animation, an SVG or Lottie image needs a width and a height.
A list of clickable rows.
| Field | Type | Required | Limit |
|---|---|---|---|
items |
list of entries | yes | 1 to 64 entries (a list needs 1 to 64 entries) |
An entry is a map with exactly these fields:
| Field | Type | Required | Limit / default |
|---|---|---|---|
id |
id | yes | |
title |
text | yes | up to 120 bytes |
subtitle |
text | no | up to 120 bytes |
icon |
text | no | icon |
long_click |
bool | no | default false |
A click on an entry sends <scope>.click with the entry’s id.
A read-only table.
| Field | Type | Required | Limit |
|---|---|---|---|
columns |
list of text | yes | 1 to 6 titles, each up to 40 bytes, one line |
rows |
list of lists of text | yes | 0 to 32 rows; each row has exactly as many cells as columns; each cell up to 120 bytes, one line |
Errors: a table needs 1 to 6 columns and at most 32 rows, every row needs a cell for each column, a column title is not valid text, a table cell is not valid text. Cells are text only; convert numbers with str().
Events sent back
Section titled “Events sent back”When the user touches an item, the plugin receives an event whose type depends on where the page is shown:
| Page | Change | Click | Long click | Other |
|---|---|---|---|---|
| settings page | settings.change |
settings.click |
settings.long_click |
settings.open (the page is being opened) |
| dialog | dialog.change |
dialog.click |
dialog.long_click |
dialog.close (the user closed it) |
| window | window.change |
window.click |
window.long_click |
window.close (the user closed it) |
Payload:
| Field | Present in | Meaning |
|---|---|---|
type |
all | the name from the table |
id |
*.change, *.click, *.long_click |
the id of the item, list entry or page button |
value |
*.change |
the new value: toggle bool, input text, slider int, select option value text, chat chat id (int), tabs tab id text |
window |
all window.* events (and window.close) |
the window id given to ui.window |
*.click has no value. The events settings.open, dialog.close and window.close have no id. dialog.close and window.close are sent when the user closes the dialog/window, when the app refuses to show it (no account, passcode lock) and never after your own close_* call. If the plugin is disabled or stops, its dialogs close silently.
In the Python SDK:
@freshgram.on_settings_opendef open_page(event): ... # settings.open
@freshgram.on_setting("enabled") # settings.change / settings.click for this iddef changed(event): print(event["type"], event.get("value"))
@freshgram.on_dialog("ok") # dialog.change / click / close for this iddef dialog_event(event): ...
@freshgram.on_window("board", "go") # window.* of window "board", item "go"def board(event): ...
@freshgram.on_long_click("row1", window="board") # *.long_click, any scopedef held(event): ...on_setting, on_dialog and on_window skip *.long_click events (use on_long_click); on_setting also skips settings.open; on_dialog and on_window without an item id also receive the *.close event. Subscribe to everything with @freshgram.on("settings.*"), "dialog.*", "window.*".
Events from the page count as a user action: for the next 10 seconds the plugin may open a dialog or window and call ui.open_settings (see limits).
Updating a page
Section titled “Updating a page”Send the page again with the same call. If the “structure” is unchanged, the app updates the existing widgets in place (texts, labels, values, disabled, hints, progress, table cells, list titles). The structure is: the types and ids of all items, icon, style, multiline, password, whether placeholder is set, slider min/max/step, the number of children of row/column/card and the card title, tab ids/labels/child counts, list entry ids/icons, table dimensions and the page buttons. If the structure changes, the page is rebuilt from scratch (pending input is sent first). For a settings page, send a new page from the settings.open handler and after any change of state. Windows use ui.window.update.
@freshgram.on_window("job", "stop")def stop(event): freshgram.ui.update_window("job", "Job", [ {"type": "progress", "label": "Stopped", "value": 100}, {"type": "button", "id": "stop", "label": "Stop", "disabled": True}, ])Dialogs and windows
Section titled “Dialogs and windows”- Dialog (
ui.dialog.show): one per plugin and account. Showing a dialog while it is open replaces its page. - Window (
ui.window.show/ui.window.update): identified byid(1 to 32 charactersa-z 0-9 _ - .), up to 3 open at once per plugin. mode(windows only):"layer"(default), a box over the chat, with the page buttons at the bottom; or"window", a separate panel with the buttons as rows at the end. An invalid value givesinvalid(mode). The mode is chosen when the window opens; later calls for an open window keep its mode.
Limits and rate limits
Section titled “Limits and rate limits”| Rule | Value | Error |
|---|---|---|
| open windows per plugin | 3 | limit: too many windows are open |
| updates of one window | not more often than every 100 ms | limit: windows are updated too often |
| opening a dialog or window | The first one is free. A further one within 60 s of the previous opening is allowed only if the user acted (any click/change event, a menu click, a command) in the last 10 s; for a dialog, a dialog that is still open also counts, for a window it does not. | limit: dialogs open after a user action, windows open after a user action |
ui.window.update of a window that is not open |
not_found: the window is not open |
|
ui.open_settings |
within 10 s after a user action; one action allows one call | denied: only right after a user action |
Without the permission the call fails with denied (the message is the permission name). When the app has no UI to show the page the call fails with unavailable.
An icon is one of three things:
- A built-in icon name (list below).
- A name of the plugin’s own icon from
contributes.iconsin the manifest. peer:<chat id>oremoji:<custom emoji id>: digits only, up to 20; the app draws a chat avatar or a custom emoji from its own data. At most 32 of these per page.peer:needs the permissionchat.infoorchats.list, works only for chats in the plugin’s scope, and is not drawn for hidden chats, the service chat or in streamer mode.emoji:is drawn only if the app can load that emoji.
A name is validated only by its shape: 1 to 32 characters of a-z 0-9 _ -. An unknown name is not an error: the app draws a generic bot icon instead. The shape error is icon must be a lowercase name, peer:<id> or emoji:<id>.
Built-in icons: add, archive, block, bot, cancel, chat, chats, clear, collapse, commands, copy, customize, delete, devices, download, edit, emoji, expand, export, file, folder, font, forward, gif, hide, hourglass, info, like, link, lock, mark_read, mute, network, night, notifications, palette, permissions, phone, photo, pin, premium, profile, qr, reactions, remove, reply, report, restore, schedule, search, select, send, settings, share, show, silent, sound_off, sound_on, spoiler, star, stats, stickers, storage, timer, translate, unmute, unpin, users, zoom_in, zoom_out. A plugin’s own icon with the same name as a built-in one is never used: the built-in wins.
Own icons are declared in the manifest and listed in files:
"contributes": { "icons": { "rocket": "icons/rocket.svg", "logo": "icons/logo.png" }, "menus": [ { "surface": "chat", "id": "launch", "title": "Launch", "icon": "rocket" } ]}At most 32 icons; names are 1 to 32 characters a-z 0-9 _ - (keys starting with x- are ignored); the path must be a safe relative path to a .png or .svg file of at most 64 KiB that is listed in files. Icons are drawn as a one-colour mask in the colour of the surrounding interface, so use a single-colour image with a transparent background. The same name is then valid as icon in menu items, page items and badges.
Menu items (contributes.menus)
Section titled “Menu items (contributes.menus)”"permissions": { "required": [ { "id": "ui.menu", "reason": "Adds a message action." } ] },"contributes": { "menus": [ { "surface": "message", "id": "copy-link", "title": "Copy link", "icon": "link", "when": { "chat_type": ["group", "supergroup"], "has_text": true } }] }Up to 32 entries. Needs the permission ui.menu (and ui.composer_button for the composer surface).
| Field | Type | Required | Limit |
|---|---|---|---|
surface |
text | yes | one of the surfaces below |
id |
text | yes | 1 to 64 characters of a-z 0-9 - (no underscore, no dot) |
title |
text or map of language to text | yes | 1 to 80 bytes |
icon |
text | no | a built-in icon or an own icon from contributes.icons; unknown names fail the manifest |
when |
map | no | conditions, see below |
Surfaces:
surface |
Where it appears | Event fields besides type, surface, id |
|---|---|---|
message |
context menu of a message | chat, topic, message, from, text |
chat |
context menu of a chat | chat, topic |
profile |
menu of a profile | chat, topic |
composer |
buttons next to the input field (at most 3 are shown, the rest go into an overflow menu) | chat, topic, reply_to; button: "right" on a right click |
selection |
menu of a selection of messages (up to 100) | chat, ids (message ids) |
shared_media |
menu of a selection in shared media (up to 100) | same as selection |
main_menu |
the main (side) menu | none |
The app also passes secret and self, but the host always removes them from these events. chat, topic and reply_to are removed when the chat is outside the scope the user granted, and message, from, text and ids are removed unless the plugin has messages.read; read such keys with event.get(...). See Event payloads.
| tray | the tray icon menu | none |
The event is menu.click. Handle it with @freshgram.on_menu("copy-link", surface="message"). The user can hide and reorder menu items in the settings; hidden items are not shown. The service chat, hidden chats and chats outside the plugin’s chat scope never show items. A message event is only sent for server messages. The menu.<surface> features can be required in the manifest for compatibility checks.
when conditions
Section titled “when conditions”All given conditions must hold (for selection and shared_media, for every selected message). Unknown keys fail the manifest. A message condition on a surface without messages fails with <key> does not apply to the <surface> surface; main_menu and tray take no conditions at all.
| Key | Type | Surfaces | Meaning |
|---|---|---|---|
chat_type |
list of up to 16 of user, bot, self, group, supergroup, forum, channel |
all except main_menu, tray |
the chat is of one of these types |
out |
bool | message, selection, shared_media |
the message is outgoing (true) or incoming (false) |
has_text |
bool | same | the message has text |
has_media |
bool | same | the message has media (a web page preview does not count) |
media |
list of up to 16 of photo, video, gif, round, voice, audio, file, sticker, poll, contact, location, webpage, other |
same | the media kind is one of these |
forwarded |
bool | same | the message is forwarded |
Toasts
Section titled “Toasts”freshgram.ui.toast("Saved")freshgram.ui.toast("Deleted", action="undo-1", undo=True, duration=6, title="Messages")freshgram.ui.toast("Open the report", action="open", label="Open")
@freshgram.on_toast("undo-1")def undo(event): ... # toast.click, event["id"] == "undo-1"
@freshgram.on_toast_close("undo-1")def gone(event): ... # toast.close, event["reason"] in ("action", "timeout")Permission ui.toast. Arguments of the operation ui.toast:
| Argument | Type | Limit / default |
|---|---|---|
text |
text | required, up to 200 bytes |
title |
text | up to 64 bytes, no newline; shown as the first line |
duration |
int | seconds, 1 to 8, default 5 |
style |
text | default or undo (Python: undo=True) |
action |
map | {"id": ..., "label": ...} (Python: action="id", label="..."): id 1 to 32 bytes; label up to 40 bytes, required unless style is undo (then “Undo” is shown) |
style: "undo" requires action. The plugin id is added in front of the text (or of the title). Not more than one toast per 2 seconds (limit: toasts are shown too often). Errors are invalid with the argument name (text, title, style, duration, action). Only toasts with an action send events: toast.click (id) when the button is pressed and toast.close (id, reason: action or timeout) when the toast goes away.
Badges
Section titled “Badges”Permission ui.badges.
freshgram.badges.set(user_id, [ {"id": "vip", "icon": "star", "tooltip": "VIP"}, {"id": "logo", "image": open("logo.png", "rb").read()},])freshgram.badges.set(user_id, []) # remove this user's badgesfreshgram.badges.clear() # remove all of the plugin's badgesbadges.set takes user (a positive id other than the service chat) and items, a list of up to 4 maps with exactly these keys:
| Field | Type | Required | Limit |
|---|---|---|---|
id |
id | yes | up to 32 characters, unique in the list |
icon |
text | one of icon / image |
a built-in or own icon name (peer: and emoji: are not allowed): a badge has a bad icon name |
image |
bytes | one of icon / image |
up to 64 KiB; PNG up to 128 x 128, or JPEG (a badge image is a PNG up to 128x128 or a JPEG) |
tooltip |
text | no | up to 80 bytes, one line |
Exactly one of icon and image (a badge has an icon or an image). Other badge errors: badges are a list of up to 4 entries, badges must be objects, a badge needs a unique id of up to 32 characters, a badge image is up to 64 KiB (empty, not bytes or too large). A plugin may have badges on up to 256 users (quota). A click arrives as badge.click with id, user, surface (message, chat, profile or chats) and, for a message, chat and message; handle it with @freshgram.on_badge_click("vip").
Validation errors
Section titled “Validation errors”All of these come back as invalid (freshgram.Invalid), with the text as the message.
| Message | Cause |
|---|---|
the page must be an object |
the page is not a map |
unknown field <name> |
an unknown key on the page or on an item, option, tab, entry or button |
<field> is required |
a required text field is missing |
<field> is not valid text |
wrong type, too long, control or bidirectional characters, or a newline where none is allowed |
<field> must be true or false |
a bool field has another type |
<field> is out of range |
an int field has another type, is a float, or is out of its range |
items must be a list of at most 64 entries |
items is missing, not a list or too long |
the page is too large |
more than 256 items or nesting deeper than 4 |
every item must be an object / an item has no type |
an item is not a map or has no type |
unknown item type <type> |
the type is not in the table; the shown type is cut to its first 20 bytes |
an item has a bad or repeated id |
see Identifiers |
alias must be a unique lowercase token of up to 32 characters |
bad or repeated alias |
icon must be a lowercase name, peer:<id> or emoji:<id> |
bad icon |
a page may use at most 32 peer and emoji icons |
too many rich icons |
style must be ... |
a style outside the allowed set |
buttons must be a list of at most 3 entries / bad button |
bad buttons |
closable must be true or false / a page that cannot be closed needs a button |
bad closable |
The remaining messages are given next to the item type. Wrong operation arguments give invalid with the argument name: page, id, mode.
Complete example
Section titled “Complete example”Taken from the style of the ui_demo test plugin:
import freshgram
@freshgram.on_initdef start(info): freshgram.ui.settings_page("Demo", [ {"type": "toggle", "id": "loud", "label": "Loud", "value": False}, {"type": "button", "id": "go", "label": "Open board", "icon": "settings"}, ])
@freshgram.on_setting("go")def go(event): freshgram.ui.window("board", "Board", [ {"type": "card", "title": "Status", "items": [ {"type": "row", "items": [ {"type": "text", "text": "Ready", "style": "accent"}, {"type": "button", "id": "run", "label": "Run", "style": "primary", "long_click": True}, ]}, {"type": "progress", "label": "Progress", "value": 40}, ]}, {"type": "tabs", "id": "view", "tabs": [ {"id": "log", "label": "Log", "items": [{"type": "code", "text": "line 1\nline 2"}]}, {"id": "tbl", "label": "Data", "items": [ {"type": "table", "columns": ["Name", "Count"], "rows": [["a", "1"], ["b", "2"]]}, ]}, ]}, ], buttons=[{"id": "close", "label": "Done"}], mode="window")
@freshgram.on_window("board", "run")def run(event): freshgram.ui.update_window("board", "Board", [ {"type": "progress", "label": "Done", "value": 100}, ])
@freshgram.on_long_click("run", window="board")def held(event): freshgram.ui.toast("Long press")Note that the update_window call above changes the structure, so the window is rebuilt; it also must come more than 100 ms after the previous update of that window.
