|
@@ -14,7 +14,7 @@ use crate::autoreply::{
|
|
|
};
|
|
};
|
|
|
use crate::config::Config;
|
|
use crate::config::Config;
|
|
|
use crate::history::{ChatSummary, Contact, History, StoredMessage};
|
|
use crate::history::{ChatSummary, Contact, History, StoredMessage};
|
|
|
-use crate::web::{WebChat, WebCommand, WebRequest, WebState};
|
|
|
|
|
|
|
+use crate::web::{self, WebChat, WebCommand, WebRequest, WebState};
|
|
|
use crate::whatsapp::{ImageRef, WaEvent, WaHandle};
|
|
use crate::whatsapp::{ImageRef, WaEvent, WaHandle};
|
|
|
|
|
|
|
|
/// Maximum number of messages of the open chat kept in `App::messages`.
|
|
/// Maximum number of messages of the open chat kept in `App::messages`.
|
|
@@ -22,10 +22,12 @@ pub const MESSAGES_SHOWN: usize = 200;
|
|
|
|
|
|
|
|
/// Key code of the function key that toggles the list mode.
|
|
/// Key code of the function key that toggles the list mode.
|
|
|
const LIST_MODE_FUNCTION_KEY: u8 = 2;
|
|
const LIST_MODE_FUNCTION_KEY: u8 = 2;
|
|
|
-/// Key code of the function key (F3) that toggles the autoreply of the open chat.
|
|
|
|
|
|
|
+/// Key code of the function key (F3) that toggles the autoreply of the focused chat.
|
|
|
const AUTOREPLY_FUNCTION_KEY: u8 = 3;
|
|
const AUTOREPLY_FUNCTION_KEY: u8 = 3;
|
|
|
/// Key code of the function key (F4) that toggles the global autoreply.
|
|
/// Key code of the function key (F4) that toggles the global autoreply.
|
|
|
const GLOBAL_AUTOREPLY_FUNCTION_KEY: u8 = 4;
|
|
const GLOBAL_AUTOREPLY_FUNCTION_KEY: u8 = 4;
|
|
|
|
|
+/// Key code of the function key (F8) that deletes the WhatsApp session and pairs again.
|
|
|
|
|
+const RELINK_FUNCTION_KEY: u8 = 8;
|
|
|
/// Key code of the function key (F10) that quits the application.
|
|
/// Key code of the function key (F10) that quits the application.
|
|
|
const QUIT_FUNCTION_KEY: u8 = 10;
|
|
const QUIT_FUNCTION_KEY: u8 = 10;
|
|
|
/// Rows the list selection moves with PageUp/PageDown.
|
|
/// Rows the list selection moves with PageUp/PageDown.
|
|
@@ -84,6 +86,20 @@ const STATUS_DELETE_CONFIRM_HINT: &str = "Canc conferma, Esc annulla";
|
|
|
const STATUS_CHAT_DELETED_PREFIX: &str = "Chat";
|
|
const STATUS_CHAT_DELETED_PREFIX: &str = "Chat";
|
|
|
/// Suffix of the status shown once a chat was deleted locally.
|
|
/// Suffix of the status shown once a chat was deleted locally.
|
|
|
const STATUS_CHAT_DELETED_SUFFIX: &str = "eliminata";
|
|
const STATUS_CHAT_DELETED_SUFFIX: &str = "eliminata";
|
|
|
|
|
+/// Status asking to confirm the deletion of the WhatsApp session (F8).
|
|
|
|
|
+const STATUS_RELINK_CONFIRM: &str = "Cancellare la sessione WhatsApp e rifare l'abbinamento? F8 conferma, Esc annulla";
|
|
|
|
|
+/// Status shown when a pending relink is cancelled by another key.
|
|
|
|
|
+const STATUS_RELINK_CANCELLED: &str = "Nuovo abbinamento annullato";
|
|
|
|
|
+/// Status shown while the old session is logged out and deleted.
|
|
|
|
|
+const STATUS_RELINKING: &str = "Cancellazione della sessione in corso...";
|
|
|
|
|
+/// Status shown once the new session is started and its QR code is coming.
|
|
|
|
|
+const STATUS_RELINK_READY: &str = "Sessione cancellata: scansiona il nuovo QR";
|
|
|
|
|
+/// Prefix of the status shown when the session cannot be deleted or the new one started.
|
|
|
|
|
+const STATUS_RELINK_FAILED_PREFIX: &str = "Nuovo abbinamento fallito";
|
|
|
|
|
+/// Status shown when the configuration files changed and were reloaded.
|
|
|
|
|
+const STATUS_CONFIG_RELOADED: &str = "Configurazione ricaricata";
|
|
|
|
|
+/// Prefix of the status shown when the changed configuration cannot be loaded.
|
|
|
|
|
+const STATUS_CONFIG_RELOAD_FAILED_PREFIX: &str = "Configurazione non ricaricata";
|
|
|
/// Status shown when the web control panel toggles a chat that is not in its list.
|
|
/// Status shown when the web control panel toggles a chat that is not in its list.
|
|
|
const STATUS_WEB_CHAT_UNKNOWN: &str = "Chat non trovata";
|
|
const STATUS_WEB_CHAT_UNKNOWN: &str = "Chat non trovata";
|
|
|
|
|
|
|
@@ -118,6 +134,10 @@ pub enum AppEvent {
|
|
|
},
|
|
},
|
|
|
/// Command from the web control panel; the reply carries the state snapshot.
|
|
/// Command from the web control panel; the reply carries the state snapshot.
|
|
|
Web(WebRequest),
|
|
Web(WebRequest),
|
|
|
|
|
+ /// A configuration file changed: the configuration reloaded from disk, or the load error.
|
|
|
|
|
+ ConfigReloaded(Result<Box<Config>, String>),
|
|
|
|
|
+ /// Outcome of a relink: the new unpaired session is started, or the error.
|
|
|
|
|
+ Relinked(Result<(), String>),
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// The chat whose images are being described, and what to do once they are.
|
|
/// The chat whose images are being described, and what to do once they are.
|
|
@@ -210,6 +230,10 @@ pub struct App {
|
|
|
catch_up_done: bool,
|
|
catch_up_done: bool,
|
|
|
/// Jid of the chat awaiting the second Delete press that confirms its local deletion.
|
|
/// Jid of the chat awaiting the second Delete press that confirms its local deletion.
|
|
|
pending_delete: Option<String>,
|
|
pending_delete: Option<String>,
|
|
|
|
|
+ /// Whether a first F8 press awaits the second one that confirms the relink.
|
|
|
|
|
+ pending_relink: bool,
|
|
|
|
|
+ /// Whether a relink is running (further requests are ignored until it ends).
|
|
|
|
|
+ relinking: bool,
|
|
|
wa: WaHandle,
|
|
wa: WaHandle,
|
|
|
events: UnboundedSender<AppEvent>,
|
|
events: UnboundedSender<AppEvent>,
|
|
|
}
|
|
}
|
|
@@ -254,6 +278,8 @@ impl App {
|
|
|
own_jids_known: false,
|
|
own_jids_known: false,
|
|
|
catch_up_done: false,
|
|
catch_up_done: false,
|
|
|
pending_delete: None,
|
|
pending_delete: None,
|
|
|
|
|
+ pending_relink: false,
|
|
|
|
|
+ relinking: false,
|
|
|
wa,
|
|
wa,
|
|
|
events,
|
|
events,
|
|
|
})
|
|
})
|
|
@@ -279,7 +305,98 @@ impl App {
|
|
|
self.handle_sent(OutgoingMessage { chat_jid, text, is_ai }, result)
|
|
self.handle_sent(OutgoingMessage { chat_jid, text, is_ai }, result)
|
|
|
}
|
|
}
|
|
|
AppEvent::Web(request) => self.handle_web_request(request),
|
|
AppEvent::Web(request) => self.handle_web_request(request),
|
|
|
|
|
+ AppEvent::ConfigReloaded(result) => {
|
|
|
|
|
+ self.apply_reloaded_config(result);
|
|
|
|
|
+ Ok(())
|
|
|
|
|
+ }
|
|
|
|
|
+ AppEvent::Relinked(result) => {
|
|
|
|
|
+ self.finish_relink(result);
|
|
|
|
|
+ Ok(())
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// Purpose: delete the WhatsApp session and start an unpaired one, which offers a new QR
|
|
|
|
|
+ /// code (F8 confirmed, or the web panel).
|
|
|
|
|
+ /// Inputs: none.
|
|
|
|
|
+ /// Output: none.
|
|
|
|
|
+ /// Side effects: ignored while a relink is running; otherwise shows the connecting screen,
|
|
|
|
|
+ /// forgets the own JIDs (the new pairing may be another account) and spawns the relink task,
|
|
|
|
|
+ /// which sends `AppEvent::Relinked`. Messages sent meanwhile fail with a send error.
|
|
|
|
|
+ fn start_relink(&mut self) {
|
|
|
|
|
+ self.pending_relink = false;
|
|
|
|
|
+ if self.relinking {
|
|
|
|
|
+ return;
|
|
|
}
|
|
}
|
|
|
|
|
+ self.relinking = true;
|
|
|
|
|
+ self.screen = Screen::Connecting;
|
|
|
|
|
+ self.qr_code = None;
|
|
|
|
|
+ self.status = STATUS_RELINKING.to_owned();
|
|
|
|
|
+ self.own_jids_known = false;
|
|
|
|
|
+ let (wa, events) = (self.wa.clone(), self.events.clone());
|
|
|
|
|
+ tokio::spawn(async move {
|
|
|
|
|
+ let result = wa.relink().await.map_err(|err| format!("{err:#}"));
|
|
|
|
|
+ send_event(&events, AppEvent::Relinked(result));
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// Purpose: report the outcome of a relink.
|
|
|
|
|
+ /// Inputs: `result`, `Ok` once the new session is started, otherwise the error text.
|
|
|
|
|
+ /// Output: none.
|
|
|
|
|
+ /// Side effects: allows new relinks and updates `status`; the screen follows the events of
|
|
|
|
|
+ /// the new session (QR codes, then connection).
|
|
|
|
|
+ fn finish_relink(&mut self, result: Result<(), String>) {
|
|
|
|
|
+ self.relinking = false;
|
|
|
|
|
+ self.status = match result {
|
|
|
|
|
+ Ok(()) => STATUS_RELINK_READY.to_owned(),
|
|
|
|
|
+ Err(err) => format!("{STATUS_RELINK_FAILED_PREFIX}: {err}"),
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// Purpose: handle F8: ask for confirmation, or start the relink on the second press.
|
|
|
|
|
+ /// Inputs: none.
|
|
|
|
|
+ /// Output: none.
|
|
|
|
|
+ /// Side effects: cancels a pending chat deletion; sets `pending_relink` and `status`, or
|
|
|
|
|
+ /// starts the relink (see `start_relink`).
|
|
|
|
|
+ fn request_relink(&mut self) {
|
|
|
|
|
+ self.cancel_pending_delete();
|
|
|
|
|
+ if self.pending_relink {
|
|
|
|
|
+ self.start_relink();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ self.pending_relink = true;
|
|
|
|
|
+ self.status = STATUS_RELINK_CONFIRM.to_owned();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// Purpose: drop a pending relink confirmation, if any.
|
|
|
|
|
+ /// Inputs: none.
|
|
|
|
|
+ /// Output: `true` if a relink was awaiting confirmation.
|
|
|
|
|
+ /// Side effects: clears `pending_relink` and, when it was set, shows the cancellation status.
|
|
|
|
|
+ fn cancel_pending_relink(&mut self) -> bool {
|
|
|
|
|
+ let was_pending = std::mem::take(&mut self.pending_relink);
|
|
|
|
|
+ if was_pending {
|
|
|
|
|
+ self.status = STATUS_RELINK_CANCELLED.to_owned();
|
|
|
|
|
+ }
|
|
|
|
|
+ was_pending
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// Purpose: apply a configuration reloaded after a change of its files.
|
|
|
|
|
+ /// Inputs: `result`, the new configuration or the load error.
|
|
|
|
|
+ /// Output: none.
|
|
|
|
|
+ /// Side effects: on success replaces `config` and rebuilds the AI client (replies already
|
|
|
|
|
+ /// running keep the previous one); updates `status`. On error the current configuration stays.
|
|
|
|
|
+ fn apply_reloaded_config(&mut self, result: Result<Box<Config>, String>) {
|
|
|
|
|
+ let config = match result {
|
|
|
|
|
+ Ok(config) => *config,
|
|
|
|
|
+ Err(err) => {
|
|
|
|
|
+ self.status = format!("{STATUS_CONFIG_RELOAD_FAILED_PREFIX}: {err}");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+ let (ai, status) = build_ai(&config);
|
|
|
|
|
+ self.config = config;
|
|
|
|
|
+ self.ai = ai;
|
|
|
|
|
+ self.status = if status.is_empty() { STATUS_CONFIG_RELOADED.to_owned() } else { status };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// Purpose: route a WhatsApp layer event.
|
|
/// Purpose: route a WhatsApp layer event.
|
|
@@ -767,8 +884,9 @@ impl App {
|
|
|
/// Purpose: handle a key press on any screen.
|
|
/// Purpose: handle a key press on any screen.
|
|
|
/// Inputs: `key`, the pressed key.
|
|
/// Inputs: `key`, the pressed key.
|
|
|
/// Output: `Ok(())`, or a history error.
|
|
/// Output: `Ok(())`, or a history error.
|
|
|
- /// Side effects: F10 quits on any screen; on the main screen F2 toggles the list mode, F3 the
|
|
|
|
|
- /// autoreply of the open chat and F4 the global autoreply (any focus); Ctrl command chords are
|
|
|
|
|
|
|
+ /// Side effects: F10 quits and F8 asks/confirms a relink on any screen; any other key cancels a
|
|
|
|
|
+ /// pending relink (Esc is then consumed); on the main screen F2 toggles the list mode, F3 the
|
|
|
|
|
+ /// autoreply of the focused chat and F4 the global autoreply (any focus); Ctrl command chords are
|
|
|
/// ignored so they never type into the input or the filter; otherwise may change
|
|
/// ignored so they never type into the input or the filter; otherwise may change
|
|
|
/// focus/selection, edit the input, send or open chats.
|
|
/// focus/selection, edit the input, send or open chats.
|
|
|
fn handle_key(&mut self, key: KeyEvent) -> Result<()> {
|
|
fn handle_key(&mut self, key: KeyEvent) -> Result<()> {
|
|
@@ -776,6 +894,13 @@ impl App {
|
|
|
self.should_quit = true;
|
|
self.should_quit = true;
|
|
|
return Ok(());
|
|
return Ok(());
|
|
|
}
|
|
}
|
|
|
|
|
+ if key.code == KeyCode::F(RELINK_FUNCTION_KEY) {
|
|
|
|
|
+ self.request_relink();
|
|
|
|
|
+ return Ok(());
|
|
|
|
|
+ }
|
|
|
|
|
+ if self.cancel_pending_relink() && key.code == KeyCode::Esc {
|
|
|
|
|
+ return Ok(());
|
|
|
|
|
+ }
|
|
|
if self.screen != Screen::Main {
|
|
if self.screen != Screen::Main {
|
|
|
return Ok(());
|
|
return Ok(());
|
|
|
}
|
|
}
|
|
@@ -973,13 +1098,18 @@ impl App {
|
|
|
self.selected = 0;
|
|
self.selected = 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /// Purpose: toggle the AI autoreply of the open chat (F3).
|
|
|
|
|
|
|
+ /// Purpose: toggle the AI autoreply of the focused chat (F3): the selected row when the list
|
|
|
|
|
+ /// has the focus, the open chat when the input has it.
|
|
|
/// Inputs: none.
|
|
/// Inputs: none.
|
|
|
/// Output: `Ok(())`, or a history error.
|
|
/// Output: `Ok(())`, or a history error.
|
|
|
/// Side effects: writes the history, updates `status`, reloads chats; refuses to enable
|
|
/// Side effects: writes the history, updates `status`, reloads chats; refuses to enable
|
|
|
/// without an AI client or outside private chats and groups (disabling is always allowed).
|
|
/// without an AI client or outside private chats and groups (disabling is always allowed).
|
|
|
fn toggle_autoreply(&mut self) -> Result<()> {
|
|
fn toggle_autoreply(&mut self) -> Result<()> {
|
|
|
- let Some(jid) = self.open_chat.clone() else {
|
|
|
|
|
|
|
+ let target = match self.focus {
|
|
|
|
|
+ Focus::List => self.selected_jid(),
|
|
|
|
|
+ Focus::Input => self.open_chat.clone(),
|
|
|
|
|
+ };
|
|
|
|
|
+ let Some(jid) = target else {
|
|
|
return Ok(());
|
|
return Ok(());
|
|
|
};
|
|
};
|
|
|
self.toggle_chat_autoreply(&jid)
|
|
self.toggle_chat_autoreply(&jid)
|
|
@@ -1010,12 +1140,13 @@ impl App {
|
|
|
/// Inputs: `request`, the command and the channel on which the state snapshot is sent back.
|
|
/// Inputs: `request`, the command and the channel on which the state snapshot is sent back.
|
|
|
/// Output: `Ok(())`, or a history error.
|
|
/// Output: `Ok(())`, or a history error.
|
|
|
/// Side effects: may toggle the global or a per-chat autoreply (writing the history), update
|
|
/// Side effects: may toggle the global or a per-chat autoreply (writing the history), update
|
|
|
- /// `status` or request the application to quit; always sends the state snapshot.
|
|
|
|
|
|
|
+ /// `status`, start a relink or request the application to quit; always sends the state snapshot.
|
|
|
fn handle_web_request(&mut self, request: WebRequest) -> Result<()> {
|
|
fn handle_web_request(&mut self, request: WebRequest) -> Result<()> {
|
|
|
match request.command {
|
|
match request.command {
|
|
|
WebCommand::State => {}
|
|
WebCommand::State => {}
|
|
|
WebCommand::ToggleGlobal => self.toggle_global_autoreply()?,
|
|
WebCommand::ToggleGlobal => self.toggle_global_autoreply()?,
|
|
|
WebCommand::ToggleChat(jid) => self.toggle_web_chat(&jid)?,
|
|
WebCommand::ToggleChat(jid) => self.toggle_web_chat(&jid)?,
|
|
|
|
|
+ WebCommand::Relink => self.start_relink(),
|
|
|
WebCommand::Quit => self.should_quit = true,
|
|
WebCommand::Quit => self.should_quit = true,
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1039,13 +1170,16 @@ impl App {
|
|
|
|
|
|
|
|
/// Purpose: snapshot of the state shown by the web control panel.
|
|
/// Purpose: snapshot of the state shown by the web control panel.
|
|
|
/// Inputs: none.
|
|
/// Inputs: none.
|
|
|
- /// Output: the global flag, the toggleable chats and the status line.
|
|
|
|
|
|
|
+ /// Output: the global flag, the toggleable chats, the status line, the current screen and,
|
|
|
|
|
+ /// while pairing, the QR code as SVG.
|
|
|
/// Side effects: none.
|
|
/// Side effects: none.
|
|
|
fn web_state(&self) -> WebState {
|
|
fn web_state(&self) -> WebState {
|
|
|
WebState {
|
|
WebState {
|
|
|
global_autoreply: self.global_autoreply,
|
|
global_autoreply: self.global_autoreply,
|
|
|
chats: web_chats(&self.chats),
|
|
chats: web_chats(&self.chats),
|
|
|
status: self.status.clone(),
|
|
status: self.status.clone(),
|
|
|
|
|
+ screen: web_screen(self.screen),
|
|
|
|
|
+ qr_svg: self.qr_code.as_deref().and_then(web::qr_svg),
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1073,13 +1207,7 @@ impl App {
|
|
|
/// Output: `Ok(())`, or a history error.
|
|
/// Output: `Ok(())`, or a history error.
|
|
|
/// Side effects: loads name, autoreply flag and messages; moves the focus to the input.
|
|
/// Side effects: loads name, autoreply flag and messages; moves the focus to the input.
|
|
|
fn open_selected(&mut self) -> Result<()> {
|
|
fn open_selected(&mut self) -> Result<()> {
|
|
|
- let jid = match self.list_mode {
|
|
|
|
|
- ListMode::Chats => self.visible_chats().get(self.selected).map(|chat| chat.jid.clone()),
|
|
|
|
|
- ListMode::Contacts => {
|
|
|
|
|
- self.visible_contacts().get(self.selected).map(|contact| contact.jid.clone())
|
|
|
|
|
- }
|
|
|
|
|
- };
|
|
|
|
|
- let Some(jid) = jid else {
|
|
|
|
|
|
|
+ let Some(jid) = self.selected_jid() else {
|
|
|
return Ok(());
|
|
return Ok(());
|
|
|
};
|
|
};
|
|
|
self.open_chat_name = self.history.display_name(&jid)?;
|
|
self.open_chat_name = self.history.display_name(&jid)?;
|
|
@@ -1090,6 +1218,19 @@ impl App {
|
|
|
Ok(())
|
|
Ok(())
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /// Purpose: identify the chat or contact on the selected row of the active list.
|
|
|
|
|
+ /// Inputs: none.
|
|
|
|
|
+ /// Output: its JID, or `None` when the visible list is empty.
|
|
|
|
|
+ /// Side effects: none.
|
|
|
|
|
+ fn selected_jid(&self) -> Option<String> {
|
|
|
|
|
+ match self.list_mode {
|
|
|
|
|
+ ListMode::Chats => self.visible_chats().get(self.selected).map(|chat| chat.jid.clone()),
|
|
|
|
|
+ ListMode::Contacts => {
|
|
|
|
|
+ self.visible_contacts().get(self.selected).map(|contact| contact.jid.clone())
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/// Purpose: number of rows of the active list.
|
|
/// Purpose: number of rows of the active list.
|
|
|
/// Inputs: none.
|
|
/// Inputs: none.
|
|
|
/// Output: length of the visible (filtered) chat or contact list according to `list_mode`.
|
|
/// Output: length of the visible (filtered) chat or contact list according to `list_mode`.
|
|
@@ -1415,6 +1556,18 @@ fn can_enable_autoreply(jid: &str, ai_configured: bool) -> Result<(), &'static s
|
|
|
Ok(())
|
|
Ok(())
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/// Purpose: name the current screen for the web control panel.
|
|
|
|
|
+/// Inputs: `screen`, the screen shown by the TUI.
|
|
|
|
|
+/// Output: `web::SCREEN_CONNECTING`, `web::SCREEN_PAIRING` or `web::SCREEN_MAIN`.
|
|
|
|
|
+/// Side effects: none.
|
|
|
|
|
+fn web_screen(screen: Screen) -> &'static str {
|
|
|
|
|
+ match screen {
|
|
|
|
|
+ Screen::Connecting => web::SCREEN_CONNECTING,
|
|
|
|
|
+ Screen::Pairing => web::SCREEN_PAIRING,
|
|
|
|
|
+ Screen::Main => web::SCREEN_MAIN,
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/// Purpose: build the chat list exposed by the web control panel.
|
|
/// Purpose: build the chat list exposed by the web control panel.
|
|
|
/// Inputs: `chats`, the chats shown by the TUI.
|
|
/// Inputs: `chats`, the chats shown by the TUI.
|
|
|
/// Output: the chats whose autoreply can be toggled (private chats and groups), in the same order.
|
|
/// Output: the chats whose autoreply can be toggled (private chats and groups), in the same order.
|