Interactive Screens & Zero-Flicker Transitions β
While standard Flow classes handle linear textual forms, InteractiveFlow is built for rich, menu-driven bots, dashboards, e-commerce product catalogs, and interactive control panels.
β‘ Zero-Flicker In-Place Editing β
Unlike other frameworks that repeatedly delete and resend messagesβcausing visible UI flickering and consuming redundant Telegram API callsβInteractiveFlow automatically tracks the active message ID ($this->messageId) and updates messages in-place using editMessageText and editMessageReplyMarkup.
Automatic Fallback Recovery β
If in-place editing is not possible (e.g. the message was deleted by the user, the 48-hour Telegram edit window elapsed, or the screen switched from an inline keyboard to a native reply keyboard), InteractiveFlow gracefully falls back to sending a new message with sendMessage, automatically updating $this->messageId.
π₯οΈ The Screen Builder β
The Screen object defines the content, formatting, keyboard markup, and navigation options for the active step:
use Tueen\Telegram\Enums\ParseMode;
use Tueen\Telegram\Flow\Screen;
use Tueen\Telegram\Keyboards\InlineKeyboard;
public function render(): Screen
{
return Screen::make("π¦ *Product Catalog*\n\nChoose an item below:")
->parseMode(ParseMode::MARKDOWN_V2)
->inline(fn (InlineKeyboard $k) => $k
->row()
->action('Item 1 ($10)', 'item_1')
->action('Item 2 ($20)', 'item_2')
->row()
->url('Official Store', 'https://example.com')
)
->withNavigation(back: true, home: true);
}Screen Builder Methods β
text(string $text): staticparseMode(ParseMode|string $mode): staticinline(callable|InlineKeyboard $keyboard): staticreply(callable|ReplyKeyboard $keyboard): staticremoveKeyboard(bool $selective = false): staticwithNavigation(bool $back = true, bool $home = true): staticeditIfPossible(bool $edit = true): staticπ Refreshing Screens In-Place β
To re-render the screen when state changes (e.g., toggling a switch or updating a counter), call $this->refresh():
#[Action('toggle_notify')]
public function toggleNotify(): void
{
$current = $this->get('notify', true);
$this->set('notify', !$current);
// Immediately re-renders the screen in-place with updated state
$this->refresh();
}π Native Telegram Alerts & Toasts β
Instead of updating the screen text, you can display native Telegram popups in response to button clicks:
#[Action('like')]
public function handleLike(): void
{
$this->likes++;
// Shows a subtle toast notification at the bottom of the user's screen:
$this->toast('β€οΈ Added to favorites!');
// Or show a modal dialog box with an OK button:
// $this->alert('β οΈ Warning: Action requires confirmation.');
$this->refresh();
}π Sticky Screen / Pin to Bottom (resendAtBottom) β
When an interactive flow accepts free-form text input from users, incoming messages push the interactive menu upward in the chat history.
Use $this->resendAtBottom() to delete the old message and send a fresh screen at the very bottom of the chat:
#[Input]
public function handleSearchQuery(Update $update): void
{
$query = trim($update->findAnyText() ?? '');
$this->set('searchQuery', $query);
// Deletes the older menu message and posts a new one at the bottom:
$this->resendAtBottom(deleteOld: true);
}πΌοΈ Media Screens (Photos, Videos, Animations) β
Screen supports rich media with automatic caption editing:
public function render(): Screen
{
return Screen::make()
->photo('https://example.com/banner.jpg', caption: 'β¨ Special Offer: 50% Off!')
->inline(fn (InlineKeyboard $k) => $k->action('Claim Coupon', 'claim'))
->withNavigation(back: true, home: true);
}β οΈ Confirmation Dialogs β
For destructive or critical operations, invoke $this->confirm(...) to display an in-place confirmation prompt:
#[Action('delete_item')]
public function confirmDeletion(): void
{
$this->confirm(
prompt: 'Are you sure you want to delete this item?',
onConfirmedAction: 'performDeletion',
confirmLabel: 'β
Yes, Delete',
cancelLabel: 'β Cancel'
);
}
#[Action('performDeletion')]
public function performDeletion(): void
{
$this->toast('ποΈ Item deleted successfully.');
$this->pop();
}βοΈ Multi-Select Checklist Grid (withChecklist) β
Interactive flows often need multi-select toggles (e.g. notifications settings, category filters, or item selection). Screen::withChecklist() builds interactive toggle button grids with automatic checkmark icons:
public function render(): Screen
{
$selected = $this->get('selected_interests', ['news', 'tech']);
$options = [
'news' => 'World News',
'tech' => 'Technology',
'crypto' => 'Cryptocurrency',
'sports' => 'Sports & Fitness',
];
return Screen::make("Choose your favorite topics:")
->withChecklist(
options: $options,
selectedKeys: $selected,
actionPattern: 'toggle_interest_{key}',
columns: 2,
checkedIcon: 'β
',
uncheckedIcon: 'β¬ '
)
->inline(fn (InlineKeyboard $k) => $k->row()->action('Continue β‘οΈ', 'proceed'))
->withNavigation(back: true, home: true);
}Toggling Checklist Items β
Use $this->toggleChecklist() to add or remove an item key from the state array:
#[Action('toggle_interest_{key}')]
public function toggleInterest(string $key): void
{
$this->toggleChecklist('selected_interests', $key);
$this->refresh();
}πͺ Step Wizard & Progress Stepper (withStepper) β
For multi-step onboarding, registration, or checkout forms, Screen::withStepper() injects a visual progress bar directly into the screen header:
public function render(): Screen
{
return Screen::make("Please enter your shipping address:")
->withStepper(currentStep: 2, totalSteps: 4, style: 'blocks')
->inline(...)
->withNavigation(back: true, home: false);
}This prefixes your screen text with:
[Step 2 of 4] π©π©β¬β¬
Please enter your shipping address:Supported Stepper Styles β
| Style | Pattern | Visual Output (Step 2 of 4) |
|---|---|---|
'blocks' | π© / β¬ | [Step 2 of 4] π©π©β¬β¬ |
'dots' | β / β | [Step 2 of 4] ββββ |
'circles' | π’ / βͺ | [Step 2 of 4] π’π’βͺβͺ |
'numbers' | (X/Y) | [Step 2 of 4] |
πΊ Single-Screen Support & replaceFlow() β
In interactive Telegram bots, users frequently type commands like /start, /tasks, or /settings while already viewing an active screen. In traditional bots, spinning up a new flow leaves redundant menu messages behind in the chat history, cluttering the conversation and breaking the single-screen paradigm.
Tueen provides first-class architectural support for Single-Screen flows and a dedicated $bot->replaceFlow() API to cleanly terminate previous screens.
The #[SingleScreen] Attribute β
Decorate an InteractiveFlow with #[SingleScreen] to automatically delete the previous active screen message whenever the flow starts:
use Tueen\Telegram\Flow\Attributes\SingleScreen;
use Tueen\Telegram\Flow\InteractiveFlow;
use Tueen\Telegram\Flow\Screen;
#[SingleScreen]
class DashboardFlow extends InteractiveFlow
{
public function render(): Screen
{
return Screen::make("π *Main Dashboard*")
->inline(...);
}
}Whenever $bot->startFlow(DashboardFlow::class) is invoked, Tueen inspects the current session. If a previous screen message was active, it is safely deleted with deleteMessage(chatId, messageId) before rendering the new screen. Any deletion failures (such as the message already being removed by the user or the 48-hour window expiring) are silently suppressed.
You can also control this behavior via the protected property or the attribute argument:
// Via attribute argument
#[SingleScreen(clearPrevious: false)]
class ModalFlow extends InteractiveFlow { ... }
// Or via protected property
class CustomDashboardFlow extends InteractiveFlow
{
protected bool $clearPreviousScreen = true;
}Dedicated $bot->replaceFlow() API β
In command handlers (e.g. #[OnCommand('start')]), use $bot->replaceFlow() to terminate the existing flow session, delete its active screen, and start a fresh flow in a single atomic call:
use Tueen\Telegram\Routing\Attributes\OnCommand;
use Tueen\Telegram\Telegram;
#[OnCommand('start')]
public function handleStart(Telegram $bot): void
{
// Safely deletes the old screen and launches DashboardFlow
$bot->replaceFlow(DashboardFlow::class);
}If you wish to retain the previous screen message in chat history while resetting the session state, pass deletePreviousScreen: false:
$bot->replaceFlow(DashboardFlow::class, deletePreviousScreen: false);Overriding at Runtime via startFlow() β
You can also explicitly override screen clearing at call-time by passing clearPreviousScreen:
// Force-clear previous screen even if the flow is not decorated with #[SingleScreen]
$bot->startFlow(SettingsFlow::class, clearPreviousScreen: true);
// Preserve previous screen even if the flow has #[SingleScreen]
$bot->startFlow(DashboardFlow::class, clearPreviousScreen: false);