Files
nixos/patches/flare/0001-feat-typing-Implement-typing-indicators.patch
Simon Gardling bcdfd8cdf5 flare: add patched flare-signal with five local feature patches
- patches/flare/000{1..5}-*.patch: typing indicators, formatted
  messages, edited messages, multi-select with delete-for-me, and
  in-channel message search. Mirror the matching commits in
  ~/projects/forks/flare and apply cleanly on top of upstream 0.20.4
  (which is what nixpkgs ships).
- home/profiles/gui.nix: include a flare-signal override that appends
  the patches via overrideAttrs. None of them touch Cargo.lock so the
  cargoDeps hash stays valid; signal-desktop stays alongside it.
2026-04-30 18:41:35 -04:00

733 lines
28 KiB
Diff

From 733ad6e63fa6408e47d87a22cf51a784f5ce103f Mon Sep 17 00:00:00 2001
From: Simon Gardling <titaniumtown@proton.me>
Date: Wed, 29 Apr 2026 19:00:12 -0400
Subject: [PATCH 1/6] feat(typing): Implement typing indicators
- Send TypingMessage Started/Stopped events as the user composes a
message, including a periodic refresh and an idle-stop timer so the
indicator follows actual composition activity.
- Display a typing indicator strip above the message input, gated on
the active channel's is-typing state.
- Add the show-typing-indicators and send-typing-indicators settings,
exposed through a new preferences group, and honour them both for
display and outbound events.
- Generalise Channel-level send_message_to_group to accept any
ContentBody so the new TypingMessage path can reuse it.
---
CHANGELOG.md | 5 +
data/de.schmidhuberj.Flare.gschema.xml | 9 +
data/resources/style.css | 6 +
data/resources/ui/channel_messages.blp | 33 +++
data/resources/ui/preferences_window.blp | 15 ++
src/backend/channel.rs | 59 +++++-
src/backend/manager.rs | 43 +++-
src/backend/manager_thread.rs | 8 +-
src/backend/message/mod.rs | 12 +-
src/gui/channel_messages.rs | 251 ++++++++++++++++++++++-
src/gui/preferences_window.rs | 23 +++
11 files changed, 443 insertions(+), 21 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 20dc578..2bde927 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Added
+
+- Send typing indicators while composing a message and display them above the message input.
+- Settings to enable or disable sending and showing typing indicators.
+
## [0.20.4] - 2026-04-22
### Fixed
diff --git a/data/de.schmidhuberj.Flare.gschema.xml b/data/de.schmidhuberj.Flare.gschema.xml
index 8a58415..0705a73 100644
--- a/data/de.schmidhuberj.Flare.gschema.xml
+++ b/data/de.schmidhuberj.Flare.gschema.xml
@@ -58,6 +58,15 @@
<summary>Send a message when the Enter-key is pressed</summary>
</key>
+ <key name="show-typing-indicators" type="b">
+ <default>true</default>
+ <summary>Show typing indicators of other users</summary>
+ </key>
+ <key name="send-typing-indicators" type="b">
+ <default>true</default>
+ <summary>Send typing indicators while composing</summary>
+ </key>
+
<key name="sort-contacts-by" type="s">
<default>"firstname"</default>
<summary>How to sort contacts, e.g with "firstname" or "surname"</summary>
diff --git a/data/resources/style.css b/data/resources/style.css
index dcd0569..00e4783 100644
--- a/data/resources/style.css
+++ b/data/resources/style.css
@@ -13,6 +13,12 @@
border-top: 1px solid @borders;
}
+.typing-indicator {
+ background-color: @window_bg_color;
+ border-top: 1px solid @borders;
+ min-height: 18px;
+}
+
.message-list row {
padding:0;
}
diff --git a/data/resources/ui/channel_messages.blp b/data/resources/ui/channel_messages.blp
index 53be7ab..7f438e4 100644
--- a/data/resources/ui/channel_messages.blp
+++ b/data/resources/ui/channel_messages.blp
@@ -102,6 +102,39 @@ template $FlChannelMessages: Box {
}
}
+ // Typing indicator
+ Box typing_indicator {
+ styles [
+ "typing-indicator",
+ ]
+
+ orientation: horizontal;
+ hexpand: true;
+ visible: bind template.show-typing as <bool>;
+
+ Adw.Clamp {
+ maximum-size: 800;
+ tightening-threshold: 600;
+ hexpand: true;
+
+ Label {
+ styles [
+ "caption",
+ "dim-label",
+ ]
+
+ halign: start;
+ ellipsize: end;
+ xalign: 0;
+ margin-start: 12;
+ margin-end: 12;
+ margin-top: 2;
+ margin-bottom: 2;
+ label: bind template.active-channel as <$FlChannel>.typing-label;
+ }
+ }
+ }
+
Box {
styles [
"toolbar",
diff --git a/data/resources/ui/preferences_window.blp b/data/resources/ui/preferences_window.blp
index dd84f74..2068cab 100644
--- a/data/resources/ui/preferences_window.blp
+++ b/data/resources/ui/preferences_window.blp
@@ -66,6 +66,21 @@ template $FlPreferencesWindow: Adw.PreferencesDialog {
);
}
}
+
+ Adw.PreferencesGroup {
+ title: _("Typing Indicators");
+ description: _("Inform other users when you are composing a message and show indicators when they are");
+
+ Adw.SwitchRow row_send_typing_indicators {
+ title: _("Send Typing Indicators");
+ subtitle: _("Notify others while you are composing a message");
+ }
+
+ Adw.SwitchRow row_show_typing_indicators {
+ title: _("Show Typing Indicators");
+ subtitle: _("Display when other users are composing a message");
+ }
+ }
}
}
diff --git a/src/backend/channel.rs b/src/backend/channel.rs
index 73e82f3..4bb1d38 100644
--- a/src/backend/channel.rs
+++ b/src/backend/channel.rs
@@ -15,8 +15,9 @@ use glib::Bytes;
use glib::{Object, prelude::Cast};
use libsignal_service::{
- proto::{DataMessage, GroupContextV2},
+ proto::{DataMessage, GroupContextV2, TypingMessage, typing_message::Action as TypingAction},
protocol::ServiceId,
+ zkgroup::groups::{GroupMasterKey, GroupSecretParams},
};
use presage::model::groups::Group;
use presage::store::Thread;
@@ -230,6 +231,62 @@ impl Channel {
self.manager().send_session_reset(uuid, ts).await
}
+ /// Send a typing indicator (started/stopped) to the channel.
+ ///
+ /// Returns `Ok(())` without sending if the user has disabled the
+ /// `send-typing-indicators` setting or the channel has no resolvable peer.
+ pub async fn send_typing(&self, started: bool) -> Result<(), ApplicationError> {
+ // Note-to-self has no useful peer to inform, and routing the
+ // event through `send_message(self_uuid, …)` would fan it out to
+ // every other linked device on the account where flare's own
+ // receive path lights up an "is typing" indicator on its copy of
+ // Note-to-self.
+ if self.is_self() {
+ return Ok(());
+ }
+ let manager = self.manager();
+ if !manager.settings().boolean("send-typing-indicators") {
+ return Ok(());
+ }
+
+ let timestamp = std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .expect("Time went backwards")
+ .as_millis() as u64;
+
+ let action = if started {
+ TypingAction::Started
+ } else {
+ TypingAction::Stopped
+ };
+
+ let group_id = self
+ .group_context()
+ .and_then(|c| c.master_key)
+ .and_then(|k| <[u8; 32]>::try_from(k).ok())
+ .map(|master_key| {
+ GroupSecretParams::derive_from_master_key(GroupMasterKey::new(master_key))
+ .get_group_identifier()
+ .to_vec()
+ });
+
+ let typing = TypingMessage {
+ timestamp: Some(timestamp),
+ action: Some(action as i32),
+ group_id: group_id.clone(),
+ };
+
+ if let Some(group_master_key) = self.group_context().and_then(|c| c.master_key) {
+ manager
+ .send_message_to_group(group_master_key, typing, timestamp)
+ .await?;
+ } else if let Some(uuid) = self.uuid() {
+ manager.send_message(uuid, typing, timestamp).await?;
+ }
+
+ Ok(())
+ }
+
/// Register a new message with the channel.
/// This does the following (based on the type of message):
/// - Add a quote to the message if needed.
diff --git a/src/backend/manager.rs b/src/backend/manager.rs
index c25fba0..eaa41e0 100644
--- a/src/backend/manager.rs
+++ b/src/backend/manager.rs
@@ -8,7 +8,7 @@ use libsignal_service::protocol::DeviceId;
use libsignal_service::{
Profile,
content::ContentBody,
- proto::{AttachmentPointer, DataMessage, GroupContextV2},
+ proto::{AttachmentPointer, GroupContextV2},
protocol::ServiceId,
sender::{AttachmentSpec, AttachmentUploadError},
websocket::account::DeviceInfo,
@@ -490,20 +490,42 @@ impl Manager {
Thread::Contact(uuid)
};
+ // Fast path: return the cached channel if we already know it.
+ // Without this, callers that arrive after initial channel discovery
+ // (incoming TypingMessage routing, in particular) would receive a
+ // freshly-built Channel object whose property notifications never
+ // reach widgets bound to the cached one in the UI — typing
+ // indicators on both the header bar and the channel-messages view
+ // would silently never light up.
+ if let Some(cached) = self.imp().channels.borrow().get(&thread).cloned() {
+ return cached;
+ }
+
let contact = Contact::from_service_address(&uuid, self).await;
let channel = Channel::from_contact_or_group(contact, group, self).await;
channel.initialize_avatar().await;
- let mut known_channels = self.imp().channels.borrow_mut();
- known_channels.entry(thread).or_insert_with(|| {
- log::trace!("Got a contact from the storage");
+ // Another task may have inserted the same thread while we were
+ // awaiting; pick whichever is already there or insert ours.
+ let stored = {
+ let mut known = self.imp().channels.borrow_mut();
+ known
+ .entry(thread)
+ .or_insert_with(|| {
+ log::trace!("Got a contact from the storage");
+ channel.clone()
+ })
+ .clone()
+ };
+
+ if stored == channel {
self.emit_by_name::<()>("channel", &[&channel]);
- channel.clone()
- });
+ }
- // No need to initialize avatar or last messages in here, will be done when initializing contacts.
+ // No need to initialize avatar or last messages in here, will be
+ // done when initializing contacts.
- channel
+ stored
}
pub fn channel_from_thread(&self, thread: Thread) -> Option<Channel> {
@@ -737,14 +759,15 @@ impl Manager {
pub(super) async fn send_message_to_group(
&self,
group_key: Vec<u8>,
- message: DataMessage,
+ message: impl Into<ContentBody>,
timestamp: u64,
) -> Result<(), ApplicationError> {
log::trace!("`Manager::send_message_to_group` start");
+ let body = message.into();
let internal = self.internal();
let r = tspawn!(async move {
internal
- .send_message_to_group(group_key, message.clone(), timestamp)
+ .send_message_to_group(group_key, body.clone(), timestamp)
.await
})
.await
diff --git a/src/backend/manager_thread.rs b/src/backend/manager_thread.rs
index 1f6a885..cba62ae 100644
--- a/src/backend/manager_thread.rs
+++ b/src/backend/manager_thread.rs
@@ -21,7 +21,7 @@ use libsignal_service::{
configuration::SignalServers,
content::ContentBody,
prelude::{ProfileKey, Uuid, phonenumber},
- proto::{AttachmentPointer, DataMessage, GroupContextV2},
+ proto::{AttachmentPointer, GroupContextV2},
protocol::ServiceId,
sender::{AttachmentSpec, AttachmentUploadError},
websocket::account::DeviceInfo,
@@ -65,7 +65,7 @@ enum Command {
),
SendMessageToGroup(
Vec<u8>,
- Box<DataMessage>,
+ Box<ContentBody>,
u64,
oneshot::Sender<Result<(), Error>>,
),
@@ -353,7 +353,7 @@ impl ManagerThread {
pub async fn send_message_to_group(
&self,
group_key: Vec<u8>,
- message: DataMessage,
+ message: impl Into<ContentBody>,
timestamp: u64,
) -> Result<(), Error> {
let (sender, receiver) = oneshot::channel();
@@ -361,7 +361,7 @@ impl ManagerThread {
.clone()
.send(Command::SendMessageToGroup(
group_key,
- Box::new(message),
+ Box::new(message.into()),
timestamp,
sender,
))
diff --git a/src/backend/message/mod.rs b/src/backend/message/mod.rs
index 11ccd7c..74952ac 100644
--- a/src/backend/message/mod.rs
+++ b/src/backend/message/mod.rs
@@ -270,14 +270,16 @@ impl Message {
// Typing messages.
// Note that they are currently only implemented for contacts, this requires upstream updates to fix.
ContentBody::TypingMessage(t) => {
+ // Both group and contact branches stay cache-only: we only
+ // surface typing for conversations the user already knows
+ // about. Going through `channel_from_uuid_or_group` here
+ // would mint a new Channel object on the first typing
+ // event from a stranger and add them to the sidebar with
+ // no actual messages.
let channel = if let Some(id) = &t.group_id {
manager.channel_from_group_id(id)
} else {
- Some(
- manager
- .channel_from_uuid_or_group(metadata.sender, &None)
- .await,
- )
+ manager.channel_from_thread(presage::store::Thread::Contact(metadata.sender))
};
let Some(channel) = channel else {
diff --git a/src/gui/channel_messages.rs b/src/gui/channel_messages.rs
index 0e8ae4e..c6684fc 100644
--- a/src/gui/channel_messages.rs
+++ b/src/gui/channel_messages.rs
@@ -5,6 +5,16 @@ use crate::ApplicationError;
const MESSAGES_REQUEST_LOAD: usize = 10;
+/// Re-send the `Started` typing event at this interval so the receiver
+/// does not let the indicator expire while the user keeps composing.
+/// Must stay strictly below `TYPING_NOTIFICATION_DURATION_SECONDS` in
+/// `crate::backend::channel`.
+const TYPING_REFRESH_SECONDS: u32 = 8;
+
+/// Send `Stopped` if no buffer change has happened in this many seconds.
+/// Mirrors how Signal apps treat composition pauses as the end of typing.
+const TYPING_IDLE_SECONDS: u32 = 5;
+
glib::wrapper! {
/// [ChannelMessages] is the right pane displaying the list of messages and the entry-bar.
pub struct ChannelMessages(ObjectSubclass<imp::ChannelMessages>)
@@ -103,6 +113,200 @@ impl ChannelMessages {
));
}
+ /// Connect the `show-typing-indicators` setting so the typing indicator
+ /// updates immediately when the user toggles the preference.
+ fn setup_typing_settings(&self) {
+ self.manager().settings().connect_changed(
+ Some("show-typing-indicators"),
+ clone!(
+ #[weak(rename_to = s)]
+ self,
+ move |_, _| s.refresh_show_typing()
+ ),
+ );
+ }
+
+ /// Re-evaluate `show-typing` for the current channel based on the channel's
+ /// `is-typing` state and the user's `show-typing-indicators` setting.
+ fn refresh_show_typing(&self) {
+ // The active-channel bind runs during template init before the
+ // manager bind, so `self.manager()` (typed as Manager, not
+ // Option<Manager>) would panic here. Read the manager directly so
+ // a null intermediate state is harmless: with no manager we don't
+ // know the user's preference, so default to showing the indicator.
+ let allowed = self
+ .imp()
+ .manager
+ .borrow()
+ .as_ref()
+ .is_none_or(|m| m.settings().boolean("show-typing-indicators"));
+ let typing = self
+ .active_channel()
+ .map(|c| c.is_typing())
+ .unwrap_or(false);
+ self.set_show_typing(allowed && typing);
+ }
+
+ /// Wire the `show-typing` property to the active channel's `is-typing`.
+ /// Called whenever the active channel changes.
+ fn setup_typing_indicator(&self) {
+ self.refresh_show_typing();
+
+ // Disconnect the handler we attached on the previous active
+ // channel so we don't accumulate one per channel switch.
+ if let Some((prev_channel, handler)) = self.imp().typing_handler.take() {
+ prev_channel.disconnect(handler);
+ }
+
+ if let Some(channel) = self.active_channel() {
+ let handler = channel.connect_notify_local(
+ Some("is-typing"),
+ clone!(
+ #[weak(rename_to = s)]
+ self,
+ move |_, _| s.refresh_show_typing()
+ ),
+ );
+ self.imp()
+ .typing_handler
+ .replace(Some((channel, handler)));
+ }
+ }
+
+ /// Send a `Started` typing event for the active channel.
+ ///
+ /// Schedules a periodic refresh so the receiver does not let the
+ /// indicator expire while the user is still composing.
+ fn send_typing_started(&self) {
+ let imp = self.imp();
+ let Some(channel) = self.active_channel() else {
+ return;
+ };
+ if !self.manager().settings().boolean("send-typing-indicators") {
+ return;
+ }
+
+ // Mark this channel as the current typing target so a later channel
+ // switch can still emit a matching `Stopped` event.
+ imp.typing_target.replace(Some(channel.clone()));
+
+ // Refresh `Started` periodically while the user keeps composing.
+ let needs_initial = !imp.sending_typing.replace(true);
+
+ if let Some(source) = imp.typing_refresh.borrow_mut().take() {
+ source.remove();
+ }
+ let refresh = glib::timeout_add_seconds_local(
+ TYPING_REFRESH_SECONDS,
+ clone!(
+ #[weak(rename_to = s)]
+ self,
+ #[upgrade_or]
+ glib::ControlFlow::Break,
+ move || {
+ if !s.imp().sending_typing.get() {
+ return glib::ControlFlow::Break;
+ }
+ s.dispatch_send_typing(true);
+ glib::ControlFlow::Continue
+ }
+ ),
+ );
+ imp.typing_refresh.replace(Some(refresh));
+
+ if needs_initial {
+ self.dispatch_send_typing(true);
+ }
+ }
+
+ /// Send a `Stopped` typing event for the channel that was last targeted.
+ fn send_typing_stopped(&self) {
+ let imp = self.imp();
+ if let Some(source) = imp.typing_refresh.borrow_mut().take() {
+ source.remove();
+ }
+ if let Some(source) = imp.typing_idle.borrow_mut().take() {
+ source.remove();
+ }
+ if !imp.sending_typing.replace(false) {
+ // Nothing to do — we never told anyone we were typing.
+ imp.typing_target.replace(None);
+ return;
+ }
+ let Some(channel) = imp.typing_target.replace(None) else {
+ return;
+ };
+ gspawn!(async move {
+ if let Err(e) = channel.send_typing(false).await {
+ log::warn!("Failed to send `Stopped` typing event: {e}");
+ }
+ });
+ }
+
+ /// Dispatch the actual `Started` typing event to whichever channel is
+ /// currently considered the typing target.
+ fn dispatch_send_typing(&self, started: bool) {
+ let Some(channel) = self.imp().typing_target.borrow().clone() else {
+ return;
+ };
+ gspawn!(async move {
+ if let Err(e) = channel.send_typing(started).await {
+ log::warn!("Failed to send typing event (started={started}): {e}");
+ }
+ });
+ }
+
+ /// Connect the text entry's buffer so we can emit `Started`/`Stopped`
+ /// typing events as the user composes a message.
+ fn setup_typing_send(&self) {
+ let buffer = self.imp().text_entry.buffer();
+ let handler = buffer.connect_changed(clone!(
+ #[weak(rename_to = s)]
+ self,
+ move |buf| {
+ let (start, end) = buf.bounds();
+ if start == end {
+ s.send_typing_stopped();
+ return;
+ }
+ // Both the Started event and the idle-stop timer are
+ // outbound-typing-only behaviours; if the user has
+ // disabled outgoing typing, do nothing and don't churn
+ // a timer per keystroke.
+ let allowed = s
+ .imp()
+ .manager
+ .borrow()
+ .as_ref()
+ .is_none_or(|m| m.settings().boolean("send-typing-indicators"));
+ if !allowed {
+ return;
+ }
+ s.send_typing_started();
+ s.reset_typing_idle_timer();
+ }
+ ));
+ self.imp().typing_buffer_handler.replace(Some(handler));
+ }
+
+ /// Schedule a one-shot timer that sends `Stopped` if the user lets the
+ /// composition idle for more than `TYPING_IDLE_SECONDS` seconds.
+ fn reset_typing_idle_timer(&self) {
+ let imp = self.imp();
+ if let Some(source) = imp.typing_idle.borrow_mut().take() {
+ source.remove();
+ }
+ let source = glib::timeout_add_seconds_local_once(
+ TYPING_IDLE_SECONDS,
+ clone!(
+ #[weak(rename_to = s)]
+ self,
+ move || s.send_typing_stopped()
+ ),
+ );
+ imp.typing_idle.replace(Some(source));
+ }
+
pub async fn clear_messages(&self) -> Result<(), ApplicationError> {
if let Some(channel) = self.active_channel() {
channel.clear_messages().await?;
@@ -165,9 +369,36 @@ pub mod imp {
filling_screen: Cell<bool>,
#[property(get = Self::has_attachments)]
has_attachments: PhantomData<bool>,
+ #[property(get, set)]
+ show_typing: Cell<bool>,
+
+ /// Whether we currently believe the user is composing a message in the
+ /// active channel and have informed the peer with a `Started` event.
+ pub(super) sending_typing: Cell<bool>,
+ /// Channel to which we last sent a `Started` typing event, kept so we
+ /// can send a matching `Stopped` even after the active channel changes.
+ pub(super) typing_target: RefCell<Option<Channel>>,
+ /// Periodic refresh of the `Started` typing event so it does not
+ /// expire on the receiver side while the user is still composing.
+ pub(super) typing_refresh: RefCell<Option<glib::SourceId>>,
+ /// One-shot timer that emits `Stopped` after a stretch of no
+ /// further buffer changes.
+ pub(super) typing_idle: RefCell<Option<glib::SourceId>>,
+ /// Notify handler installed on the active channel's `is-typing`
+ /// property so we can disconnect it before re-attaching when the
+ /// active channel changes.
+ pub(super) typing_handler: RefCell<Option<(Channel, glib::SignalHandlerId)>>,
+ /// Notify + selection-changed handlers installed on the active
+ /// channel by `setup_selection_listener`, kept so we can disconnect
+ /// them before re-attaching on the next channel change.
+ pub(super) selection_handlers: RefCell<Vec<(Channel, glib::SignalHandlerId)>>,
+ /// Buffer change handler that drives the typing-send logic; we
+ /// block it while restoring a draft so loading a draft does not
+ /// transmit a Started typing event.
+ pub(super) typing_buffer_handler: RefCell<Option<glib::SignalHandlerId>>,
#[property(get, set = Self::set_manager, type = Manager)]
- manager: RefCell<Option<Manager>>,
+ pub(super) manager: RefCell<Option<Manager>>,
}
#[gtk::template_callbacks]
@@ -181,10 +412,16 @@ pub mod imp {
self.manager.replace(man);
if initialized {
self.obj().setup_send_on_enter();
+ self.obj().setup_typing_settings();
+ self.obj().setup_typing_send();
}
}
fn set_active_channel(&self, chan: Option<Channel>) {
+ // Inform the previous channel we have stopped typing before we
+ // forget about it.
+ self.obj().send_typing_stopped();
+
if let Some(active_chan) = self.active_channel.borrow().as_ref() {
active_chan.set_property("draft", self.text_entry.text());
}
@@ -195,6 +432,7 @@ pub mod imp {
}
self.obj().focus_input();
+ self.obj().setup_typing_indicator();
}
#[template_callback(function)]
@@ -501,7 +739,18 @@ pub mod imp {
s.obj().set_reply_message(None::<TextMessage>);
if let Some(channel) = s.active_channel.borrow().as_ref() {
let draft = channel.property("draft");
+ // Block the typing buffer-changed handler so
+ // restoring a stored draft does not transmit
+ // a Started typing event to the peer.
+ let buffer = s.text_entry.buffer();
+ let handler_guard = s.typing_buffer_handler.borrow();
+ if let Some(handler) = handler_guard.as_ref() {
+ buffer.block_signal(handler);
+ }
s.text_entry.set_text(draft);
+ if let Some(handler) = handler_guard.as_ref() {
+ buffer.unblock_signal(handler);
+ }
};
}
),
diff --git a/src/gui/preferences_window.rs b/src/gui/preferences_window.rs
index 8137af7..b2b6405 100644
--- a/src/gui/preferences_window.rs
+++ b/src/gui/preferences_window.rs
@@ -78,6 +78,11 @@ pub mod imp {
#[template_child]
row_send_on_enter: TemplateChild<adw::SwitchRow>,
+ #[template_child]
+ row_send_typing_indicators: TemplateChild<adw::SwitchRow>,
+ #[template_child]
+ row_show_typing_indicators: TemplateChild<adw::SwitchRow>,
+
settings: Settings,
}
@@ -173,6 +178,22 @@ pub mod imp {
.bind("send-on-enter", &self.row_send_on_enter.get(), "active")
.flags(SettingsBindFlags::DEFAULT)
.build();
+ self.settings
+ .bind(
+ "send-typing-indicators",
+ &self.row_send_typing_indicators.get(),
+ "active",
+ )
+ .flags(SettingsBindFlags::DEFAULT)
+ .build();
+ self.settings
+ .bind(
+ "show-typing-indicators",
+ &self.row_show_typing_indicators.get(),
+ "active",
+ )
+ .flags(SettingsBindFlags::DEFAULT)
+ .build();
}
}
@@ -194,6 +215,8 @@ pub mod imp {
row_background: TemplateChild::default(),
row_messages_selectable: TemplateChild::default(),
row_send_on_enter: TemplateChild::default(),
+ row_send_typing_indicators: TemplateChild::default(),
+ row_show_typing_indicators: TemplateChild::default(),
}
}
--
2.53.0