room-history: Move message toolbar to a separate module
This commit is contained in:
parent
08d10991f5
commit
52c5f51280
16 changed files with 1183 additions and 1002 deletions
|
@ -71,7 +71,6 @@ src/session/view/content/room_details/member_page/member_menu.ui
|
|||
src/session/view/content/room_details/member_page/mod.rs
|
||||
src/session/view/content/room_details/member_page/mod.ui
|
||||
src/session/view/content/room_details/mod.ui
|
||||
src/session/view/content/room_history/attachment_dialog.ui
|
||||
src/session/view/content/room_history/event_actions.ui
|
||||
src/session/view/content/room_history/item_row.rs
|
||||
src/session/view/content/room_history/message_row/audio.rs
|
||||
|
@ -80,6 +79,9 @@ src/session/view/content/room_history/message_row/file.ui
|
|||
src/session/view/content/room_history/message_row/location.rs
|
||||
src/session/view/content/room_history/message_row/media.rs
|
||||
src/session/view/content/room_history/message_row/mod.ui
|
||||
src/session/view/content/room_history/message_toolbar/attachment_dialog.ui
|
||||
src/session/view/content/room_history/message_toolbar/mod.rs
|
||||
src/session/view/content/room_history/message_toolbar/mod.ui
|
||||
src/session/view/content/room_history/mod.rs
|
||||
src/session/view/content/room_history/mod.ui
|
||||
src/session/view/content/room_history/state_row/creation.rs
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use std::fmt;
|
||||
use std::{borrow::Cow, fmt};
|
||||
|
||||
use gtk::{glib, prelude::*, subclass::prelude::*};
|
||||
use indexmap::IndexMap;
|
||||
|
@ -9,7 +9,7 @@ use matrix_sdk_ui::timeline::{
|
|||
use ruma::{
|
||||
events::{receipt::Receipt, room::message::MessageType, AnySyncTimelineEvent},
|
||||
serde::Raw,
|
||||
MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedTransactionId, OwnedUserId,
|
||||
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedTransactionId, OwnedUserId,
|
||||
};
|
||||
|
||||
mod reaction_group;
|
||||
|
@ -41,6 +41,32 @@ impl fmt::Display for EventKey {
|
|||
}
|
||||
}
|
||||
|
||||
impl glib::StaticVariantType for EventKey {
|
||||
fn static_variant_type() -> Cow<'static, glib::VariantTy> {
|
||||
Cow::Borrowed(glib::VariantTy::STRING)
|
||||
}
|
||||
}
|
||||
|
||||
impl glib::ToVariant for EventKey {
|
||||
fn to_variant(&self) -> glib::Variant {
|
||||
self.to_string().to_variant()
|
||||
}
|
||||
}
|
||||
|
||||
impl glib::FromVariant for EventKey {
|
||||
fn from_variant(variant: &glib::Variant) -> Option<Self> {
|
||||
let s = variant.str()?;
|
||||
|
||||
if let Some(s) = s.strip_prefix("transaction_id:") {
|
||||
Some(EventKey::TransactionId(s.into()))
|
||||
} else if let Some(s) = s.strip_prefix("event_id:") {
|
||||
EventId::parse(s).ok().map(EventKey::EventId)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, glib::Boxed)]
|
||||
#[boxed_type(name = "BoxedEventTimelineItem")]
|
||||
pub struct BoxedEventTimelineItem(EventTimelineItem);
|
||||
|
|
|
@ -15,22 +15,21 @@ use crate::{
|
|||
view::EventSourceDialog,
|
||||
},
|
||||
spawn, spawn_tokio, toast,
|
||||
utils::{media::save_to_file, BoundObjectWeakRef},
|
||||
utils::media::save_to_file,
|
||||
};
|
||||
|
||||
mod imp {
|
||||
use std::{cell::RefCell, collections::HashMap, rc::Rc};
|
||||
|
||||
use glib::signal::SignalHandlerId;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ItemRow {
|
||||
pub room_history: BoundObjectWeakRef<RoomHistory>,
|
||||
pub room_history: glib::WeakRef<RoomHistory>,
|
||||
pub message_toolbar_handler: RefCell<Option<glib::SignalHandlerId>>,
|
||||
pub item: RefCell<Option<TimelineItem>>,
|
||||
pub action_group: RefCell<Option<gio::SimpleActionGroup>>,
|
||||
pub notify_handlers: RefCell<Vec<SignalHandlerId>>,
|
||||
pub notify_handlers: RefCell<Vec<glib::SignalHandlerId>>,
|
||||
pub binding: RefCell<Option<glib::Binding>>,
|
||||
pub reaction_chooser: RefCell<Option<ReactionChooser>>,
|
||||
pub emoji_chooser: RefCell<Option<gtk::EmojiChooser>>,
|
||||
|
@ -105,7 +104,11 @@ mod imp {
|
|||
expr_watch.unwatch();
|
||||
}
|
||||
|
||||
self.room_history.disconnect_signals();
|
||||
if let Some(room_history) = self.room_history.upgrade() {
|
||||
if let Some(handler) = self.message_toolbar_handler.take() {
|
||||
room_history.message_toolbar().disconnect(handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -188,25 +191,27 @@ impl ItemRow {
|
|||
|
||||
/// The ancestor room history of this row.
|
||||
pub fn room_history(&self) -> RoomHistory {
|
||||
self.imp().room_history.obj().unwrap()
|
||||
self.imp().room_history.upgrade().unwrap()
|
||||
}
|
||||
|
||||
/// Set the ancestor room history of this row.
|
||||
fn set_room_history(&self, room_history: Option<&RoomHistory>) {
|
||||
let Some(room_history) = room_history else {
|
||||
// Ignore missing `RoomHistory`.
|
||||
return;
|
||||
};
|
||||
|
||||
let related_event_handler = room_history.connect_notify_local(
|
||||
let imp = self.imp();
|
||||
imp.room_history.set(Some(room_history));
|
||||
|
||||
let related_event_handler = room_history.message_toolbar().connect_notify_local(
|
||||
Some("related-event"),
|
||||
clone!(@weak self as obj => move |_, _| {
|
||||
obj.update_for_related_event();
|
||||
clone!(@weak self as obj => move |message_toolbar, _| {
|
||||
obj.update_for_related_event(message_toolbar.related_event());
|
||||
}),
|
||||
);
|
||||
|
||||
self.imp()
|
||||
.room_history
|
||||
.set(room_history, vec![related_event_handler]);
|
||||
imp.message_toolbar_handler
|
||||
.replace(Some(related_event_handler));
|
||||
}
|
||||
|
||||
pub fn action_group(&self) -> Option<gio::SimpleActionGroup> {
|
||||
|
@ -402,8 +407,7 @@ impl ItemRow {
|
|||
}
|
||||
|
||||
/// Update this row for the currently related event.
|
||||
fn update_for_related_event(&self) {
|
||||
let related_event = self.room_history().related_event();
|
||||
fn update_for_related_event(&self, related_event: Option<Event>) {
|
||||
let event = self.item().and_downcast::<Event>();
|
||||
|
||||
if event.is_some() && event == related_event {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
mod audio;
|
||||
pub mod content;
|
||||
mod content;
|
||||
mod file;
|
||||
mod location;
|
||||
mod media;
|
||||
|
@ -17,8 +17,8 @@ use gtk::{
|
|||
use matrix_sdk::ruma::events::room::message::MessageType;
|
||||
use tracing::warn;
|
||||
|
||||
pub use self::content::ContentFormat;
|
||||
use self::{content::MessageContent, media::MessageMedia, reaction_list::MessageReactionList};
|
||||
pub use self::content::{ContentFormat, MessageContent};
|
||||
use self::{media::MessageMedia, reaction_list::MessageReactionList};
|
||||
use super::ReadReceiptsList;
|
||||
use crate::{components::Avatar, prelude::*, session::model::Event, Window};
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ mod imp {
|
|||
|
||||
#[derive(Debug, Default, CompositeTemplate)]
|
||||
#[template(
|
||||
resource = "/org/gnome/Fractal/ui/session/view/content/room_history/completion/completion_popover.ui"
|
||||
resource = "/org/gnome/Fractal/ui/session/view/content/room_history/message_toolbar/completion/completion_popover.ui"
|
||||
)]
|
||||
pub struct CompletionPopover {
|
||||
#[template_child]
|
|
@ -12,7 +12,7 @@ mod imp {
|
|||
|
||||
#[derive(Debug, Default, CompositeTemplate)]
|
||||
#[template(
|
||||
resource = "/org/gnome/Fractal/ui/session/view/content/room_history/completion/completion_row.ui"
|
||||
resource = "/org/gnome/Fractal/ui/session/view/content/room_history/message_toolbar/completion/completion_row.ui"
|
||||
)]
|
||||
pub struct CompletionRow {
|
||||
#[template_child]
|
929
src/session/view/content/room_history/message_toolbar/mod.rs
Normal file
929
src/session/view/content/room_history/message_toolbar/mod.rs
Normal file
|
@ -0,0 +1,929 @@
|
|||
use ashpd::{
|
||||
desktop::location::{Accuracy, LocationProxy},
|
||||
WindowIdentifier,
|
||||
};
|
||||
use futures_util::{FutureExt, StreamExt, TryFutureExt};
|
||||
use geo_uri::GeoUri;
|
||||
use gettextrs::{gettext, pgettext};
|
||||
use gtk::{
|
||||
gdk, gio,
|
||||
glib::{self, clone},
|
||||
prelude::*,
|
||||
subclass::prelude::*,
|
||||
CompositeTemplate,
|
||||
};
|
||||
use matrix_sdk::{
|
||||
attachment::{AttachmentInfo, BaseFileInfo, BaseImageInfo},
|
||||
ruma::events::{
|
||||
room::message::{EmoteMessageEventContent, FormattedBody, MessageType},
|
||||
AnySyncMessageLikeEvent, AnySyncTimelineEvent, SyncMessageLikeEvent,
|
||||
},
|
||||
};
|
||||
use ruma::events::{
|
||||
room::message::{
|
||||
AddMentions, ForwardThread, LocationMessageEventContent, MessageFormat,
|
||||
OriginalSyncRoomMessageEvent, RoomMessageEventContent,
|
||||
},
|
||||
AnyMessageLikeEventContent,
|
||||
};
|
||||
use sourceview::prelude::*;
|
||||
use tracing::{debug, error, warn};
|
||||
|
||||
mod attachment_dialog;
|
||||
mod completion;
|
||||
|
||||
use self::{attachment_dialog::AttachmentDialog, completion::CompletionPopover};
|
||||
use super::message_row::MessageContent;
|
||||
use crate::{
|
||||
components::{CustomEntry, LabelWithWidgets, Pill},
|
||||
gettext_f,
|
||||
prelude::*,
|
||||
session::model::{Event, EventKey, Room},
|
||||
spawn, spawn_tokio, toast,
|
||||
utils::{
|
||||
matrix::extract_mentions,
|
||||
media::{filename_for_mime, get_audio_info, get_image_info, get_video_info, load_file},
|
||||
template_callbacks::TemplateCallbacks,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Default, Hash, Eq, PartialEq, Clone, Copy, glib::Enum)]
|
||||
#[repr(i32)]
|
||||
#[enum_type(name = "RelatedEventType")]
|
||||
pub enum RelatedEventType {
|
||||
#[default]
|
||||
None = 0,
|
||||
Reply = 1,
|
||||
Edit = 2,
|
||||
}
|
||||
|
||||
mod imp {
|
||||
use std::cell::{Cell, RefCell};
|
||||
|
||||
use glib::subclass::InitializingObject;
|
||||
|
||||
use super::*;
|
||||
use crate::Application;
|
||||
|
||||
#[derive(Debug, Default, CompositeTemplate)]
|
||||
#[template(
|
||||
resource = "/org/gnome/Fractal/ui/session/view/content/room_history/message_toolbar/mod.ui"
|
||||
)]
|
||||
pub struct MessageToolbar {
|
||||
pub room: glib::WeakRef<Room>,
|
||||
pub md_enabled: Cell<bool>,
|
||||
pub completion: CompletionPopover,
|
||||
#[template_child]
|
||||
pub message_entry: TemplateChild<sourceview::View>,
|
||||
#[template_child]
|
||||
pub related_event_header: TemplateChild<LabelWithWidgets>,
|
||||
#[template_child]
|
||||
pub related_event_content: TemplateChild<MessageContent>,
|
||||
pub related_event_type: Cell<RelatedEventType>,
|
||||
pub related_event: RefCell<Option<Event>>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for MessageToolbar {
|
||||
const NAME: &'static str = "MessageToolbar";
|
||||
type Type = super::MessageToolbar;
|
||||
type ParentType = gtk::Box;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
CustomEntry::static_type();
|
||||
|
||||
Self::bind_template(klass);
|
||||
Self::Type::bind_template_callbacks(klass);
|
||||
TemplateCallbacks::bind_template_callbacks(klass);
|
||||
|
||||
klass.install_action(
|
||||
"message-toolbar.send-text-message",
|
||||
None,
|
||||
move |widget, _, _| {
|
||||
widget.send_text_message();
|
||||
},
|
||||
);
|
||||
|
||||
klass.install_action("message-toolbar.select-file", None, move |widget, _, _| {
|
||||
spawn!(clone!(@weak widget => async move {
|
||||
widget.select_file().await;
|
||||
}));
|
||||
});
|
||||
|
||||
klass.install_action("message-toolbar.open-emoji", None, move |widget, _, _| {
|
||||
widget.open_emoji();
|
||||
});
|
||||
|
||||
klass.install_action("message-toolbar.send-location", None, move |widget, _, _| {
|
||||
spawn!(clone!(@weak widget => async move {
|
||||
let toast_error = match widget.send_location().await {
|
||||
// Do nothing if the request was cancelled by the user
|
||||
Err(ashpd::Error::Response(ashpd::desktop::ResponseError::Cancelled)) => {
|
||||
error!("Location request was cancelled by the user");
|
||||
Some(gettext("The location request has been cancelled."))
|
||||
},
|
||||
Err(error) => {
|
||||
error!("Failed to send location {error}");
|
||||
Some(gettext("Failed to retrieve current location."))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(message) = toast_error {
|
||||
toast!(widget, message);
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
klass.install_property_action("message-toolbar.markdown", "markdown-enabled");
|
||||
|
||||
klass.install_action(
|
||||
"message-toolbar.clear-related-event",
|
||||
None,
|
||||
move |widget, _, _| widget.clear_related_event(),
|
||||
);
|
||||
}
|
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for MessageToolbar {
|
||||
fn properties() -> &'static [glib::ParamSpec] {
|
||||
use once_cell::sync::Lazy;
|
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
||||
vec![
|
||||
glib::ParamSpecObject::builder::<Room>("room")
|
||||
.explicit_notify()
|
||||
.build(),
|
||||
glib::ParamSpecBoolean::builder("markdown-enabled")
|
||||
.explicit_notify()
|
||||
.build(),
|
||||
glib::ParamSpecEnum::builder::<RelatedEventType>("related-event-type")
|
||||
.read_only()
|
||||
.build(),
|
||||
glib::ParamSpecObject::builder::<Event>("related-event")
|
||||
.read_only()
|
||||
.build(),
|
||||
]
|
||||
});
|
||||
|
||||
PROPERTIES.as_ref()
|
||||
}
|
||||
|
||||
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
|
||||
let obj = self.obj();
|
||||
|
||||
match pspec.name() {
|
||||
"room" => obj.set_room(value.get::<Option<Room>>().unwrap().as_ref()),
|
||||
"markdown-enabled" => obj.set_markdown_enabled(value.get().unwrap()),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||
let obj = self.obj();
|
||||
|
||||
match pspec.name() {
|
||||
"room" => obj.room().to_value(),
|
||||
"markdown-enabled" => obj.markdown_enabled().to_value(),
|
||||
"related-event-type" => obj.related_event_type().to_value(),
|
||||
"related-event" => obj.related_event().to_value(),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
let obj = self.obj();
|
||||
|
||||
// Clipboard.
|
||||
self.message_entry
|
||||
.connect_paste_clipboard(clone!(@weak obj => move |entry| {
|
||||
let formats = obj.clipboard().formats();
|
||||
|
||||
// We only handle files and supported images.
|
||||
if formats.contains_type(gio::File::static_type()) || formats.contains_type(gdk::Texture::static_type()) {
|
||||
entry.stop_signal_emission_by_name("paste-clipboard");
|
||||
spawn!(
|
||||
clone!(@weak obj => async move {
|
||||
obj.read_clipboard().await;
|
||||
}));
|
||||
}
|
||||
}));
|
||||
self.message_entry
|
||||
.connect_copy_clipboard(clone!(@weak obj => move |entry| {
|
||||
entry.stop_signal_emission_by_name("copy-clipboard");
|
||||
obj.copy_buffer_selection_to_clipboard();
|
||||
}));
|
||||
self.message_entry
|
||||
.connect_cut_clipboard(clone!(@weak obj => move |entry| {
|
||||
entry.stop_signal_emission_by_name("cut-clipboard");
|
||||
obj.copy_buffer_selection_to_clipboard();
|
||||
entry.buffer().delete_selection(true, true);
|
||||
}));
|
||||
|
||||
// Key bindings.
|
||||
let key_events = gtk::EventControllerKey::new();
|
||||
key_events
|
||||
.connect_key_pressed(clone!(@weak obj => @default-return glib::Propagation::Proceed, move |_, key, _, modifier| {
|
||||
if modifier.is_empty() && (key == gdk::Key::Return || key == gdk::Key::KP_Enter) {
|
||||
obj.send_text_message();
|
||||
glib::Propagation::Stop
|
||||
} else if modifier.is_empty() && key == gdk::Key::Escape && obj.related_event_type() != RelatedEventType::None {
|
||||
obj.clear_related_event();
|
||||
glib::Propagation::Stop
|
||||
} else {
|
||||
glib::Propagation::Proceed
|
||||
}
|
||||
}));
|
||||
self.message_entry.add_controller(key_events);
|
||||
|
||||
let buffer = self
|
||||
.message_entry
|
||||
.buffer()
|
||||
.downcast::<sourceview::Buffer>()
|
||||
.unwrap();
|
||||
|
||||
crate::utils::sourceview::setup_style_scheme(&buffer);
|
||||
|
||||
// Actions on changes in message entry.
|
||||
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);
|
||||
|
||||
// Markdown highlighting.
|
||||
let md_lang = sourceview::LanguageManager::default().language("markdown");
|
||||
buffer.set_language(md_lang.as_ref());
|
||||
obj.bind_property("markdown-enabled", &buffer, "highlight-syntax")
|
||||
.sync_create()
|
||||
.build();
|
||||
|
||||
let settings = Application::default().settings();
|
||||
settings
|
||||
.bind("markdown-enabled", &*obj, "markdown-enabled")
|
||||
.build();
|
||||
|
||||
// Tab auto-completion.
|
||||
self.completion.set_parent(&*self.message_entry);
|
||||
}
|
||||
|
||||
fn dispose(&self) {
|
||||
self.completion.unparent();
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for MessageToolbar {}
|
||||
impl BoxImpl for MessageToolbar {}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
/// A toolbar with different actions to send messages.
|
||||
pub struct MessageToolbar(ObjectSubclass<imp::MessageToolbar>)
|
||||
@extends gtk::Widget, gtk::Box, @implements gtk::Accessible;
|
||||
}
|
||||
|
||||
#[gtk::template_callbacks]
|
||||
impl MessageToolbar {
|
||||
pub fn new() -> Self {
|
||||
glib::Object::new()
|
||||
}
|
||||
|
||||
/// The room to send messages in.
|
||||
pub fn room(&self) -> Option<Room> {
|
||||
self.imp().room.upgrade()
|
||||
}
|
||||
|
||||
/// Set the room currently displayed.
|
||||
pub fn set_room(&self, room: Option<&Room>) {
|
||||
if self.room().as_ref() == room {
|
||||
return;
|
||||
}
|
||||
|
||||
let imp = self.imp();
|
||||
self.clear_related_event();
|
||||
|
||||
imp.room.set(room);
|
||||
|
||||
self.update_completion(room);
|
||||
imp.message_entry.grab_focus();
|
||||
|
||||
self.notify("room");
|
||||
}
|
||||
|
||||
/// Whether outgoing messages should be interpreted as markdown.
|
||||
pub fn markdown_enabled(&self) -> bool {
|
||||
self.imp().md_enabled.get()
|
||||
}
|
||||
|
||||
/// Set whether outgoing messages should be interpreted as markdown.
|
||||
pub fn set_markdown_enabled(&self, enabled: bool) {
|
||||
let imp = self.imp();
|
||||
|
||||
imp.md_enabled.set(enabled);
|
||||
|
||||
self.notify("markdown-enabled");
|
||||
}
|
||||
|
||||
/// The type of related event of the composer.
|
||||
pub fn related_event_type(&self) -> RelatedEventType {
|
||||
self.imp().related_event_type.get()
|
||||
}
|
||||
|
||||
/// Set the type of related event of the composer.
|
||||
fn set_related_event_type(&self, related_type: RelatedEventType) {
|
||||
if self.related_event_type() == related_type {
|
||||
return;
|
||||
}
|
||||
|
||||
self.imp().related_event_type.set(related_type);
|
||||
self.notify("related-event-type");
|
||||
}
|
||||
|
||||
/// The related event of the composer.
|
||||
pub fn related_event(&self) -> Option<Event> {
|
||||
self.imp().related_event.borrow().clone()
|
||||
}
|
||||
|
||||
/// Set the related event of the composer.
|
||||
fn set_related_event(&self, event: Option<Event>) {
|
||||
// We shouldn't reply to events that are not sent yet.
|
||||
if let Some(event) = &event {
|
||||
if event.event_id().is_none() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let prev_event = self.related_event();
|
||||
|
||||
if prev_event == event {
|
||||
return;
|
||||
}
|
||||
|
||||
self.imp().related_event.replace(event);
|
||||
self.notify("related-event");
|
||||
}
|
||||
|
||||
pub fn clear_related_event(&self) {
|
||||
if self.related_event_type() == RelatedEventType::Edit {
|
||||
// Clean up the entry.
|
||||
self.imp().message_entry.buffer().set_text("");
|
||||
};
|
||||
|
||||
self.set_related_event(None);
|
||||
self.set_related_event_type(RelatedEventType::default());
|
||||
}
|
||||
|
||||
pub fn set_reply_to(&self, event: Event) {
|
||||
let imp = self.imp();
|
||||
imp.related_event_header
|
||||
.set_widgets(vec![Pill::for_user(event.sender().upcast_ref())]);
|
||||
imp.related_event_header
|
||||
// Translators: Do NOT translate the content between '{' and '}',
|
||||
// this is a variable name. In this string, 'Reply' is a noun.
|
||||
.set_label(Some(gettext_f("Reply to {user}", &[("user", "<widget>")])));
|
||||
|
||||
imp.related_event_content.update_for_event(&event);
|
||||
imp.related_event_content.set_visible(true);
|
||||
|
||||
self.set_related_event_type(RelatedEventType::Reply);
|
||||
self.set_related_event(Some(event));
|
||||
imp.message_entry.grab_focus();
|
||||
}
|
||||
|
||||
/// Set the event to edit.
|
||||
pub fn set_edit(&self, event: Event) {
|
||||
// We don't support editing non-text messages.
|
||||
let Some((text, formatted)) = event.message().and_then(|msg| match msg {
|
||||
MessageType::Emote(emote) => Some((format!("/me {}", emote.body), emote.formatted)),
|
||||
MessageType::Text(text) => Some((text.body, text.formatted)),
|
||||
_ => None,
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mentions = if let Some(html) =
|
||||
formatted.and_then(|f| (f.format == MessageFormat::Html).then_some(f.body))
|
||||
{
|
||||
let (_, mentions) = extract_mentions(&html, &event.room());
|
||||
let mut pos = 0;
|
||||
// This is looking for the mention link's inner text in the Markdown
|
||||
// so it is not super reliable: if there is other text that matches
|
||||
// a user's display name in the string it might be replaced instead
|
||||
// of the actual mention.
|
||||
// Short of an HTML to Markdown converter, it won't be a simple task
|
||||
// to locate mentions in Markdown.
|
||||
mentions
|
||||
.into_iter()
|
||||
.filter_map(|(pill, s)| {
|
||||
text[pos..].find(&s).map(|index| {
|
||||
let start = pos + index;
|
||||
let end = start + s.len();
|
||||
pos = end;
|
||||
(pill, (start, end))
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let imp = self.imp();
|
||||
imp.related_event_header.set_widgets::<gtk::Widget>(vec![]);
|
||||
imp.related_event_header
|
||||
// Translators: In this string, 'Edit' is a noun.
|
||||
.set_label(Some(pgettext("room-history", "Edit")));
|
||||
|
||||
imp.related_event_content.set_visible(false);
|
||||
|
||||
self.set_related_event_type(RelatedEventType::Edit);
|
||||
self.set_related_event(Some(event));
|
||||
|
||||
let view = &*imp.message_entry;
|
||||
let buffer = view.buffer();
|
||||
|
||||
if mentions.is_empty() {
|
||||
buffer.set_text(&text);
|
||||
} else {
|
||||
// Place the pills instead of the text at the appropriate places in
|
||||
// the TextView.
|
||||
buffer.set_text("");
|
||||
|
||||
let mut pos = 0;
|
||||
let mut iter = buffer.iter_at_offset(0);
|
||||
|
||||
for (pill, (start, end)) in mentions {
|
||||
if pos != start {
|
||||
buffer.insert(&mut iter, &text[pos..start]);
|
||||
}
|
||||
|
||||
let anchor = buffer.create_child_anchor(&mut iter);
|
||||
view.add_child_at_anchor(&pill, &anchor);
|
||||
|
||||
pos = end;
|
||||
}
|
||||
|
||||
if pos != text.len() {
|
||||
buffer.insert(&mut iter, &text[pos..])
|
||||
}
|
||||
}
|
||||
|
||||
imp.message_entry.grab_focus();
|
||||
}
|
||||
|
||||
/// Get an iterator over chunks of the message entry's text between the
|
||||
/// given start and end, split by mentions.
|
||||
fn split_buffer_mentions(&self, start: gtk::TextIter, end: gtk::TextIter) -> SplitMentions {
|
||||
SplitMentions { iter: start, end }
|
||||
}
|
||||
|
||||
fn send_text_message(&self) {
|
||||
let Some(room) = self.room() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let imp = self.imp();
|
||||
let buffer = imp.message_entry.buffer();
|
||||
let (start_iter, end_iter) = buffer.bounds();
|
||||
let body_len = buffer.text(&start_iter, &end_iter, true).len();
|
||||
|
||||
let is_markdown = imp.md_enabled.get();
|
||||
let mut has_mentions = false;
|
||||
let mut plain_body = String::with_capacity(body_len);
|
||||
// formatted_body is Markdown if is_markdown is true, and HTML if false.
|
||||
let mut formatted_body = String::with_capacity(body_len);
|
||||
|
||||
for chunk in self.split_buffer_mentions(start_iter, end_iter) {
|
||||
match chunk {
|
||||
MentionChunk::Text(text) => {
|
||||
plain_body.push_str(&text);
|
||||
formatted_body.push_str(&text);
|
||||
}
|
||||
MentionChunk::Mention { name, uri } => {
|
||||
has_mentions = true;
|
||||
plain_body.push_str(&name);
|
||||
formatted_body.push_str(&if is_markdown {
|
||||
format!("[{name}]({uri})")
|
||||
} else {
|
||||
format!("<a href=\"{uri}\">{name}</a>")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let is_emote = plain_body.starts_with("/me ");
|
||||
if is_emote {
|
||||
plain_body.replace_range(.."/me ".len(), "");
|
||||
formatted_body.replace_range(.."/me ".len(), "");
|
||||
}
|
||||
|
||||
let html_body = if is_markdown {
|
||||
FormattedBody::markdown(formatted_body).map(|b| b.body)
|
||||
} else if has_mentions {
|
||||
// Already formatted with HTML
|
||||
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 {
|
||||
let mut content = if let Some(html_body) = html_body {
|
||||
RoomMessageEventContent::text_html(plain_body, html_body)
|
||||
} else {
|
||||
RoomMessageEventContent::text_plain(plain_body)
|
||||
};
|
||||
|
||||
if self.related_event_type() == RelatedEventType::Reply {
|
||||
let related_event = self
|
||||
.related_event()
|
||||
.unwrap()
|
||||
.raw()
|
||||
.unwrap()
|
||||
.deserialize()
|
||||
.unwrap();
|
||||
if let AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
|
||||
SyncMessageLikeEvent::Original(related_message_event),
|
||||
)) = related_event
|
||||
{
|
||||
let full_related_message_event = related_message_event
|
||||
.into_full_event(self.room().unwrap().room_id().to_owned());
|
||||
content = content.make_reply_to(
|
||||
&full_related_message_event,
|
||||
ForwardThread::Yes,
|
||||
AddMentions::No,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
content
|
||||
};
|
||||
|
||||
// Handle edit.
|
||||
if self.related_event_type() == RelatedEventType::Edit {
|
||||
let related_event = self.related_event().unwrap();
|
||||
let related_message = related_event
|
||||
.raw()
|
||||
.unwrap()
|
||||
.deserialize_as::<OriginalSyncRoomMessageEvent>()
|
||||
.unwrap();
|
||||
|
||||
// Try to get the replied to message of the original event if it's available
|
||||
// locally.
|
||||
let replied_to_message = related_event
|
||||
.reply_to_id()
|
||||
.and_then(|id| room.timeline().event_by_key(&EventKey::EventId(id)))
|
||||
.and_then(|e| e.raw())
|
||||
.and_then(|r| r.deserialize_as::<OriginalSyncRoomMessageEvent>().ok())
|
||||
.map(|e| e.into_full_event(room.room_id().to_owned()));
|
||||
|
||||
content = content.make_replacement(&related_message, replied_to_message.as_ref());
|
||||
}
|
||||
|
||||
room.send_room_message_event(content);
|
||||
buffer.set_text("");
|
||||
self.clear_related_event();
|
||||
}
|
||||
|
||||
fn open_emoji(&self) {
|
||||
self.imp().message_entry.emit_insert_emoji();
|
||||
}
|
||||
|
||||
async fn send_location(&self) -> ashpd::Result<()> {
|
||||
let Some(room) = self.room() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let handle = spawn_tokio!(async move {
|
||||
let proxy = LocationProxy::new().await?;
|
||||
let identifier = WindowIdentifier::default();
|
||||
|
||||
let session = proxy
|
||||
.create_session(Some(0), Some(0), Some(Accuracy::Exact))
|
||||
.await?;
|
||||
|
||||
// We want to be listening for new locations whenever the session is up
|
||||
// otherwise we might lose the first response and will have to wait for a future
|
||||
// update by geoclue
|
||||
// FIXME: We should update the location on the map according to updates received
|
||||
// by the proxy.
|
||||
let mut stream = proxy.receive_location_updated().await?;
|
||||
let (_, location) = futures_util::try_join!(
|
||||
proxy.start(&session, &identifier).into_future(),
|
||||
stream.next().map(|l| l.ok_or(ashpd::Error::NoResponse))
|
||||
)?;
|
||||
|
||||
ashpd::Result::Ok(location)
|
||||
});
|
||||
|
||||
let location = handle.await.unwrap()?;
|
||||
let geo_uri = GeoUri::builder()
|
||||
.latitude(location.latitude())
|
||||
.longitude(location.longitude())
|
||||
.build()
|
||||
.expect("Got invalid coordinates from ashpd");
|
||||
|
||||
let window = self.root().and_downcast::<gtk::Window>().unwrap();
|
||||
let dialog = AttachmentDialog::for_location(&window, &gettext("Your Location"), &geo_uri);
|
||||
if dialog.run_future().await != gtk::ResponseType::Ok {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let geo_uri_string = geo_uri.to_string();
|
||||
let iso8601_datetime =
|
||||
glib::DateTime::from_unix_local(location.timestamp().as_secs() as i64)
|
||||
.expect("Valid location timestamp");
|
||||
let location_body = gettext_f(
|
||||
// Translators: Do NOT translate the content between '{' and '}', this is a variable
|
||||
// name.
|
||||
"User Location {geo_uri} at {iso8601_datetime}",
|
||||
&[
|
||||
("geo_uri", &geo_uri_string),
|
||||
(
|
||||
"iso8601_datetime",
|
||||
iso8601_datetime.format_iso8601().unwrap().as_str(),
|
||||
),
|
||||
],
|
||||
);
|
||||
room.send_room_message_event(AnyMessageLikeEventContent::RoomMessage(
|
||||
RoomMessageEventContent::new(MessageType::Location(LocationMessageEventContent::new(
|
||||
location_body,
|
||||
geo_uri_string,
|
||||
))),
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_image(&self, image: gdk::Texture) {
|
||||
let window = self.root().and_downcast::<gtk::Window>().unwrap();
|
||||
let filename = filename_for_mime(Some(mime::IMAGE_PNG.as_ref()), None);
|
||||
let dialog = AttachmentDialog::for_image(&window, &filename, &image);
|
||||
|
||||
if dialog.run_future().await != gtk::ResponseType::Ok {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(room) = self.room() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let bytes = image.save_to_png_bytes();
|
||||
let info = AttachmentInfo::Image(BaseImageInfo {
|
||||
width: Some((image.width() as u32).into()),
|
||||
height: Some((image.height() as u32).into()),
|
||||
size: Some((bytes.len() as u32).into()),
|
||||
blurhash: None,
|
||||
});
|
||||
|
||||
room.send_attachment(bytes.to_vec(), mime::IMAGE_PNG, &filename, info);
|
||||
}
|
||||
|
||||
pub async fn select_file(&self) {
|
||||
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) {
|
||||
match load_file(&file).await {
|
||||
Ok((bytes, file_info)) => {
|
||||
let window = self.root().and_downcast::<gtk::Window>().unwrap();
|
||||
let dialog = AttachmentDialog::for_file(&window, &file_info.filename, &file);
|
||||
|
||||
if dialog.run_future().await != gtk::ResponseType::Ok {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(room) = self.room() else {
|
||||
error!("Cannot send file without a room");
|
||||
return;
|
||||
};
|
||||
|
||||
let size = file_info.size.map(Into::into);
|
||||
let info = match file_info.mime.type_() {
|
||||
mime::IMAGE => {
|
||||
let mut info = get_image_info(&file).await;
|
||||
info.size = size;
|
||||
AttachmentInfo::Image(info)
|
||||
}
|
||||
mime::VIDEO => {
|
||||
let mut info = get_video_info(&file).await;
|
||||
info.size = size;
|
||||
AttachmentInfo::Video(info)
|
||||
}
|
||||
mime::AUDIO => {
|
||||
let mut info = get_audio_info(&file).await;
|
||||
info.size = size;
|
||||
AttachmentInfo::Audio(info)
|
||||
}
|
||||
_ => AttachmentInfo::File(BaseFileInfo { size }),
|
||||
};
|
||||
|
||||
room.send_attachment(bytes, file_info.mime, &file_info.filename, info);
|
||||
}
|
||||
Err(error) => {
|
||||
warn!("Could not read file: {error}");
|
||||
toast!(self, gettext("Error reading file"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_clipboard(&self) {
|
||||
let clipboard = self.clipboard();
|
||||
let formats = clipboard.formats();
|
||||
|
||||
if formats.contains_type(gdk::Texture::static_type()) {
|
||||
// There is an image in the clipboard.
|
||||
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()) {
|
||||
// There is a file in the clipboard.
|
||||
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(event) = &*self.imp().related_event.borrow() {
|
||||
self.activate_action(
|
||||
"room-history.scroll-to-event",
|
||||
Some(&event.key().to_variant()),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_paste_action(&self) {
|
||||
spawn!(glib::clone!(@weak self as obj => async move {
|
||||
obj.read_clipboard().await;
|
||||
}));
|
||||
}
|
||||
|
||||
// Update the completion for the current room.
|
||||
fn update_completion(&self, room: Option<&Room>) {
|
||||
let completion = &self.imp().completion;
|
||||
|
||||
completion
|
||||
.set_user_id(room.and_then(|r| r.session().user().map(|u| u.user_id().to_string())));
|
||||
// `RoomHistory` should have a strong reference to the list so we can use
|
||||
// `get_or_create_members()`.
|
||||
completion.set_members(room.map(|r| r.get_or_create_members()));
|
||||
}
|
||||
|
||||
// Copy the selection in the message entry to the clipboard while replacing
|
||||
// mentions.
|
||||
fn copy_buffer_selection_to_clipboard(&self) {
|
||||
if let Some((start, end)) = self.imp().message_entry.buffer().selection_bounds() {
|
||||
let content: String = self
|
||||
.split_buffer_mentions(start, end)
|
||||
.map(|chunk| match chunk {
|
||||
MentionChunk::Text(str) => str,
|
||||
MentionChunk::Mention { name, .. } => name,
|
||||
})
|
||||
.collect();
|
||||
self.clipboard().set_text(&content);
|
||||
}
|
||||
}
|
||||
|
||||
fn send_typing_notification(&self, typing: bool) {
|
||||
if let Some(room) = self.room() {
|
||||
room.send_typing_notification(typing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum MentionChunk {
|
||||
Text(String),
|
||||
Mention { name: String, uri: String },
|
||||
}
|
||||
|
||||
struct SplitMentions {
|
||||
iter: gtk::TextIter,
|
||||
end: gtk::TextIter,
|
||||
}
|
||||
|
||||
impl Iterator for SplitMentions {
|
||||
type Item = MentionChunk;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.iter == self.end {
|
||||
// We reached the end.
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(pill) = self
|
||||
.iter
|
||||
.child_anchor()
|
||||
.map(|anchor| anchor.widgets())
|
||||
.as_ref()
|
||||
.and_then(|widgets| widgets.first())
|
||||
.and_then(|widget| widget.downcast_ref::<Pill>())
|
||||
{
|
||||
// This chunk is a mention.
|
||||
let (name, uri) = if let Some(user) = pill.user() {
|
||||
(
|
||||
user.display_name(),
|
||||
user.user_id().matrix_to_uri().to_string(),
|
||||
)
|
||||
} else if let Some(room) = pill.room() {
|
||||
(
|
||||
room.display_name(),
|
||||
room.room_id().matrix_to_uri().to_string(),
|
||||
)
|
||||
} else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
self.iter.forward_cursor_position();
|
||||
|
||||
return Some(MentionChunk::Mention { name, uri });
|
||||
}
|
||||
|
||||
// This chunk is not a mention. Go forward until the next mention or the
|
||||
// end and return the text in between.
|
||||
let start = self.iter;
|
||||
while self.iter.forward_cursor_position() && self.iter != self.end {
|
||||
if self
|
||||
.iter
|
||||
.child_anchor()
|
||||
.map(|anchor| anchor.widgets())
|
||||
.as_ref()
|
||||
.and_then(|widgets| widgets.first())
|
||||
.and_then(|widget| widget.downcast_ref::<Pill>())
|
||||
.is_some()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let text = self.iter.buffer().text(&start, &self.iter, false);
|
||||
// We might somehow have an empty string before the end, or at the end,
|
||||
// because of hidden `char`s in the buffer, so we must only return
|
||||
// `None` when we have an empty string at the end.
|
||||
if self.iter == self.end && text.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(MentionChunk::Text(text.into()))
|
||||
}
|
||||
}
|
||||
}
|
153
src/session/view/content/room_history/message_toolbar/mod.ui
Normal file
153
src/session/view/content/room_history/message_toolbar/mod.ui
Normal file
|
@ -0,0 +1,153 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<menu id="message-menu-model">
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">_Location</attribute>
|
||||
<attribute name="action">message-toolbar.send-location</attribute>
|
||||
<attribute name="icon">map-marker-symbolic</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">_Markdown</attribute>
|
||||
<attribute name="action">message-toolbar.markdown</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</menu>
|
||||
<template class="MessageToolbar" parent="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="related_event_toolbar">
|
||||
<style>
|
||||
<class name="related-event-toolbar"/>
|
||||
</style>
|
||||
<property name="spacing">12</property>
|
||||
<binding name="visible">
|
||||
<closure type="gboolean" function="object_is_some">
|
||||
<lookup name="related-event">MessageToolbar</lookup>
|
||||
</closure>
|
||||
</binding>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="margin-bottom">6</property>
|
||||
<property name="margin-top">8</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="LabelWithWidgets" id="related_event_header">
|
||||
<style>
|
||||
<class name="heading"/>
|
||||
</style>
|
||||
<property name="valign">center</property>
|
||||
<property name="hexpand">true</property>
|
||||
<property name="margin-top">2</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="ContentMessageContent" id="related_event_content">
|
||||
<style>
|
||||
<class name="related-event-content"/>
|
||||
<class name="dim-label"/>
|
||||
</style>
|
||||
<property name="format">ellipsized</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkGestureClick">
|
||||
<signal name="pressed" handler="handle_related_event_click" swapped="yes"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton">
|
||||
<style>
|
||||
<class name="circular"/>
|
||||
</style>
|
||||
<property name="halign">end</property>
|
||||
<property name="valign">start</property>
|
||||
<property name="icon-name">window-close-symbolic</property>
|
||||
<property name="action-name">message-toolbar.clear-related-event</property>
|
||||
<property name="tooltip-text" translatable="yes">Cancel</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<style>
|
||||
<class name="toolbar"/>
|
||||
</style>
|
||||
<child>
|
||||
<object class="GtkButton">
|
||||
<property name="valign">end</property>
|
||||
<property name="icon-name">mail-attachment-symbolic</property>
|
||||
<property name="action-name">message-toolbar.select-file</property>
|
||||
<accessibility>
|
||||
<property name="label" translatable="yes">Browse Attachment</property>
|
||||
</accessibility>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton">
|
||||
<property name="valign">end</property>
|
||||
<property name="icon-name">emoji-people-symbolic</property>
|
||||
<property name="action-name">message-toolbar.open-emoji</property>
|
||||
<accessibility>
|
||||
<property name="label" translatable="yes">Open Emoji Picker</property>
|
||||
</accessibility>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="CustomEntry">
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="vexpand">True</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="vscrollbar-policy">external</property>
|
||||
<property name="max-content-height">200</property>
|
||||
<property name="propagate-natural-height">True</property>
|
||||
<property name="child">
|
||||
<object class="GtkSourceView" id="message_entry">
|
||||
<property name="hexpand">True</property>
|
||||
<property name="accepts-tab">False</property>
|
||||
<property name="top-margin">7</property>
|
||||
<property name="bottom-margin">7</property>
|
||||
<property name="wrap-mode">word</property>
|
||||
<accessibility>
|
||||
<property name="label" translatable="yes">Message Entry</property>
|
||||
</accessibility>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuButton">
|
||||
<property name="valign">end</property>
|
||||
<property name="direction">up</property>
|
||||
<property name="icon-name">view-more-horizontal-symbolic</property>
|
||||
<property name="menu-model">message-menu-model</property>
|
||||
<accessibility>
|
||||
<property name="label" translatable="yes">Open Message Menu</property>
|
||||
</accessibility>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton">
|
||||
<property name="valign">end</property>
|
||||
<property name="icon-name">send-symbolic</property>
|
||||
<property name="focus-on-click">False</property>
|
||||
<property name="action-name">message-toolbar.send-text-message</property>
|
||||
<style>
|
||||
<class name="suggested-action"/>
|
||||
<class name="circular"/>
|
||||
</style>
|
||||
<accessibility>
|
||||
<property name="label" translatable="yes">Send Message</property>
|
||||
</accessibility>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</template>
|
||||
</interface>
|
File diff suppressed because it is too large
Load diff
|
@ -25,19 +25,6 @@
|
|||
</item>
|
||||
</section>
|
||||
</menu>
|
||||
<menu id="message-menu-model">
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">_Location</attribute>
|
||||
<attribute name="action">room-history.send-location</attribute>
|
||||
<attribute name="icon">map-marker-symbolic</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">_Markdown</attribute>
|
||||
<attribute name="action">room-history.markdown</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</menu>
|
||||
<template class="ContentRoomHistory" parent="AdwBin">
|
||||
<property name="vexpand">True</property>
|
||||
<property name="hexpand">True</property>
|
||||
|
@ -192,142 +179,8 @@
|
|||
<property name="maximum-size">750</property>
|
||||
<property name="tightening-threshold">550</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="related_event_toolbar">
|
||||
<style>
|
||||
<class name="related-event-toolbar"/>
|
||||
</style>
|
||||
<property name="spacing">12</property>
|
||||
<binding name="visible">
|
||||
<closure type="gboolean" function="object_is_some">
|
||||
<lookup name="related-event">ContentRoomHistory</lookup>
|
||||
</closure>
|
||||
</binding>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="margin-bottom">6</property>
|
||||
<property name="margin-top">8</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="LabelWithWidgets" id="related_event_header">
|
||||
<style>
|
||||
<class name="heading"/>
|
||||
</style>
|
||||
<property name="valign">center</property>
|
||||
<property name="hexpand">true</property>
|
||||
<property name="margin-top">2</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="ContentMessageContent" id="related_event_content">
|
||||
<style>
|
||||
<class name="related-event-content"/>
|
||||
<class name="dim-label"/>
|
||||
</style>
|
||||
<property name="format">ellipsized</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkGestureClick">
|
||||
<signal name="pressed" handler="handle_related_event_click" swapped="yes"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton">
|
||||
<style>
|
||||
<class name="circular"/>
|
||||
</style>
|
||||
<property name="halign">end</property>
|
||||
<property name="valign">start</property>
|
||||
<property name="icon-name">window-close-symbolic</property>
|
||||
<property name="action-name">room-history.clear-related-event</property>
|
||||
<property name="tooltip-text" translatable="yes">Cancel</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<style>
|
||||
<class name="toolbar"/>
|
||||
</style>
|
||||
<child>
|
||||
<object class="GtkButton">
|
||||
<property name="valign">end</property>
|
||||
<property name="icon-name">mail-attachment-symbolic</property>
|
||||
<property name="action-name">room-history.select-file</property>
|
||||
<accessibility>
|
||||
<property name="label" translatable="yes">Browse Attachment</property>
|
||||
</accessibility>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton">
|
||||
<property name="valign">end</property>
|
||||
<property name="icon-name">emoji-people-symbolic</property>
|
||||
<property name="action-name">room-history.open-emoji</property>
|
||||
<accessibility>
|
||||
<property name="label" translatable="yes">Open Emoji Picker</property>
|
||||
</accessibility>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="CustomEntry">
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="vexpand">True</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="vscrollbar-policy">external</property>
|
||||
<property name="max-content-height">200</property>
|
||||
<property name="propagate-natural-height">True</property>
|
||||
<property name="child">
|
||||
<object class="GtkSourceView" id="message_entry">
|
||||
<property name="hexpand">True</property>
|
||||
<property name="accepts-tab">False</property>
|
||||
<property name="top-margin">7</property>
|
||||
<property name="bottom-margin">7</property>
|
||||
<property name="wrap-mode">word</property>
|
||||
<accessibility>
|
||||
<property name="label" translatable="yes">Message Entry</property>
|
||||
</accessibility>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuButton">
|
||||
<property name="valign">end</property>
|
||||
<property name="direction">up</property>
|
||||
<property name="icon-name">view-more-horizontal-symbolic</property>
|
||||
<property name="menu-model">message-menu-model</property>
|
||||
<accessibility>
|
||||
<property name="label" translatable="yes">Open Message Menu</property>
|
||||
</accessibility>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton">
|
||||
<property name="valign">end</property>
|
||||
<property name="icon-name">send-symbolic</property>
|
||||
<property name="focus-on-click">False</property>
|
||||
<property name="action-name">room-history.send-text-message</property>
|
||||
<style>
|
||||
<class name="suggested-action"/>
|
||||
<class name="circular"/>
|
||||
</style>
|
||||
<accessibility>
|
||||
<property name="label" translatable="yes">Send Message</property>
|
||||
</accessibility>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<object class="MessageToolbar" id="message_toolbar">
|
||||
<property name="room" bind-source="ContentRoomHistory" bind-property="room" bind-flags="sync-create" />
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
|
|
|
@ -60,9 +60,6 @@
|
|||
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_details/member_page/members_list_view/mod.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_details/member_page/mod.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_details/mod.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/attachment_dialog.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/completion/completion_popover.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/completion/completion_row.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/divider_row.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/event_actions.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/message_row/audio.ui</file>
|
||||
|
@ -73,6 +70,10 @@
|
|||
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/message_row/reaction.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/message_row/reaction_list.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/message_row/reply.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/message_toolbar/attachment_dialog.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/message_toolbar/completion/completion_popover.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/message_toolbar/completion/completion_row.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/message_toolbar/mod.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/mod.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/read_receipts_list.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/state_row/creation.ui</file>
|
||||
|
|
Loading…
Reference in a new issue