use std::{collections::HashMap, fmt::Write};
use adw::{prelude::*, subclass::prelude::*};
use futures_util::{future, pin_mut, StreamExt};
use gettextrs::{gettext, pgettext};
use gtk::{
gdk, gio,
glib::{self, clone},
CompositeTemplate,
};
use matrix_sdk::{
attachment::{AttachmentConfig, AttachmentInfo, BaseFileInfo, Thumbnail},
room::edit::EditedContent,
};
use matrix_sdk_ui::timeline::{RepliedToInfo, TimelineItemContent};
use ruma::{
events::{
room::message::{
EmoteMessageEventContent, FormattedBody, ForwardThread, LocationMessageEventContent,
MessageType, RoomMessageEventContent, RoomMessageEventContentWithoutRelation,
},
Mentions,
},
OwnedRoomId, OwnedUserId,
};
use tracing::{debug, error, warn};
mod attachment_dialog;
mod completion;
mod composer_state;
pub use self::composer_state::{ComposerState, RelationInfo};
use self::{attachment_dialog::AttachmentDialog, completion::CompletionPopover};
use super::message_row::MessageContent;
use crate::{
components::{AtRoom, CustomEntry, LabelWithWidgets, Pill, PillSource},
gettext_f,
prelude::*,
session::model::{Event, Member, Room},
spawn, spawn_tokio, toast,
utils::{
matrix::AT_ROOM,
media::{
filename_for_mime, image::ImageInfoLoader, load_audio_info, load_file,
video::load_video_info,
},
template_callbacks::TemplateCallbacks,
Location, LocationError, TokioDrop,
},
};
type ComposerStatesMap = HashMap<Option<String>, HashMap<Option<OwnedRoomId>, ComposerState>>;
mod imp {
use std::{
cell::{Cell, RefCell},
marker::PhantomData,
};
use glib::subclass::InitializingObject;
use super::*;
use crate::Application;
#[derive(Debug, Default, CompositeTemplate, glib::Properties)]
#[template(
resource = "/org/gnome/Fractal/ui/session/view/content/room_history/message_toolbar/mod.ui"
)]
#[properties(wrapper_type = super::MessageToolbar)]
pub struct MessageToolbar {
#[property(get, set = Self::set_room, explicit_notify, nullable)]
pub room: glib::WeakRef<Room>,
pub can_send_message_handler: RefCell<Option<glib::SignalHandlerId>>,
#[property(get, set)]
pub markdown_enabled: Cell<bool>,
pub completion: CompletionPopover,
#[template_child]
pub main_stack: TemplateChild<gtk::Stack>,
#[template_child]
pub message_entry: TemplateChild<sourceview::View>,
#[template_child]
pub related_event_header: TemplateChild<LabelWithWidgets>,
#[template_child]
pub related_event_content: TemplateChild<MessageContent>,
#[property(get = Self::current_composer_state)]
pub current_composer_state: PhantomData<ComposerState>,
composer_state_handler: RefCell<Option<glib::SignalHandlerId>>,
buffer_handlers: RefCell<Option<(glib::SignalHandlerId, glib::Binding)>>,
pub composer_states: RefCell<ComposerStatesMap>,
}
#[glib::object_subclass]
impl ObjectSubclass for MessageToolbar {
const NAME: &'static str = "MessageToolbar";
type Type = super::MessageToolbar;
type ParentType = adw::Bin;
fn class_init(klass: &mut Self::Class) {
CustomEntry::ensure_type();
Self::bind_template(klass);
Self::Type::bind_template_callbacks(klass);
TemplateCallbacks::bind_template_callbacks(klass);
klass.install_action_async(
"message-toolbar.send-text-message",
None,
|widget, _, _| async move {
widget.send_text_message().await;
},
);
klass.install_action_async(
"message-toolbar.select-file",
None,
|widget, _, _| async move {
widget.select_file().await;
},
);
klass.install_action("message-toolbar.open-emoji", None, |widget, _, _| {
widget.open_emoji();
});
klass.install_action_async(
"message-toolbar.send-location",
None,
|widget, _, _| async move {
widget.send_location().await;
},
);
klass.install_property_action("message-toolbar.markdown", "markdown-enabled");
klass.install_action("message-toolbar.clear-related-event", None, |obj, _, _| {
obj.current_composer_state().set_related_to(None);
});
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
#[glib::derived_properties]
impl ObjectImpl for MessageToolbar {
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
self.message_entry.connect_paste_clipboard(clone!(
#[weak]
obj,
move |entry| {
if !obj.imp().can_send_message() {
return;
}
let formats = obj.clipboard().formats();
if formats.contains_type(gio::File::static_type())
|| formats.contains_type(gdk::Texture::static_type())
{
entry.stop_signal_emission_by_name("paste-clipboard");
spawn!(async move {
obj.read_clipboard_file().await;
});
}
}
));
self.message_entry.connect_copy_clipboard(clone!(
#[weak]
obj,
move |entry| {
entry.stop_signal_emission_by_name("copy-clipboard");
spawn!(async move {
obj.copy_buffer_selection_to_clipboard().await;
});
}
));
self.message_entry.connect_cut_clipboard(clone!(
#[weak]
obj,
move |entry| {
entry.stop_signal_emission_by_name("cut-clipboard");
spawn!(clone!(
#[weak]
entry,
async move {
obj.copy_buffer_selection_to_clipboard().await;
entry.buffer().delete_selection(true, true);
}
));
}
));
let key_events = gtk::EventControllerKey::new();
key_events.connect_key_pressed(clone!(
#[weak]
obj,
#[upgrade_or]
glib::Propagation::Proceed,
move |_, key, _, modifier| {
if modifier.is_empty() && (key == gdk::Key::Return || key == gdk::Key::KP_Enter)
{
spawn!(async move {
obj.send_text_message().await;
});
glib::Propagation::Stop
} else if modifier.is_empty()
&& key == gdk::Key::Escape
&& obj.current_composer_state().has_relation()
{
obj.current_composer_state().set_related_to(None);
glib::Propagation::Stop
} else {
glib::Propagation::Proceed
}
}
));
self.message_entry.add_controller(key_events);
let settings = Application::default().settings();
settings
.bind("markdown-enabled", &*obj, "markdown-enabled")
.build();
self.completion.set_parent(&*self.message_entry);
obj.bind_property("room", &self.completion, "room")
.sync_create()
.build();
let location = Location::new();
obj.action_set_enabled("message-toolbar.send-location", location.is_available());
}
fn dispose(&self) {
self.completion.unparent();
if let Some(room) = self.room.upgrade() {
if let Some(handler) = self.can_send_message_handler.take() {
room.permissions().disconnect(handler);
}
}
}
}
impl WidgetImpl for MessageToolbar {}
impl BinImpl for MessageToolbar {}
impl MessageToolbar {
fn set_room(&self, room: Option<Room>) {
let old_room = self.room.upgrade();
if old_room == room {
return;
}
let obj = self.obj();
if let Some(room) = &old_room {
if let Some(handler) = self.can_send_message_handler.take() {
room.permissions().disconnect(handler);
}
}
if let Some(room) = &room {
let can_send_message_handler =
room.permissions().connect_can_send_message_notify(clone!(
#[weak(rename_to = imp)]
self,
move |_| {
imp.can_send_message_updated();
}
));
self.can_send_message_handler
.replace(Some(can_send_message_handler));
}
self.room.set(room.as_ref());
self.can_send_message_updated();
self.message_entry.grab_focus();
obj.notify_room();
self.update_current_composer_state(old_room.as_ref());
}
pub(super) fn can_send_message(&self) -> bool {
self.room
.upgrade()
.is_some_and(|r| r.permissions().can_send_message())
}
fn can_send_message_updated(&self) {
let page = if self.can_send_message() {
"enabled"
} else {
"disabled"
};
self.main_stack.set_visible_child_name(page);
}
fn current_composer_state(&self) -> ComposerState {
let room = self.room.upgrade();
self.composer_state(room.as_ref())
}
fn composer_state(&self, room: Option<&Room>) -> ComposerState {
self.composer_states
.borrow_mut()
.entry(
room.and_then(|r| r.session())
.map(|s| s.session_id().to_owned()),
)
.or_default()
.entry(room.map(|r| r.room_id().to_owned()))
.or_insert_with(|| ComposerState::new(room))
.clone()
}
fn update_current_composer_state(&self, old_room: Option<&Room>) {
let old_composer_state = self.composer_state(old_room);
old_composer_state.attach_to_view(None);
if let Some(handler) = self.composer_state_handler.take() {
old_composer_state.disconnect(handler);
}
if let Some((handler, binding)) = self.buffer_handlers.take() {
let prev_buffer = self.message_entry.buffer();
prev_buffer.disconnect(handler);
binding.unbind();
}
let composer_state = self.current_composer_state();
let buffer = composer_state.buffer();
let obj = self.obj();
composer_state.attach_to_view(Some(&self.message_entry));
let text_notify_handler = buffer.connect_text_notify(clone!(
#[weak]
obj,
move |buffer| {
let (start_iter, end_iter) = buffer.bounds();
let is_empty = start_iter == end_iter;
obj.action_set_enabled("message-toolbar.send-text-message", !is_empty);
obj.send_typing_notification(!is_empty);
}
));
let (start_iter, end_iter) = buffer.bounds();
obj.action_set_enabled("message-toolbar.send-text-message", start_iter != end_iter);
let markdown_binding = obj
.bind_property("markdown-enabled", &buffer, "highlight-syntax")
.sync_create()
.build();
self.buffer_handlers
.replace(Some((text_notify_handler, markdown_binding)));
let composer_state_handler = composer_state.connect_related_to_changed(clone!(
#[weak(rename_to = imp)]
self,
move |_| {
imp.update_related_event();
}
));
self.composer_state_handler
.replace(Some(composer_state_handler));
self.update_related_event();
obj.notify_current_composer_state();
}
fn update_related_event(&self) {
let composer_state = self.current_composer_state();
match composer_state.related_to() {
Some(RelationInfo::Reply(info)) => {
self.update_for_reply(info);
}
Some(RelationInfo::Edit(_)) => {
self.update_for_edit();
}
None => {}
}
}
fn update_for_reply(&self, info: RepliedToInfo) {
let Some(room) = self.room.upgrade() else {
return;
};
let sender = room
.get_or_create_members()
.get_or_create(info.sender().to_owned());
self.related_event_header
.set_widgets(vec![Pill::new(&sender)]);
self.related_event_header
.set_label(Some(gettext_f("Reply to {user}", &[("user", "<widget>")])));
self.related_event_content
.update_for_related_event(info, sender);
self.related_event_content.set_visible(true);
}
fn update_for_edit(&self) {
self.related_event_header.set_widgets::<gtk::Widget>(vec![]);
self.related_event_header
.set_label(Some(pgettext("room-history", "Edit")));
self.related_event_content.set_visible(false);
}
}
}
glib::wrapper! {
pub struct MessageToolbar(ObjectSubclass<imp::MessageToolbar>)
@extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
}
#[gtk::template_callbacks]
impl MessageToolbar {
pub fn new() -> Self {
glib::Object::new()
}
pub fn mention_member(&self, member: &Member) {
let view = &*self.imp().message_entry;
let buffer = view.buffer();
let mut insert = buffer.iter_at_mark(&buffer.get_insert());
let pill = member.to_pill();
self.current_composer_state().add_widget(pill, &mut insert);
view.grab_focus();
}
pub fn set_reply_to(&self, event: Event) {
let imp = self.imp();
if !imp.can_send_message() {
return;
}
let Ok(info) = event.item().replied_to_info() else {
warn!("Unsupported event type for reply");
return;
};
self.current_composer_state()
.set_related_to(Some(RelationInfo::Reply(info)));
imp.message_entry.grab_focus();
}
pub fn set_edit(&self, event: Event) {
let imp = self.imp();
if !imp.can_send_message() {
return;
}
let item = event.item();
let Some(event_id) = item.event_id() else {
warn!("Cannot send edit for event that is not sent yet");
return;
};
let TimelineItemContent::Message(message) = item.content() else {
warn!("Unsupported event type for edit");
return;
};
self.current_composer_state()
.set_edit_source(event_id.to_owned(), message);
imp.message_entry.grab_focus();
}
async fn send_text_message(&self) {
let imp = self.imp();
if !imp.can_send_message() {
return;
}
let Some(room) = self.room() else {
return;
};
let composer_state = self.current_composer_state();
let buffer = composer_state.buffer();
let (start_iter, end_iter) = buffer.bounds();
let body_len = end_iter.offset() as usize;
let is_markdown = self.markdown_enabled();
let mut has_rich_mentions = false;
let mut plain_body = String::with_capacity(body_len);
let mut formatted_body = String::with_capacity(body_len);
let mut mentions = Mentions::new();
let split_message = MessageBufferParser::new(&composer_state, start_iter, end_iter);
for chunk in split_message {
match chunk {
MessageBufferChunk::Text(text) => {
plain_body.push_str(&text);
formatted_body.push_str(&text);
}
MessageBufferChunk::Mention(source) => match Mention::from_source(&source).await {
Mention::Rich { name, uri, user_id } => {
has_rich_mentions = true;
plain_body.push_str(&name);
if is_markdown {
let _ = write!(formatted_body, "[{name}]({uri})");
} else {
let _ = write!(formatted_body, "<a href=\"{uri}\">{name}</a>");
};
if let Some(user_id) = user_id {
mentions.user_ids.insert(user_id);
}
}
Mention::AtRoom => {
plain_body.push_str(AT_ROOM);
formatted_body.push_str(AT_ROOM);
mentions.room = true;
}
},
}
}
let is_emote = plain_body.starts_with("/me ");
if is_emote {
plain_body.replace_range(.."/me ".len(), "");
formatted_body.replace_range(.."/me ".len(), "");
}
if plain_body.trim().is_empty() {
return;
}
let html_body = if is_markdown {
FormattedBody::markdown(formatted_body).map(|b| b.body)
} else if has_rich_mentions {
Some(formatted_body)
} else {
None
};
let mut content = if is_emote {
MessageType::Emote(if let Some(html_body) = html_body {
EmoteMessageEventContent::html(plain_body, html_body)
} else {
EmoteMessageEventContent::plain(plain_body)
})
.into()
} else if let Some(html_body) = html_body {
RoomMessageEventContentWithoutRelation::text_html(plain_body, html_body)
} else {
RoomMessageEventContentWithoutRelation::text_plain(plain_body)
};
content = content.add_mentions(mentions);
let matrix_timeline = room.timeline().matrix_timeline();
match composer_state.related_to() {
Some(RelationInfo::Reply(replied_to_info)) => {
let handle = spawn_tokio!(async move {
matrix_timeline
.send_reply(content, replied_to_info, ForwardThread::Yes)
.await
});
if let Err(error) = handle.await.unwrap() {
error!("Could not send reply: {error}");
toast!(self, gettext("Could not send reply"));
}
}
Some(RelationInfo::Edit(event_id)) => {
let matrix_room = room.matrix_room().clone();
let handle = spawn_tokio!(async move {
let full_content = matrix_room
.make_edit_event(&event_id, EditedContent::RoomMessage(content))
.await?;
let send_queue = matrix_room.send_queue();
send_queue.send(full_content).await?;
Ok::<(), matrix_sdk_ui::timeline::Error>(())
});
if let Err(error) = handle.await.unwrap() {
error!("Could not send edit: {error}");
toast!(self, gettext("Could not send edit"));
}
}
_ => {
let handle = spawn_tokio!(async move {
matrix_timeline
.send(content.with_relation(None).into())
.await
});
if let Err(error) = handle.await.unwrap() {
error!("Could not send message: {error}");
toast!(self, gettext("Could not send message"));
}
}
}
composer_state.clear();
}
fn open_emoji(&self) {
let imp = self.imp();
if !imp.can_send_message() {
return;
}
imp.message_entry.emit_insert_emoji();
}
async fn send_location(&self) {
if !self.imp().can_send_message() {
return;
}
let Some(room) = self.room() else {
return;
};
let location = Location::new();
if !location.is_available() {
return;
}
if let Err(error) = location.init().await {
self.location_error_toast(error);
return;
}
let dialog = AttachmentDialog::new(&gettext("Your Location"));
let response_fut = dialog.response_future(self);
pin_mut!(response_fut);
let location_stream_fut = location.updates_stream();
pin_mut!(location_stream_fut);
let (mut location_stream, response_fut) =
match future::select(location_stream_fut, response_fut).await {
future::Either::Left((stream_res, response_fut)) => match stream_res {
Ok(stream) => (stream, response_fut),
Err(error) => {
dialog.close();
self.location_error_toast(error);
return;
}
},
future::Either::Right(_) => {
return;
}
};
let mut response_fut_wrapper = Some(response_fut);
let mut geo_uri_wrapper = None;
loop {
let response_fut = response_fut_wrapper.take().unwrap();
match future::select(location_stream.next(), response_fut).await {
future::Either::Left((update, response_fut)) => {
if let Some(uri) = update {
dialog.set_location(&uri);
geo_uri_wrapper.replace(uri);
}
response_fut_wrapper.replace(response_fut);
}
future::Either::Right((response, _)) => {
let stream_drop = TokioDrop::new();
let _ = stream_drop.set(location_stream);
if response == gtk::ResponseType::Ok {
break;
} else {
return;
}
}
};
}
let Some(geo_uri) = geo_uri_wrapper else {
return;
};
let geo_uri_string = geo_uri.to_string();
let timestamp =
glib::DateTime::now_local().expect("Should be able to get the local timestamp");
let location_body = gettext_f(
"User Location {geo_uri} at {iso8601_datetime}",
&[
("geo_uri", &geo_uri_string),
(
"iso8601_datetime",
timestamp.format_iso8601().unwrap().as_str(),
),
],
);
let content = RoomMessageEventContent::new(MessageType::Location(
LocationMessageEventContent::new(location_body, geo_uri_string),
))
.add_mentions(Mentions::default());
let matrix_timeline = room.timeline().matrix_timeline();
let handle = spawn_tokio!(async move { matrix_timeline.send(content.into()).await });
if let Err(error) = handle.await.unwrap() {
error!("Could not send location: {error}");
toast!(self, gettext("Could not send location"))
}
}
fn location_error_toast(&self, error: LocationError) {
let msg = match error {
LocationError::Cancelled => gettext("The location request has been cancelled"),
LocationError::Disabled => gettext("The location services are disabled"),
LocationError::Other => gettext("Could not retrieve current location"),
};
toast!(self, msg);
}
async fn send_attachment(
&self,
bytes: Vec<u8>,
mime: mime::Mime,
body: String,
info: AttachmentInfo,
thumbnail: Option<Thumbnail>,
) {
let Some(room) = self.room() else {
return;
};
let config = if let Some(thumbnail) = thumbnail {
AttachmentConfig::with_thumbnail(thumbnail)
} else {
AttachmentConfig::new()
}
.info(info);
let matrix_room = room.matrix_room().clone();
let handle = spawn_tokio!(async move {
matrix_room
.send_attachment(&body, &mime, bytes, config)
.await
});
if let Err(error) = handle.await.unwrap() {
error!("Could not send file: {error}");
toast!(self, gettext("Could not send file"));
}
}
async fn send_image(&self, image: gdk::Texture) {
if !self.imp().can_send_message() {
return;
}
let filename = filename_for_mime(Some(mime::IMAGE_PNG.as_ref()), None);
let dialog = AttachmentDialog::new(&filename);
dialog.set_image(&image);
if dialog.response_future(self).await != gtk::ResponseType::Ok {
return;
}
let bytes = image.save_to_png_bytes();
let filesize = bytes.len().try_into().ok();
let (mut base_info, thumbnail) = ImageInfoLoader::from(image)
.load_info_and_thumbnail(filesize)
.await;
base_info.size = filesize.map(Into::into);
let info = AttachmentInfo::Image(base_info);
self.send_attachment(bytes.to_vec(), mime::IMAGE_PNG, filename, info, thumbnail)
.await;
}
pub async fn select_file(&self) {
if !self.imp().can_send_message() {
return;
}
let dialog = gtk::FileDialog::builder()
.title(gettext("Select File"))
.modal(true)
.accept_label(gettext("Select"))
.build();
match dialog
.open_future(self.root().and_downcast_ref::<gtk::Window>())
.await
{
Ok(file) => {
self.send_file(file).await;
}
Err(error) => {
if error.matches(gtk::DialogError::Dismissed) {
debug!("File dialog dismissed by user");
} else {
error!("Could not open file: {error:?}");
toast!(self, gettext("Could not open file"));
}
}
};
}
pub async fn send_file(&self, file: gio::File) {
if !self.imp().can_send_message() {
return;
}
let (bytes, file_info) = match load_file(&file).await {
Ok(data) => data,
Err(error) => {
warn!("Could not read file: {error}");
toast!(self, gettext("Error reading file"));
return;
}
};
let dialog = AttachmentDialog::new(&file_info.filename);
dialog.set_file(&file);
if dialog.response_future(self).await != gtk::ResponseType::Ok {
return;
}
let size = file_info.size.map(Into::into);
let (info, thumbnail) = match file_info.mime.type_() {
mime::IMAGE => {
let (mut info, thumbnail) = ImageInfoLoader::from(file)
.load_info_and_thumbnail(file_info.size)
.await;
info.size = size;
(AttachmentInfo::Image(info), thumbnail)
}
mime::VIDEO => {
let (mut info, thumbnail) = load_video_info(&file).await;
info.size = size;
(AttachmentInfo::Video(info), thumbnail)
}
mime::AUDIO => {
let mut info = load_audio_info(&file).await;
info.size = size;
(AttachmentInfo::Audio(info), None)
}
_ => (AttachmentInfo::File(BaseFileInfo { size }), None),
};
self.send_attachment(bytes, file_info.mime, file_info.filename, info, thumbnail)
.await;
}
async fn read_clipboard_file(&self) {
let clipboard = self.clipboard();
let formats = clipboard.formats();
if formats.contains_type(gdk::Texture::static_type()) {
match clipboard
.read_value_future(gdk::Texture::static_type(), glib::Priority::DEFAULT)
.await
{
Ok(value) => match value.get::<gdk::Texture>() {
Ok(texture) => {
self.send_image(texture).await;
return;
}
Err(error) => warn!("Could not get GdkTexture from value: {error:?}"),
},
Err(error) => warn!("Could not get GdkTexture from the clipboard: {error:?}"),
}
toast!(self, gettext("Error getting image from clipboard"));
} else if formats.contains_type(gio::File::static_type()) {
match clipboard
.read_value_future(gio::File::static_type(), glib::Priority::DEFAULT)
.await
{
Ok(value) => match value.get::<gio::File>() {
Ok(file) => {
self.send_file(file).await;
return;
}
Err(error) => warn!("Could not get file from value: {error:?}"),
},
Err(error) => warn!("Could not get file from the clipboard: {error:?}"),
}
toast!(self, gettext("Error getting file from clipboard"));
}
}
#[template_callback]
fn handle_related_event_click(&self) {
if let Some(related_to) = self.current_composer_state().related_to() {
self.activate_action(
"room-history.scroll-to-event",
Some(&related_to.key().to_variant()),
)
.unwrap();
}
}
pub fn handle_paste_action(&self) {
if !self.imp().can_send_message() {
return;
}
spawn!(clone!(
#[weak(rename_to = obj)]
self,
async move {
obj.read_clipboard_file().await;
}
));
}
async fn copy_buffer_selection_to_clipboard(&self) {
let buffer = self.imp().message_entry.buffer();
let Some((start, end)) = buffer.selection_bounds() else {
return;
};
let composer_state = self.current_composer_state();
let body_len = end.offset().saturating_sub(start.offset()) as usize;
let mut body = String::with_capacity(body_len);
let split_message = MessageBufferParser::new(&composer_state, start, end);
for chunk in split_message {
match chunk {
MessageBufferChunk::Text(text) => {
body.push_str(&text);
}
MessageBufferChunk::Mention(source) => {
if let Some(user) = source.downcast_ref::<Member>() {
body.push_str(&user.display_name());
} else if let Some(room) = source.downcast_ref::<Room>() {
body.push_str(
room.aliases()
.alias()
.as_ref()
.map(AsRef::as_ref)
.unwrap_or_else(|| room.room_id().as_ref()),
);
} else if source.is::<AtRoom>() {
body.push_str(AT_ROOM);
} else {
unreachable!()
}
}
}
}
self.clipboard().set_text(&body);
}
fn send_typing_notification(&self, typing: bool) {
let Some(room) = self.room() else {
return;
};
let Some(session) = room.session() else {
return;
};
if !session.settings().typing_enabled() {
return;
}
room.send_typing_notification(typing);
}
}
enum MessageBufferChunk {
Text(String),
Mention(PillSource),
}
enum Mention {
Rich {
name: String,
uri: String,
user_id: Option<OwnedUserId>,
},
AtRoom,
}
impl Mention {
async fn from_source(source: &PillSource) -> Self {
if let Some(user) = source.downcast_ref::<Member>() {
Self::Rich {
name: user.display_name(),
uri: user.matrix_to_uri().to_string(),
user_id: Some(user.user_id().clone()),
}
} else if let Some(room) = source.downcast_ref::<Room>() {
let matrix_to_uri = room.matrix_to_uri().await;
let string_repr = room
.aliases()
.alias_string()
.unwrap_or_else(|| room.room_id_string());
Self::Rich {
name: string_repr,
uri: matrix_to_uri.to_string(),
user_id: None,
}
} else if source.is::<AtRoom>() {
Self::AtRoom
} else {
unreachable!()
}
}
}
struct MessageBufferParser<'a> {
composer_state: &'a ComposerState,
iter: gtk::TextIter,
end: gtk::TextIter,
}
impl<'a> MessageBufferParser<'a> {
fn new(composer_state: &'a ComposerState, start: gtk::TextIter, end: gtk::TextIter) -> Self {
Self {
composer_state,
iter: start,
end,
}
}
}
impl<'a> Iterator for MessageBufferParser<'a> {
type Item = MessageBufferChunk;
fn next(&mut self) -> Option<Self::Item> {
if self.iter == self.end {
return None;
}
if let Some(source) = self
.iter
.child_anchor()
.and_then(|anchor| self.composer_state.widget_at_anchor(&anchor))
.and_then(|widget| widget.downcast::<Pill>().ok())
.and_then(|p| p.source())
{
self.iter.forward_cursor_position();
return Some(MessageBufferChunk::Mention(source));
}
let start = self.iter;
while self.iter.forward_cursor_position() && self.iter != self.end {
if self
.iter
.child_anchor()
.and_then(|anchor| self.composer_state.widget_at_anchor(&anchor))
.and_then(|widget| widget.downcast::<Pill>().ok())
.is_some()
{
break;
}
}
let text = self.iter.buffer().text(&start, &self.iter, false);
if self.iter == self.end && text.is_empty() {
None
} else {
Some(MessageBufferChunk::Text(text.into()))
}
}
}