From b3888f48cbd25d4ac77d989af94e9b22a77eb08c Mon Sep 17 00:00:00 2001 From: Julian Schmidhuber Date: Mon, 18 May 2026 18:33:30 +0200 Subject: [PATCH] refactor(): Migrate to the new flare-backend See https://gitlab.com/schmiddi-on-mobile/flare-backend. --- CHANGELOG.md | 4 + Cargo.lock | 81 +- Cargo.toml | 12 +- data/resources/ui/channel_item.blp | 2 +- data/resources/ui/channel_item_compact.blp | 1 + data/resources/ui/channel_messages.blp | 4 +- .../ui/components/attachment_image.blp | 29 +- data/resources/ui/message_item.blp | 66 +- po/POTFILES | 13 +- src/backend/attachment.rs | 82 +- src/backend/channel.rs | 621 ++------ src/backend/contact.rs | 265 +--- src/backend/device_info.rs | 2 +- src/backend/dummy.rs | 817 +++++------ src/backend/manager.rs | 1264 ++++++++--------- src/backend/manager_thread.rs | 812 ----------- src/backend/message.rs | 464 ++++++ src/backend/message/call_message.rs | 121 -- src/backend/message/deletion_message.rs | 65 - src/backend/message/display_message.rs | 196 --- src/backend/message/mod.rs | 495 ------- src/backend/message/reaction_message.rs | 131 -- src/backend/message/text_message.rs | 449 ------ src/backend/mod.rs | 4 +- src/backend/server.rs | 4 +- src/backend/{timeline/mod.rs => timeline.rs} | 41 +- src/backend/timeline/timeline_item.rs | 158 --- src/error.rs | 136 +- src/gui/call_message_item.rs | 80 -- src/gui/channel_item.rs | 7 +- src/gui/channel_item_compact.rs | 21 +- src/gui/channel_list.rs | 2 +- src/gui/channel_messages.rs | 130 +- src/gui/components/item_row.rs | 70 +- src/gui/components/time_divider.rs | 6 +- src/gui/linked_devices_window.rs | 2 +- src/gui/message_item.rs | 102 +- src/gui/mod.rs | 1 - src/gui/setup_window.rs | 27 +- src/gui/utility.rs | 2 +- src/gui/window.rs | 32 +- src/main.rs | 1 - src/utils.rs | 10 - 43 files changed, 1992 insertions(+), 4840 deletions(-) delete mode 100644 src/backend/manager_thread.rs create mode 100644 src/backend/message.rs delete mode 100644 src/backend/message/call_message.rs delete mode 100644 src/backend/message/deletion_message.rs delete mode 100644 src/backend/message/display_message.rs delete mode 100644 src/backend/message/mod.rs delete mode 100644 src/backend/message/reaction_message.rs delete mode 100644 src/backend/message/text_message.rs rename src/backend/{timeline/mod.rs => timeline.rs} (90%) delete mode 100644 src/backend/timeline/timeline_item.rs delete mode 100644 src/gui/call_message_item.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 3253d48..571c99b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Rewritten backend, which should make maintenance as well as adding new features to Flare easier in the future. + ## [0.20.6] - 2026-06-09 ### Fixed diff --git a/Cargo.lock b/Cargo.lock index ad40d39..6212b75 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -266,6 +266,28 @@ dependencies = [ "syn", ] +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -1108,9 +1130,13 @@ name = "flare" version = "0.20.6" dependencies = [ "ashpd 0.12.3", + "async-stream", "blurhash", "chrono", "env_logger", + "flare-manager", + "flare-store", + "flare-types", "futures", "gettext-rs", "gio", @@ -1118,14 +1144,11 @@ dependencies = [ "image", "lazy_static", "libadwaita", - "libsignal-service", "libspelling", "log", "once_cell", "oo7", "phonenumber", - "presage", - "presage-store-sqlite", "qrcode-generator", "rand 0.10.1", "regex", @@ -1133,6 +1156,54 @@ dependencies = [ "tokio", "tracing", "url", + "uuid", +] + +[[package]] +name = "flare-manager" +version = "0.1.0" +source = "git+https://gitlab.com/schmiddi-on-mobile/flare-backend.git?rev=70097933a0c9c32aa49831c8fc734d68ce46aab2#70097933a0c9c32aa49831c8fc734d68ce46aab2" +dependencies = [ + "chrono", + "flare-types", + "futures", + "libsignal-service", + "log", + "phonenumber", + "presage", + "presage-store-sqlite", + "tokio", + "url", +] + +[[package]] +name = "flare-store" +version = "0.1.0" +source = "git+https://gitlab.com/schmiddi-on-mobile/flare-backend.git?rev=70097933a0c9c32aa49831c8fc734d68ce46aab2#70097933a0c9c32aa49831c8fc734d68ce46aab2" +dependencies = [ + "async-stream", + "chrono", + "flare-manager", + "flare-types", + "futures", + "log", + "presage", + "presage-store-sqlite", +] + +[[package]] +name = "flare-types" +version = "0.1.0" +source = "git+https://gitlab.com/schmiddi-on-mobile/flare-backend.git?rev=70097933a0c9c32aa49831c8fc734d68ce46aab2#70097933a0c9c32aa49831c8fc734d68ce46aab2" +dependencies = [ + "chrono", + "futures", + "hex", + "libsignal-service", + "log", + "phonenumber", + "presage", + "uuid", ] [[package]] @@ -3360,7 +3431,7 @@ dependencies = [ [[package]] name = "presage" version = "0.8.0-dev" -source = "git+https://github.com/whisperfish/presage?rev=55ed137421cb2abb7c2119975379915426b1936b#55ed137421cb2abb7c2119975379915426b1936b" +source = "git+https://github.com/whisperfish/presage?rev=94208274ef7218dfe4af5603a0b9be6e275c0a1a#94208274ef7218dfe4af5603a0b9be6e275c0a1a" dependencies = [ "base64", "bytes", @@ -3381,7 +3452,7 @@ dependencies = [ [[package]] name = "presage-store-sqlite" version = "0.8.0-dev" -source = "git+https://github.com/whisperfish/presage?rev=55ed137421cb2abb7c2119975379915426b1936b#55ed137421cb2abb7c2119975379915426b1936b" +source = "git+https://github.com/whisperfish/presage?rev=94208274ef7218dfe4af5603a0b9be6e275c0a1a#94208274ef7218dfe4af5603a0b9be6e275c0a1a" dependencies = [ "async-trait", "bytes", diff --git a/Cargo.toml b/Cargo.toml index 1ea777f..83edf64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,14 +25,20 @@ tokio = { version = "1.51.0", features = ["rt", "macros", "rt-multi-thread", "ti regex = "1.12.3" lazy_static = "1.5.0" -presage = { git = "https://github.com/whisperfish/presage", rev = "55ed137421cb2abb7c2119975379915426b1936b" } -presage-store-sqlite = { git = "https://github.com/whisperfish/presage", rev = "55ed137421cb2abb7c2119975379915426b1936b" } -libsignal-service = { git = "https://github.com/whisperfish/libsignal-service-rs", rev = "bd585acc9bd40a0f4000615f659d9ab319fde299", default-features = false, features = ["phonenumber"] } +# ftypes = { package = "flare-types", path = "../flare-backend/flare-types" } +# fmanager = { package = "flare-manager", path = "../flare-backend/flare-manager" } +# fstore = { package = "flare-store", path = "../flare-backend/flare-store" } + +ftypes = { package = "flare-types", git = "https://gitlab.com/schmiddi-on-mobile/flare-backend.git", rev = "70097933a0c9c32aa49831c8fc734d68ce46aab2" } +fmanager = { package = "flare-manager", git = "https://gitlab.com/schmiddi-on-mobile/flare-backend.git", rev = "70097933a0c9c32aa49831c8fc734d68ce46aab2" } +fstore = { package = "flare-store", git = "https://gitlab.com/schmiddi-on-mobile/flare-backend.git", rev = "70097933a0c9c32aa49831c8fc734d68ce46aab2" } oo7 = { version = "0.6.0", default-features = false, features = ["native_crypto", "tokio"] } rand = "0.10.0" url = "2.5.8" once_cell = "1.21.4" +async-stream = "0.3" +uuid = "1.23" qrcode-generator = "5.0.0" diff --git a/data/resources/ui/channel_item.blp b/data/resources/ui/channel_item.blp index 47c3c3b..3196886 100644 --- a/data/resources/ui/channel_item.blp +++ b/data/resources/ui/channel_item.blp @@ -68,7 +68,7 @@ template $FlChannelItem: Box { wrap-mode: word_char; natural-wrap-mode: none; label: bind $format_last_message( - template.channel as <$FlChannel>.last-message as <$FlTextMessage>, + template.channel as <$FlChannel>.last-message as <$FlMessage>, template.channel as <$FlChannel>.is-typing as , template.channel as <$FlChannel>.draft as ) as ; diff --git a/data/resources/ui/channel_item_compact.blp b/data/resources/ui/channel_item_compact.blp index 22248c8..f2551e9 100644 --- a/data/resources/ui/channel_item_compact.blp +++ b/data/resources/ui/channel_item_compact.blp @@ -26,5 +26,6 @@ template $FlChannelItemCompact: Box { hexpand: true; ellipsize: end; use-markup: true; + label: bind template.channel as <$FlChannel>.title; } } diff --git a/data/resources/ui/channel_messages.blp b/data/resources/ui/channel_messages.blp index 53be7ab..128ae7d 100644 --- a/data/resources/ui/channel_messages.blp +++ b/data/resources/ui/channel_messages.blp @@ -155,7 +155,7 @@ template $FlChannelMessages: Box { ] halign: start; - label: bind template.reply-message as <$FlTextMessage>.sender as <$FlContact>.title; + label: bind template.reply-message as <$FlMessage>.sender as <$FlContact>.title; wrap: true; wrap-mode: word_char; lines: 1; @@ -173,7 +173,7 @@ template $FlChannelMessages: Box { ] halign: fill; - label: bind $attachment_placeholder(template.reply-message as <$FlTextMessage>.body) as ; + label: bind $attachment_placeholder(template.reply-message as <$FlMessage>.body) as ; wrap: true; wrap-mode: word_char; lines: 2; diff --git a/data/resources/ui/components/attachment_image.blp b/data/resources/ui/components/attachment_image.blp index ec303a7..e69a97f 100644 --- a/data/resources/ui/components/attachment_image.blp +++ b/data/resources/ui/components/attachment_image.blp @@ -1,18 +1,23 @@ using Gtk 4.0; +using Adw 1; template $FlAttachmentImage: $FlAttachmentBase { - Picture picture { - styles [ - "photo", - ] + Adw.Clamp { + maximum-size: bind $resize_width(template.attachment as <$FlAttachmentObject>.width) as ; - height-request: bind $resize_height( - template.attachment as <$FlAttachmentObject>.width, - template.attachment as <$FlAttachmentObject>.height - ) as ; - width-request: bind $resize_width(template.attachment as <$FlAttachmentObject>.width) as ; - overflow: hidden; - paintable: bind template.attachment as <$FlAttachmentObject>.image; - content-fit: scale_down; + Picture picture { + styles [ + "photo", + ] + + height-request: bind $resize_height( + template.attachment as <$FlAttachmentObject>.width, + template.attachment as <$FlAttachmentObject>.height + ) as ; + width-request: bind $resize_width(template.attachment as <$FlAttachmentObject>.width) as ; + overflow: hidden; + paintable: bind template.attachment as <$FlAttachmentObject>.image; + content-fit: scale_down; + } } } diff --git a/data/resources/ui/message_item.blp b/data/resources/ui/message_item.blp index 82c018b..5d94128 100644 --- a/data/resources/ui/message_item.blp +++ b/data/resources/ui/message_item.blp @@ -56,8 +56,6 @@ template $FlMessageItem: $ContextMenuBin { "message-item", ] - visible: bind $not(template.message as <$FlTextMessage>.is-deleted) as ; - Grid { column-spacing: 12; row-spacing: 12; @@ -67,8 +65,8 @@ template $FlMessageItem: $ContextMenuBin { "avatar-other", ] - text: bind template.message as <$FlTextMessage>.sender as <$FlContact>.title; - custom-image: bind template.message as <$FlTextMessage>.sender as <$FlContact>.avatar; + text: bind template.message as <$FlMessage>.sender as <$FlContact>.title; + custom-image: bind template.message as <$FlMessage>.sender as <$FlContact>.avatar; show-initials: true; valign: start; size: 32; @@ -80,7 +78,7 @@ template $FlMessageItem: $ContextMenuBin { } Adw.Spinner { - visible: bind template.message as <$FlTextMessage>.pending; + visible: bind template.message as <$FlMessage>.pending; tooltip-text: _("This message is currently being sent"); valign: center; @@ -93,7 +91,7 @@ template $FlMessageItem: $ContextMenuBin { Gtk.Image { icon-name: "dialog-error-symbolic"; tooltip-text: _("Sending this message failed"); - visible: bind template.message as <$FlTextMessage>.error; + visible: bind template.message as <$FlMessage>.error; valign: center; layout { @@ -126,7 +124,7 @@ template $FlMessageItem: $ContextMenuBin { "heading", ] - label: bind template.message as <$FlTextMessage>.sender as <$FlContact>.title; + label: bind template.message as <$FlMessage>.sender as <$FlContact>.title; hexpand: true; halign: start; ellipsize: end; @@ -149,14 +147,14 @@ template $FlMessageItem: $ContextMenuBin { "quote", ] - visible: bind $is_some(template.message as <$FlTextMessage>.quote) as ; + visible: bind $is_some(template.message as <$FlMessage>.quote) as ; Label { styles [ "heading", ] - label: bind template.message as <$FlTextMessage>.quote as <$FlTextMessage>.sender as <$FlContact>.title; + label: bind template.message as <$FlMessage>.quote as <$FlMessage>.sender as <$FlContact>.title; hexpand: true; halign: start; @@ -179,9 +177,9 @@ template $FlMessageItem: $ContextMenuBin { halign: start; use-markup: true; label: bind $attachment_placeholder( - $markup_urls(template.message as <$FlTextMessage>.quote as <$FlTextMessage>.body) as + $markup_urls(template.message as <$FlMessage>.quote as <$FlMessage>.body) as ) as ; - attributes: bind template.message as <$FlTextMessage>.quote as <$FlTextMessage>.message-attributes; + attributes: bind template.message as <$FlMessage>.quote as <$FlMessage>.message-attributes; layout { row: 1; @@ -242,17 +240,49 @@ template $FlMessageItem: $ContextMenuBin { $FlMessageIndicators timestamp_img { valign: end; halign: end; - timestamp: bind $format_time_human(template.message as <$FlTextMessage>.datetime) as ; + timestamp: bind $format_time_human(template.message as <$FlMessage>.datetime) as ; } } + Label { + ellipsize: end; + lines: 2; + wrap: true; + xalign: 0; + wrap-mode: word_char; + justify: left; + vexpand: false; + valign: start; + hexpand: true; + halign: start; + label: _("Sent a message type that is currently not supported by Flare"); + attributes: "family italic"; + visible: bind template.message as <$FlMessage>.is-unsupported; + } + + Label { + ellipsize: end; + lines: 2; + wrap: true; + xalign: 0; + wrap-mode: word_char; + justify: left; + vexpand: false; + valign: start; + hexpand: true; + halign: start; + label: _("Message was deleted"); + attributes: "family italic"; + visible: bind template.message as <$FlMessage>.is-deleted; + } + $FlMessageLabel label_message { - label: bind $markup_urls(template.message as <$FlTextMessage>.body) as ; - attributes: bind template.message as <$FlTextMessage>.message-attributes; + label: bind $markup_urls(template.message as <$FlMessage>.body) as ; + attributes: bind template.message as <$FlMessage>.message-attributes; + visible: bind $not(template.message as <$FlMessage>.is-deleted) as ; indicators: $FlMessageIndicators timestamp { - timestamp: bind $format_time_human(template.message as <$FlTextMessage>.datetime) as ; - visible: bind $not_empty(template.message as <$FlTextMessage>.body) as ; + timestamp: bind $format_time_human(template.message as <$FlMessage>.datetime) as ; }; } @@ -275,8 +305,8 @@ template $FlMessageItem: $ContextMenuBin { "reaction", ] - label: bind $fix_emoji(template.message as <$FlTextMessage>.reactions) as ; - visible: bind template.has-reaction; + label: bind $fix_emoji(template.message as <$FlMessage>.reactions) as ; + visible: bind template.message as <$FlMessage>.has-reactions; wrap-mode: word; justify: left; vexpand: false; diff --git a/po/POTFILES b/po/POTFILES index 0090232..0adba9c 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -1,7 +1,6 @@ # fd -t f -e blp . data/ # fd -t f -e rs . src/ -data/resources/ui/about.blp data/resources/ui/attachment.blp data/resources/ui/call_message_item.blp data/resources/ui/channel_info_dialog.blp @@ -40,20 +39,12 @@ src/backend/contact.rs src/backend/device_info.rs src/backend/dummy.rs src/backend/manager.rs -src/backend/manager_thread.rs -src/backend/message/call_message.rs -src/backend/message/deletion_message.rs -src/backend/message/display_message.rs -src/backend/message/mod.rs -src/backend/message/reaction_message.rs -src/backend/message/text_message.rs +src/backend/message.rs src/backend/mod.rs src/backend/server.rs -src/backend/timeline/mod.rs -src/backend/timeline/timeline_item.rs +src/backend/timeline.rs src/error.rs src/gui/attachment.rs -src/gui/call_message_item.rs src/gui/channel_info_dialog.rs src/gui/channel_item.rs src/gui/channel_item_compact.rs diff --git a/src/backend/attachment.rs b/src/backend/attachment.rs index 33df0ce..faf91de 100644 --- a/src/backend/attachment.rs +++ b/src/backend/attachment.rs @@ -7,8 +7,7 @@ use glib::{Bytes, Object, Priority}; use gtk::gdk_pixbuf::Pixbuf; use gtk::{MediaFile, MediaStream}; -use libsignal_service::proto::AttachmentPointer; -use libsignal_service::sender::AttachmentSpec; +use ftypes::AttachmentOrPointer; use std::path::PathBuf; @@ -169,7 +168,7 @@ impl Attachment { /// Convert the attachment to an attachment that can be uploaded. /// /// This is currently only possible for pictures and files. - pub(super) async fn as_upload_attachment(&self) -> (AttachmentSpec, Vec) { + pub async fn as_upload_attachment(&self) -> AttachmentOrPointer { let file = self.property::("file"); let image = self.property::>("image"); let bytes = file @@ -202,49 +201,52 @@ impl Attachment { None => None, }; - ( - AttachmentSpec { - content_type: self.content_type().unwrap_or_else(|| { + AttachmentOrPointer::Attachment(ftypes::Attachment { + properties: ftypes::AttachmentProperties { + blur_hash: img_blur_hash, + caption: None, + width: img_width, + height: img_height, + size: Some(bytes.len() as u32), + content_type: Some(self.content_type().unwrap_or_else(|| { gio::content_type_guess(file.basename(), Some(&*bytes)) .0 .as_str() .to_owned() - }), - length: bytes.len(), + })), file_name: self.name().or_else(|| { file.basename() .and_then(|f| f.file_name().map(|s| s.to_string_lossy().to_string())) }), - preview: None, - voice_note: None, - borderless: None, - width: img_width, - height: img_height, - caption: None, - blur_hash: img_blur_hash, + thumbnail: None, + uploaded: None, }, - bytes, - ) + content: bytes, + }) } - pub(super) async fn from_pointer(pointer: &AttachmentPointer, manager: &Manager) -> Self { - crate::trace!("Trying to build a Attachment from a pointer",); + pub(super) async fn from_ftypes(pointer: &AttachmentOrPointer, manager: &Manager) -> Self { + log::trace!("Trying to build a Attachment",); + let mut properties = pointer.properties().clone(); log::trace!( "Attachment with content type: {}", - pointer.content_type.as_ref().unwrap_or(&"None".to_string()) + properties + .content_type + .as_ref() + .unwrap_or(&"None".to_string()) ); // For some reason, some webp stickers are sent with content type Some(""). // Also, some stickers mention the completely wrong file type (image/webp instead of image/png), but that should not matter as both are equally handled by gtk::Picture. - let mut pointer = pointer.clone(); - if pointer.content_type == Some("".to_owned()) { - pointer.content_type = Some("image/webp".to_owned()); + // TODO: Move to ftypes. + if properties.content_type == Some("".to_owned()) { + properties.content_type = Some("image/webp".to_owned()); } let mut image = None; - let blur_hash = pointer.blur_hash.clone(); - let name = pointer.file_name.clone().unwrap_or_else(|| { - match &pointer.content_type { + let blur_hash = properties.blur_hash.clone(); + let name = properties.file_name.clone().unwrap_or_else(|| { + match &properties.content_type { Some(t) if t.starts_with("image/") => format!("image.{}", &t[6..]), Some(t) if t.starts_with("video/") => format!("video.{}", &t[6..]), Some(t) if t.starts_with("audio/") => format!("audio.{}", &t[6..]), @@ -252,11 +254,11 @@ impl Attachment { _ => "file".to_string(), } }); - let size = pointer.size.unwrap_or_default(); - let width = pointer.width.unwrap_or_default(); - let height = pointer.height.unwrap_or_default(); + let size = properties.size.unwrap_or_default(); + let width = properties.width.unwrap_or_default(); + let height = properties.height.unwrap_or_default(); - if let Some(blur_hash) = &pointer.blur_hash { + if let Some(blur_hash) = &properties.blur_hash { let actual_width = Utility::resize_width(width); let actual_height = Utility::resize_height(width, height); @@ -281,9 +283,11 @@ impl Attachment { .property("blur-hash", blur_hash) .property("width", width) .property("height", height) - .property("content-type", pointer.content_type.as_ref()) + .property("content-type", properties.content_type.as_ref()) .build(); - *s.imp().pointer.borrow_mut() = Some(pointer); + if let ftypes::AttachmentOrPointer::Pointer(p) = pointer { + *s.imp().pointer.borrow_mut() = Some(p.clone()); + } s } @@ -338,7 +342,7 @@ impl Attachment { } } - match &pointer.content_type { + match &pointer.properties.content_type { Some(t) if t.starts_with("image/") => { log::trace!("Attachment is a image, converting to usable type"); image = file @@ -417,7 +421,7 @@ mod imp { use glib::Bytes; use gtk::MediaStream; - use libsignal_service::proto::AttachmentPointer; + use ftypes::AttachmentPointer; #[derive(Default, glib::Properties)] #[properties(wrapper_type = super::Attachment)] @@ -480,7 +484,15 @@ mod imp { } fn flags(&self) -> Flags { - Flags::from(self.pointer.borrow().as_ref().unwrap().flags()) + Flags::from( + self.pointer + .borrow() + .as_ref() + .unwrap() + .secrets + .flags + .unwrap_or_default(), + ) } } diff --git a/src/backend/channel.rs b/src/backend/channel.rs index 73e82f3..d6534b6 100644 --- a/src/backend/channel.rs +++ b/src/backend/channel.rs @@ -1,32 +1,17 @@ -use crate::backend::{ - Contact, Manager, Message, - message::{ - DeletionMessage, DisplayMessage, DisplayMessageExt, MessageExt, ReactionMessage, - TextMessage, - }, - timeline::{TimelineItem, TimelineItemExt}, -}; +use crate::backend::{Contact, Manager, Message}; use crate::prelude::*; use std::hash::Hash; -use gdk::Texture; -use glib::Bytes; +use ftypes::{AccountId, ThreadId}; +use futures::StreamExt; use glib::{Object, prelude::Cast}; -use libsignal_service::{ - proto::{DataMessage, GroupContextV2}, - protocol::ServiceId, -}; -use presage::model::groups::Group; -use presage::store::Thread; - gtk::glib::wrapper! { /// A channel represents either a 1-to-1 channel or a group. pub struct Channel(ObjectSubclass); } -const EMPTY_MESSAGE_BODY: &str = ""; const TYPING_NOTIFICATION_DURATION_SECONDS: u32 = 10; #[derive(Eq, Hash, PartialEq, Clone, Debug)] @@ -36,98 +21,38 @@ pub struct TypingNotification { } impl Channel { - pub(super) async fn from_contact_or_group( - contact: Contact, - group_context: &Option, - manager: &Manager, - ) -> Self { - log::trace!("Trying to build a `Channel` from a `Contact` or `GroupContextV2`"); - - // Use cached channel if available. - let available_channels = manager.available_channels(); + pub(super) fn from_ftypes(inner: ftypes::Thread, manager: &Manager) -> Self { let s: Self = Object::builder::() .property("manager", manager) .build(); - - // Set from a group. - if let Some(group_context_v2) = group_context { - if let Some(channel) = available_channels.iter().find(|c| { - c.group_context().and_then(|c| c.master_key) == group_context_v2.master_key - }) { - return channel.clone(); - } - - // TODO: Can be `None`? - // XXX: Error on invalid size? - let group = manager - .get_group_v2( - group_context_v2 - .master_key - .clone() - .unwrap_or_default() - .try_into() - .unwrap_or_default(), - ) - .await; - if let Ok(Some(group)) = group { - return Self::from_group(group, group_context_v2, manager).await; - } else { - contact.connect_notify_local( - Some("title"), - clone!( - #[weak] - s, - move |_, _| s.notify("title") - ), - ); - s.imp().contact.swap(&RefCell::new(Some(contact))); - } - } else { - // Set from a contact. - contact.connect_notify_local( - Some("title"), - clone!( - #[weak] - s, - move |_, _| s.notify("title") - ), - ); - s.imp().contact.swap(&RefCell::new(Some(contact))); - } - - s.initialize_participants().await; + s.imp().inner.borrow_mut().replace(inner); s } - pub(super) async fn from_group( - group: Group, - group_context_v2: &GroupContextV2, - manager: &Manager, - ) -> Self { - let s: Self = Object::builder::() - .property("manager", manager) - .build(); - s.imp().group.swap(&RefCell::new(Some(group))); - s.imp() - .group_context - .swap(&RefCell::new(Some(group_context_v2.clone()))); - s.initialize_participants().await; - s + pub(super) fn set_ftypes(&self, inner: ftypes::Thread) { + self.imp().inner.borrow_mut().replace(inner); + + for prop in self.list_properties() { + self.notify(prop.name()); + } } - pub fn thread(&self) -> Thread { - if let Some(key) = self - .group_context() - .and_then(|c| c.master_key) - .and_then(|k| <[u8; 32]>::try_from(k).ok()) - { - Thread::Group(key) - } else { - Thread::Contact( - self.uuid() - .expect("Channel to either have a group key or have a UUID"), - ) - } + pub(super) fn inner(&self) -> ftypes::Thread { + self.imp() + .inner + .borrow_mut() + .as_ref() + .expect("Channel to have an inner thread") + .clone() + } + + pub fn thread(&self) -> ThreadId { + self.imp() + .inner + .borrow() + .as_ref() + .expect("Channel inner to be set") + .id() } /// Load the specified number of message from the end of the message queue. @@ -141,7 +66,6 @@ impl Channel { .timeline .borrow() .iter_forwards() - .filter(|i| i.is::()) .map(|m| m.timestamp()) .next(); crate::trace!( @@ -157,21 +81,26 @@ impl Channel { return; } - for content in iter.unwrap() { - let msg = Message::from_content(content, &manager).await; - if let Some(msg) = msg { - if let Some(msg) = msg.dynamic_cast_ref::() { - results.insert(0, msg.clone()); - } + let iter = iter.unwrap(); + futures::pin_mut!(iter); + let mut count = 0; + + while let Some(msg) = iter.next().await { + if let Ok(msg) = msg { + results.insert(0, msg.clone()); // Mark message from storage as already read. // TODO: Mark as read instead using sync messages, and store read state in DB. msg.mark_as_read(); let _ = self.do_new_message(&msg).await; + + if !msg.is_deleted() { + count += 1; + } } - if results.len() == number { + if count == number { break; } } @@ -180,8 +109,8 @@ impl Channel { results .into_iter() .map(|i| { - i.dynamic_cast::() - .expect("A 'DisplayMessage' to be a 'TimelineItem'") + i.dynamic_cast::() + .expect("A 'Message' to be a 'Message'") }) .collect(), ); @@ -189,33 +118,22 @@ impl Channel { } pub fn trim_old(&self) { - self.imp().timeline.borrow().trim_old() + if let Some(message) = self.last_message() { + self.imp() + .timeline + .borrow() + .trim_older_than(message.id().timestamp); + } } pub async fn clear_messages(&self) -> Result<(), ApplicationError> { - self.manager().clear_channel_messages(self).await?; + self.manager().clear_channel_messages(self.thread()).await?; self.imp().timeline.borrow().clear(); Ok(()) } - pub(super) fn group_context(&self) -> Option { - self.imp().group_context.borrow().clone() - } - - pub fn group(&self) -> std::cell::Ref<'_, Option> { - self.imp().group.borrow() - } - - pub fn contact(&self) -> Option { - self.imp().contact.borrow().clone() - } - - pub fn uuid(&self) -> Option { - self.imp() - .contact - .borrow() - .as_ref() - .and_then(|c| c.address()) + pub fn account_id(&self) -> Option { + self.thread().contact() } pub async fn send_session_reset(&self) -> Result<(), ApplicationError> { @@ -224,17 +142,14 @@ impl Channel { .duration_since(std::time::UNIX_EPOCH) .expect("Time went backwards") .as_millis() as u64; - let Some(uuid) = self.uuid() else { + let Some(account_id) = self.account_id() else { return Ok(()); }; - self.manager().send_session_reset(uuid, ts).await + self.manager().send_session_reset(account_id, ts).await } /// 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. - /// - Cache pending reactions and apply them for the correct message. - /// - Delete a message in the current channel. + /// This does the following: /// - Mark the message as read if the current channel is active. pub(super) async fn do_new_message( &self, @@ -244,125 +159,6 @@ impl Channel { message.mark_as_read(); } - if let Some(message) = message.dynamic_cast_ref::() { - let body = message - .body() - .unwrap_or_else(|| String::from(EMPTY_MESSAGE_BODY)); - - // Message quote. - crate::trace!("Channel {} got new message: {}", self.title(), body); - if let Some(quote) = message.quote_timestamp() { - log::trace!("Message claims to have a quote"); - let thread = self.thread(); - if let Ok(Some(quoted_msg)) = self.manager().message(&thread, quote).await - && let Some(quoted_msg) = quoted_msg.dynamic_cast_ref::() - { - crate::trace!( - "Message {} quotes other message {}", - body, - quoted_msg - .property::>("body") - .unwrap_or_default() - ); - message.set_quote(quoted_msg); - } - } - - // Apply pending reactions. - let id = message.timestamp(); - if let Some(reactions) = self.imp().pending_reactions.borrow_mut().get(&id) { - log::trace!( - "Adding pending reactions to message: {:?}", - reactions.iter().map(|r| r.emoji()).collect::>() - ); - for reaction in reactions { - message.react(reaction); - } - } - } - - // Apply reactions or store them. - if let Some(reaction) = message.dynamic_cast_ref::() { - crate::trace!( - "Channel {} got new reaction: {}", - self.title(), - reaction.emoji() - ); - - let reacted_msg = self - .imp() - .timeline - .borrow() - .get_by_timestamp(reaction.target_timestamp()) - .and_then(|o| o.dynamic_cast::().ok()); - - if let Some(reacted_msg) = reacted_msg { - if let Some(reacted_msg) = reacted_msg.dynamic_cast_ref::() { - crate::trace!( - "Reaction to message {}", - reacted_msg - .property::>("body") - .unwrap_or_default() - ); - reacted_msg.react(reaction); - } else { - log::warn!("Reaction message for a non-TextMessage"); - } - } else { - log::trace!( - "Message reacted to another message that could not be found yet. Inserting into pending reactions" - ); - let mut pending_reactions = self.imp().pending_reactions.borrow_mut(); - let entry = pending_reactions - .entry(reaction.target_timestamp()) - .or_default(); - let to_insert = entry - .binary_search_by_key(&reaction.timestamp(), |m| m.timestamp()) - .unwrap_or_else(|e| e); - entry.insert(to_insert, reaction.clone()); - } - } - - // Delete messages. - // Note we do not need to store pending deletion requests, those will not be returned by presage after deletion. - if let Some(deletion) = message - .dynamic_cast_ref::() - .map(|r| r.deletion()) - { - crate::trace!( - "Channel {} got a delete message for timestamp: {}", - self.title(), - &deletion.target_sent_timestamp() - ); - - if deletion.target_sent_timestamp.is_none() { - log::warn!("Got deletion message but without a sent timestamp. Aborting deletion."); - return Ok(()); - } - - let deleted_msg = self - .imp() - .timeline - .borrow() - .get_by_timestamp(deletion.target_sent_timestamp.unwrap()) - .and_then(|o| o.dynamic_cast::().ok()); - if let Some(deleted_msg) = deleted_msg { - if let Some(deleted_msg) = deleted_msg.dynamic_cast_ref::() { - crate::trace!( - "Deletion to message {}", - deleted_msg - .property::>("body") - .unwrap_or_default() - ); - deleted_msg.mark_as_deleted(); - self.notify("last-message"); - } else { - log::warn!("Deletion message for a non-TextMessage"); - } - } else { - log::trace!("Deletion message aimed at a unloaded message. Will be ignored"); - } - } Ok(()) } @@ -373,12 +169,12 @@ impl Channel { ) -> Result<(), gtk::glib::error::BoolError> { log::trace!("Adding new message to channel"); self.do_new_message(&message).await?; - if let Some(message) = message.dynamic_cast_ref::() { + if let Some(message) = message.dynamic_cast_ref::() { self.imp().timeline.borrow().append( message .clone() - .dynamic_cast::() - .expect("A 'DisplayMessage' to be a 'TimelineItem'"), + .dynamic_cast::() + .expect("A 'Message' to be a 'Message'"), ); self.notify("last-message"); @@ -386,88 +182,16 @@ impl Channel { message.send_notification(); } self.emit_by_name::<()>("message", &[&message]); - } else if let Some(message) = message.dynamic_cast_ref::() { - if self.manager().settings().boolean("notify-reactions") - && !message.property::("read") - { - message.send_notification(); - } } else { log::trace!("Channel skip adding empty message"); - } - Ok(()) - } - pub(super) async fn send_internal_message( - &self, - mut data: DataMessage, - timestamp: u64, - ) -> Result<(), crate::ApplicationError> { - let manager = self.manager(); - let receiver_contact = self - .imp() - .contact - .borrow() - .as_ref() - .and_then(|c| c.address()); - - if let Some(contact) = receiver_contact { - log::trace!("Sending to single contact"); - manager.send_message(contact, data, timestamp).await?; - } else { - let context = self.imp().group_context.borrow().clone(); - data.group_v2.clone_from(&context); - // TODO: Can this be `None`? - if let Some(key) = context.as_ref().and_then(|c| c.master_key.clone()) { - manager.send_message_to_group(key, data, timestamp).await?; - } - } - Ok(()) - } - - /// Send a message to the channel and add it to the channel. - pub async fn send_message(&self, msg: Message) -> Result<(), crate::ApplicationError> { - msg.mark_as_read(); - msg.set_pending(true); - - log::trace!("Inserting pending sent message to message list"); - if let Some(msg) = msg.dynamic_cast_ref::() { self.imp().timeline.borrow().append( - msg.clone() - .dynamic_cast::() - .expect("A 'DisplayMessage' to be a 'TimelineItem'"), + message + .clone() + .dynamic_cast::() + .expect("A 'Message' to be a 'Message'"), ); } - - if let Some(text_msg) = msg.dynamic_cast_ref::() { - log::trace!("Uploading attachments of text message"); - if let Err(e) = text_msg.upload_attachments().await { - msg.set_pending(false); - msg.set_error(true); - return Err(e); - } - } - - crate::debug!( - "Sending a message {} to channel {} (timestamp {})", - msg.property::>("body") - .unwrap_or_else(|| "(empty)".to_owned()), - self.title(), - msg.timestamp() - ); - if let Some(data) = msg.internal_data() - && let Err(e) = self.send_internal_message(data, msg.timestamp()).await - { - msg.set_pending(false); - msg.set_error(true); - return Err(e); - } - - msg.set_pending(false); - - self.notify("last-message"); - self.emit_by_name::<()>("message", &[&msg]); - Ok(()) } @@ -475,71 +199,6 @@ impl Channel { self.imp().participants.borrow().clone() } - pub async fn participant_by_uuid(&self, uuid: Uuid) -> Contact { - let found = self.participants().into_iter().find(|c| c.uuid() == uuid); - if let Some(found) = found { - return found; - } - let new = - Contact::from_service_address(&ServiceId::Aci(uuid.into()), &self.manager()).await; - self.imp().participants.borrow_mut().push(new.clone()); - new - } - - async fn initialize_participants(&self) { - let manager = self.manager(); - // Make sure to not hold reference accross await. - let members = { self.group().as_ref().map(|g| g.members.clone()) }; - if let Some(members) = members { - let mut participants = Vec::with_capacity(members.len()); - // XXX: Parallelize? - for p in &members { - let address = ServiceId::Aci(p.aci); - let contact = Contact::from_service_address(&address, &manager).await; - participants.push(contact); - } - for p in &participants { - p.set_channel(Some(self)); - p.update_profile_name_and_avatar().await; - } - self.imp().participants.replace(participants); - } else { - let participants = self - .imp() - .contact - .borrow() - .as_ref() - .cloned() - .into_iter() - .collect::>(); - for p in &participants { - p.set_channel(Some(self)); - } - self.imp().participants.replace(participants); - } - } - - pub async fn initialize_avatar(&self) { - if let Some(context) = self.group_context() { - let Some(avatar) = self - .manager() - .retrieve_group_avatar(context) - .await - .ok() - .flatten() - .and_then(|b| Texture::from_bytes(&Bytes::from_owned(b)).ok()) - else { - log::debug!("Failed to fetch group avatar; it may not have a profile picture set",); - return; - }; - - self.imp().group_avatar.replace(Some(avatar.into())); - } else if let Some(contact) = self.contact() { - contact.update_profile_name_and_avatar().await; - } - self.notify("avatar"); - } - /// Cap the description to a single line. pub fn single_line_description(&self) -> Option { Utility::single_line(self.description()) @@ -550,7 +209,7 @@ impl Channel { .imp() .typing .borrow_mut() - .insert(notification.sender.uuid(), notification.clone()); + .insert(notification.sender.account_id(), notification.clone()); gspawn!(clone!( #[weak(rename_to = s)] self, @@ -560,10 +219,12 @@ impl Channel { glib::timeout_future_seconds(TYPING_NOTIFICATION_DURATION_SECONDS).await; let mut typing = s.imp().typing.borrow_mut(); - if typing.get(¬ification.sender.uuid()).map(|t| t.timestamp) + if typing + .get(¬ification.sender.account_id()) + .map(|t| t.timestamp) == Some(notification.timestamp) { - typing.remove(¬ification.sender.uuid()); + typing.remove(¬ification.sender.account_id()); drop(typing); s.notify("is-typing"); s.notify("typing-label"); @@ -576,37 +237,14 @@ impl Channel { pub fn remove_user_typing(&self, contact: Contact) { let mut typing = self.imp().typing.borrow_mut(); - typing.remove(&contact.uuid()); + typing.remove(&contact.account_id()); drop(typing); self.notify("is-typing"); self.notify("typing-label"); } - /// Split up the name into first names (if available) and last name. - /// For groups, everything will be "last name". - pub fn name_parts(&self) -> (Option, String) { - if self.is_self() { - (None, gettextrs::gettext("Note to self")) - } else if let Some(group) = &*self.group() { - (None, group.title.clone()) - } else if let Some(contact) = self.contact() { - contact.name_parts() - } else { - panic!("Contact is neither group nor contact; impossible.") - } - } - - pub fn last_name(&self) -> String { - self.name_parts().1 - } - - /// The name which defines how channels are sorted. Depends on the settings. pub fn sort_name(&self) -> String { - if self.manager().settings().string("sort-contacts-by") == "surname" { - self.last_name() - } else { - self.title() - } + self.title() } /// Whether the channel is currently visible in the UI. @@ -614,14 +252,13 @@ impl Channel { self.set_property("is-active", active); } - fn first_unread_message(&self) -> Option { + fn first_unread_message(&self) -> Option { self.imp() .timeline .borrow() .iter_backwards() - .filter(|i| i.is::()) .map_while(|m| { - let message = m.dynamic_cast::().unwrap(); + let message = m.dynamic_cast::().unwrap(); // We can stop at first read message if !message.property::("read") { return Some(message); @@ -641,7 +278,7 @@ impl Channel { .timeline .borrow() .iter_backwards() - .filter(|i| i.is::() || i.is::()) + .filter(|i| i.is::()) .map_while(|m| { let message = m.dynamic_cast::().unwrap(); // We can stop at first read message @@ -652,37 +289,11 @@ impl Channel { }) .collect() } - - pub fn notification_variant(&self) -> (u8, Vec) { - let thread = self.thread(); - match thread { - Thread::Contact(u) => (0, u.service_id_binary()), - Thread::Group(g) => (1, g.into()), - } - } - - pub fn from_notification_variant((kind, data): (u8, Vec)) -> Thread { - match kind { - 0 => Thread::Contact( - ServiceId::parse_from_service_id_binary(&data) - .expect("Notification variant for contact to be a service ID"), - ), - 1 => Thread::Group( - data.try_into() - .expect("Notification variant for groups to have the correct number of bytes"), - ), - _ => unreachable!("Unexpected notification variant type"), - } - } } mod imp { use super::TypingNotification; - use crate::backend::{ - Contact, Manager, - message::{DisplayMessage, ReactionMessage, TextMessage}, - timeline::Timeline, - }; + use crate::backend::{Contact, Manager, message::Message, timeline::Timeline}; use crate::prelude::*; use std::{ @@ -690,25 +301,21 @@ mod imp { marker::PhantomData, }; + use ftypes::AccountId; use gdk::Paintable; - use libsignal_service::proto::GroupContextV2; - use presage::model::groups::Group; - #[derive(Default, glib::Properties)] #[properties(wrapper_type = super::Channel)] pub struct Channel { - pub(super) contact: RefCell>, - pub(super) group: RefCell>, - pub(super) group_context: RefCell>, + #[property(name = "title", type = String, get = |s: &Self| s.inner.borrow().as_ref().expect("Channel inner to be set").title().unwrap_or_else(|| gettextrs::gettext("Unknown Contact")))] + pub(super) inner: RefCell>, pub(super) participants: RefCell>, - pub(super) pending_reactions: RefCell>>, - pub(super) typing: RefCell>, + pub(super) typing: RefCell>, - #[property(name = "avatar", get = Self::avatar)] - pub(super) group_avatar: RefCell>, + #[property(name = "avatar", get, set)] + pub(super) avatar: RefCell>, #[property(get)] pub(super) timeline: RefCell, @@ -718,11 +325,11 @@ mod imp { pub(super) is_active: RefCell, #[property(get = Self::last_message)] - pub(super) last_message: PhantomData>, - #[property(get = Self::title)] - pub(super) title: PhantomData, + pub(super) last_message: PhantomData>, #[property(get = Self::is_contact)] pub(super) is_contact: PhantomData, + #[property(get = Self::is_group)] + pub(super) is_group: PhantomData, #[property(get = Self::is_self)] pub(super) is_self: PhantomData, /// In seconds. A value of 0 means messages don't disappear. @@ -742,81 +349,49 @@ mod imp { } impl Channel { - fn avatar(&self) -> Option { - if let Some(contact) = self.contact.borrow().as_ref() { - contact.property("avatar") - } else { - self.group_avatar.borrow().clone() - } - } - - fn last_message(&self) -> Option { + fn last_message(&self) -> Option { self.timeline .borrow() .iter_backwards() - .filter(|i| i.is::()) - .map(|m| m.dynamic_cast::().unwrap()) + .map(|m| m.dynamic_cast::().unwrap()) .find(|m| { !(*m) .clone() - .downcast::() + .downcast::() .map(|m| m.is_deleted()) .unwrap_or(false) }) } - fn title(&self) -> String { - if let Some(group) = self.group.borrow().as_ref() { - group.title.clone() - } else if let Some(contact) = self.contact.borrow().as_ref() { - if contact.property::("is-self") { - gettextrs::gettext("Note to self") - } else { - contact.title() - } - } else { - "".to_string() - } + fn is_contact(&self) -> bool { + self.obj().thread().group().is_none() } - fn is_contact(&self) -> bool { - self.contact.borrow().as_ref().is_some() + fn is_group(&self) -> bool { + self.obj().thread().group().is_some() } fn is_self(&self) -> bool { - self.contact.borrow().as_ref().is_some_and(|c| c.is_self()) + Some(self.obj().manager().account_id()) == self.obj().thread().contact() } fn disappearing_messages_timer(&self) -> u32 { - if let Some(group) = self.group.borrow().as_ref() { - group - .disappearing_messages_timer - .as_ref() - .map(|t| t.duration) - .unwrap_or_default() - } else if let Some(contact) = self.contact.borrow().as_ref() { - contact.expire_timer() - } else { - 0 + match self.obj().inner().disappearing_messages_timer() { + ftypes::Timer::Off => 0, + ftypes::Timer::On(time_delta) => time_delta.num_seconds() as u32, } } fn phone_number(&self) -> Option { - self.contact - .borrow() - .as_ref() - .and_then(|c| c.phone_number()) + self.obj() + .inner() + .contact() + .and_then(|c| c.phone_number.as_ref()) + .map(|p| p.to_string()) } fn description(&self) -> Option { - if let Some(contact) = self.contact.borrow().as_ref() { - contact.description() - } else { - self.group - .borrow() - .as_ref() - .and_then(|g| g.description.clone()) - } + self.obj().inner().description() } fn is_typing(&self) -> bool { @@ -853,7 +428,7 @@ mod imp { static SIGNALS: Lazy> = Lazy::new(|| -> Vec { vec![ Signal::builder("message") - .param_types([DisplayMessage::static_type()]) + .param_types([Message::static_type()]) .build(), ] }); diff --git a/src/backend/contact.rs b/src/backend/contact.rs index 452ca37..ab4a4d7 100644 --- a/src/backend/contact.rs +++ b/src/backend/contact.rs @@ -1,11 +1,6 @@ use crate::prelude::*; -use gdk::Texture; -use glib::Bytes; -use libsignal_service::{ - prelude::{Uuid, phonenumber::Mode}, - protocol::ServiceId, -}; +use ftypes::AccountId; use super::Manager; @@ -15,33 +10,28 @@ gtk::glib::wrapper! { } impl Contact { - pub(super) async fn from_service_address(address: &ServiceId, manager: &Manager) -> Self { - log::trace!("Building a `Contact` from a `ServiceId`"); - if let Ok(Some(contact)) = manager.get_contact_by_id(*address).await { - return Self::from_contact(contact, manager); - } - log::trace!("Not in the contact list"); + pub(super) fn from_ftypes(inner: ftypes::Contact, manager: &Manager) -> Self { let s: Self = Object::builder::() .property("manager", manager) .build(); - s.imp().uuid.swap(&RefCell::new(Some(address.raw_uuid()))); + s.imp().inner.borrow_mut().replace(inner); s } - pub(super) fn from_contact( - contact: presage::model::contacts::Contact, - manager: &Manager, - ) -> Self { - log::trace!("Building a `Contact` from a `presage::model::contacts::Contact`"); - let s: Self = Object::builder::() - .property("manager", manager) - .build(); - s.imp() - .phonenumber - .swap(&RefCell::new(contact.phone_number.clone())); - s.imp().uuid.swap(&RefCell::new(Some(contact.uuid))); - s.imp().contact.swap(&RefCell::new(Some(contact))); - s + pub(super) fn set_ftypes(&self, inner: ftypes::Contact) { + self.imp().inner.borrow_mut().replace(inner); + + for prop in self.list_properties() { + self.notify(prop.name()); + } + } + + pub fn inner(&self) -> ftypes::Contact { + self.imp() + .inner + .borrow() + .clone() + .expect("Inner contact to be set") } pub fn is_blocked(&self) -> bool { @@ -49,175 +39,19 @@ impl Contact { false } - /// Query the profile name and avatar of the contact. - pub async fn update_profile_name_and_avatar(&self) { - let obj = self.imp(); - let manager = self.manager(); - let address = self.address(); - let uuid = self.uuid(); - - let mut profile_key = { - let contact = obj.contact.borrow(); - let channel = self.channel(); - - // Note: The profile key may be different if the contact is in a group or a 1-to-1 message. - if let Some(channel) = channel { - if let Some(group) = &*channel.group() { - group - .members - .iter() - .filter(|m| Some(ServiceId::Aci(m.aci)) == address) - .map(|c| c.profile_key) - .next() - } else { - contact - .as_ref() - .and_then(|c| c.profile_key.clone().try_into().ok()) - .map(ProfileKey::create) - } - } else { - contact - .as_ref() - .and_then(|c| c.profile_key.clone().try_into().ok()) - .map(ProfileKey::create) - } - }; - - if profile_key.is_none() - && let Some(address) = address - { - profile_key = manager.get_profile_key_by_id(address).await.ok().flatten(); - } - - if let Some(key) = profile_key { - // TODO: Error handling? - let profile = manager.retrieve_profile_by_uuid(uuid, key).await.ok(); - obj.profile.replace(profile); - self.notify("title"); - - // Avatar - match manager - .retrieve_profile_avatar_by_uuid(uuid, key) - .await - .map(|b| b.and_then(|b| Texture::from_bytes(&Bytes::from_owned(b)).ok())) - { - Ok(Some(avatar)) => { - self.set_avatar(avatar); - } - Ok(None) => { - log::debug!( - "Failed to parse avatar data for {}; they may not have a profile picture set.", - self.title() - ); - } - Err(e) => { - log::debug!( - "Failed to fetch avatar for {}; they may not have a profile picture set: {e:#?}", - self.title() - ); - } - } - } - } - - pub fn uuid(&self) -> Uuid { - if let Some(a) = self.address() { - a.raw_uuid() - } else { - (*self.imp().uuid.borrow()).unwrap_or_default() - } - } - - pub(super) fn address(&self) -> Option { - self.imp() - .contact - .borrow() - .as_ref() - .map(|c| ServiceId::Aci(c.uuid.into())) - .or(self.imp().uuid.borrow().map(|u| ServiceId::Aci(u.into()))) + pub fn account_id(&self) -> AccountId { + self.inner().id.account_id } pub fn expire_timer(&self) -> u32 { - // Unwrap should never happen. - self.imp() - .contact - .borrow() - .as_ref() - .map(|c| c.expire_timer) - .unwrap_or_default() + match self.inner().disappearing_messages_timer { + ftypes::Timer::Off => 0, + ftypes::Timer::On(time_delta) => time_delta.num_seconds() as u32, + } } pub fn phone_number(&self) -> Option { - self.imp() - .contact - .borrow() - .as_ref() - .and_then(|c| c.phone_number.as_ref().map(|p| p.to_string())) - } - - pub fn description(&self) -> Option { - self.imp().profile.borrow().as_ref().and_then(|c| { - let emoji = c.about_emoji.clone().unwrap_or_default(); - let about = c.about.clone().unwrap_or_default(); - match (emoji.as_str(), about.as_str()) { - ("", "") => None, - ("", about) => Some(about.to_string()), - (emoji, "") => Some(emoji.to_string()), - (emoji, about) => Some(format!("{emoji} {about}")), - } - }) - } - - /// A heuristic how to split the name. - /// - /// Note that this may not be the best solution, e.g. for persons with multi-part surname. - pub fn name_parts(&self) -> (Option, String) { - if self.is_self() { - return (None, self.manager().profile_name()); - } - - let contact_title = self.imp().contact.borrow().as_ref().and_then(|c| { - if c.name.is_empty() { - None - } else if let Some((p1, p2)) = c.name.rsplit_once(' ') { - Some((Some(p1.to_owned()), p2.to_owned())) - } else { - Some((None, c.name.clone())) - } - }); - let profile_title = || { - self.imp() - .profile - .borrow() - .as_ref() - .and_then(|p| p.name.as_ref()) - .map(|n| { - if let Some(family) = &n.family_name { - (Some(n.given_name.clone()), family.clone()) - } else { - (None, n.given_name.clone()) - } - }) - }; - let phonenumber_title = || { - self.imp() - .phonenumber - .borrow() - .as_ref() - .map(|p| (None, p.format().mode(Mode::National).to_string())) - }; - - let (mut s1, mut s2) = contact_title - .or_else(profile_title) - .or_else(phonenumber_title) - .unwrap_or_else(|| (None, gettextrs::gettext("Unknown contact"))); - // For some reason, Signal includes some special "isolate" control - // characters around names with special symbols. - if let Some(s1) = &mut s1 { - s1.retain(|c| c != '\u{2068}' && c != '\u{2069}') - } - s2.retain(|c| c != '\u{2068}' && c != '\u{2069}'); - (s1, s2) + self.inner().phone_number.map(|p| p.to_string()) } } @@ -227,18 +61,14 @@ mod imp { use std::marker::PhantomData; use gdk::Paintable; - use libsignal_service::Profile; - use libsignal_service::prelude::phonenumber::{Mode, PhoneNumber}; use crate::backend::{Channel, Manager}; #[derive(Default, glib::Properties)] #[properties(wrapper_type = super::Contact)] pub struct Contact { - pub(super) contact: RefCell>, - pub(super) phonenumber: RefCell>, - pub(super) uuid: RefCell>, - pub(super) profile: RefCell>, + #[property(name = "title", type = String, get = |s: &Self| s.inner.borrow().as_ref().expect("Contact inner to be set").title().unwrap_or_else(|| gettextrs::gettext("Unknown Contact")))] + pub(super) inner: RefCell>, #[property(get, set)] pub(crate) avatar: RefCell>, @@ -250,52 +80,11 @@ mod imp { #[property(get = Self::is_self)] is_self: PhantomData, - #[property(get = Self::title)] - title: PhantomData, } impl Contact { fn is_self(&self) -> bool { - self.obj().uuid() == self.manager.borrow().as_ref().unwrap().uuid() - } - - fn title(&self) -> String { - if self.obj().is_self() { - return self.manager.borrow().as_ref().unwrap().profile_name(); - } - - let contact_title = self.contact.borrow().as_ref().and_then(|c| { - if c.name.is_empty() { - None - } else { - Some(c.name.clone()) - } - }); - let profile_title = || { - self.profile - .borrow() - .as_ref() - .and_then(|p| p.name.as_ref()) - .map(crate::utils::format_profile_name) - }; - let phonenumber_title = || { - self.phonenumber - .borrow() - .as_ref() - .map(|p| p.format().mode(Mode::National).to_string()) - }; - - contact_title - .or_else(profile_title) - .or_else(phonenumber_title) - .or_else(|| Some(gettextrs::gettext("Unknown contact"))) - // For some reason, Signal includes some special "isolate" control - // characters around names with special symbols. - .map(|mut s| { - s.retain(|c| c != '\u{2068}' && c != '\u{2069}'); - s - }) - .unwrap_or_default() + self.obj().account_id() == self.manager.borrow().as_ref().unwrap().account_id() } } diff --git a/src/backend/device_info.rs b/src/backend/device_info.rs index 89c4c19..3dbe00d 100644 --- a/src/backend/device_info.rs +++ b/src/backend/device_info.rs @@ -6,7 +6,7 @@ gtk::glib::wrapper! { } impl DeviceInfo { - pub fn new(info: libsignal_service::websocket::account::DeviceInfo) -> DeviceInfo { + pub fn new(info: ftypes::DeviceInfo) -> DeviceInfo { Object::builder::() .property("id", u8::from(info.id)) .property("name", info.name) diff --git a/src/backend/dummy.rs b/src/backend/dummy.rs index e6824e8..1de9f38 100644 --- a/src/backend/dummy.rs +++ b/src/backend/dummy.rs @@ -1,477 +1,414 @@ -use std::cell::OnceCell; -use std::path::Path; - -use gtk::glib::{BoxedAnyObject, DateTime}; -use gtk::prelude::{Cast, FileExt}; -use gtk::{prelude::ObjectExt, subclass::prelude::ObjectSubclassIsExt}; -use libsignal_service::Profile; -use libsignal_service::content::CallMessage as PreCallMessage; -use libsignal_service::prelude::AttachmentPointer; -use libsignal_service::prelude::Content; -use libsignal_service::prelude::ProfileKey; -use libsignal_service::prelude::Uuid; -use libsignal_service::proto::GroupContextV2; -use libsignal_service::proto::call_message::Hangup; -use libsignal_service::proto::call_message::Offer; -use libsignal_service::proto::data_message::Reaction; -use libsignal_service::protocol::DeviceId; -use libsignal_service::sender::AttachmentSpec; -use libsignal_service::sender::AttachmentUploadError; -use libsignal_service::websocket::account::DeviceInfo; -use presage::model::contacts::Contact as LContact; -use presage::model::groups::Group; -use presage::store::Thread; - -use super::{ - Channel, Contact, - message::{CallMessage, Message, MessageExt, ReactionMessage, TextMessage}, +use chrono::{DateTime, Duration, NaiveTime, Utc}; +use fmanager::SignalClient; +use ftypes::{ + AccountId, Contact, ContactId, DeviceId, DeviceInfo, MessageId, MessageSubId, Quote, QuoteKind, + ReactionMessage, RichMessage, Thread, ThreadId, Timer, }; -use crate::backend::timeline::timeline_item::TimelineItemExt; -use crate::{backend::SetupResult, error::ApplicationError}; +use futures::Stream; +use gtk::gdk::Texture; +use std::{ops::RangeBounds, path::Path}; +use uuid::Uuid; -const GROUP_ID: usize = 5; -type PresageError = presage::Error; - -macro_rules! msg { - ($s:expr, $m:expr, $i:expr, $j:expr, $t:expr) => { - TextMessage::pub_from_text_channel_sender_timestamp( - $m, - $s.dummy_channels().await[$j].clone(), - $s.dummy_contacts().await[$i].clone(), - $t * 1000 * 60, - $s, - ) - .upcast::() - }; - ($s:expr, $m:expr, $i:expr, $t:expr) => { - msg!($s, $m, $i, 1, $t) - }; -} - -macro_rules! call_msg { - ($s:expr, $m:expr, $i:expr, $t:expr) => {{ - let c = $s.dummy_contacts().await[$i].clone(); - CallMessage::from_call( - &c, - &Channel::from_contact_or_group(c.clone(), &None, $s).await, - $t * 1000 * 60, - $s, - $m, - ) - .expect("`CallMessage` to be valid") - .upcast::() - }}; -} - -pub fn dummy_presage_contacts() -> Vec { +fn threads() -> Vec { vec![ - LContact { - uuid: Uuid::from_u128(0), - phone_number: None, - name: "".to_string(), - verified: Default::default(), - profile_key: vec![0; 32], - expire_timer: 0, - inbox_position: 0, - avatar: None, - expire_timer_version: 0, + main_thread(), + fun_thread_1(), + fun_thread_2(), + fun_thread_3(), + ] +} + +fn main_thread() -> Thread { + Thread::Group(ftypes::Group { + id: ftypes::GroupId([0; 32]), + name: "Flare".to_string(), + description: Some("For screenshots".to_string()), + announcement_only: false, + disappearing_messages_timer: ftypes::Timer::Off, + members: vec![], + pending_members: vec![], + requesting_members: vec![], + invite_link_password: vec![], + access_control: None, + secrets: ftypes::GroupSecrets { + master_key: [0; 32], + revision: 0, }, - LContact { - uuid: Uuid::from_u128(1), - phone_number: None, - name: "Postmarket OS Linux Mobile User".to_string(), - verified: Default::default(), - profile_key: vec![0; 32], - expire_timer: 0, - inbox_position: 0, - avatar: None, - expire_timer_version: 0, + }) +} + +fn fun_thread_1() -> Thread { + Thread::Contact(fun_contact_1()) +} + +fn fun_thread_2() -> Thread { + Thread::Contact(fun_contact_2()) +} + +fn fun_thread_3() -> Thread { + Thread::Contact(fun_contact_3()) +} + +fn contacts() -> Vec { + vec![ + developer(), + main_thread_contact_1(), + main_thread_contact_2(), + main_thread_contact_3(), + fun_contact_1(), + fun_contact_2(), + fun_contact_3(), + ] +} + +fn developer() -> Contact { + ftypes::Contact { + id: ContactId { + account_id: AccountId::from_raw(Uuid::from_u128(0)), + in_group: Some(main_thread().group().expect("Main thread to be a group").id), }, - LContact { - uuid: Uuid::from_u128(2), - phone_number: Some( - phonenumber::parse(None, "+123456789") - .expect("Developer to have a valid phone number"), - ), - name: "Developer".to_string(), - verified: Default::default(), - profile_key: vec![0; 32], - expire_timer: 0, - inbox_position: 0, - avatar: None, - expire_timer_version: 0, + phone_number: None, + username: Some("Developer".to_string()), + email: None, + is_blocked: false, + given_name: None, + family_name: None, + about: None, + about_emoji: None, + disappearing_messages_timer: Timer::Off, + secrets: ftypes::ContactSecrets { profile_key: None }, + } +} + +fn main_thread_contact_1() -> Contact { + ftypes::Contact { + id: ContactId { + account_id: AccountId::from_raw(Uuid::from_u128(1)), + in_group: Some(main_thread().group().expect("Main thread to be a group").id), }, - LContact { - uuid: Uuid::from_u128(3), - phone_number: None, - name: "Admrial Ackbar".to_string(), - verified: Default::default(), - profile_key: vec![0; 32], - expire_timer: 0, - inbox_position: 0, - avatar: None, - expire_timer_version: 0, + phone_number: None, + username: Some("Test1".to_string()), + email: None, + is_blocked: false, + given_name: None, + family_name: None, + about: None, + about_emoji: None, + disappearing_messages_timer: Timer::Off, + secrets: ftypes::ContactSecrets { profile_key: None }, + } +} + +fn main_thread_contact_2() -> Contact { + ftypes::Contact { + id: ContactId { + account_id: AccountId::from_raw(Uuid::from_u128(2)), + in_group: Some(main_thread().group().expect("Main thread to be a group").id), }, - LContact { - uuid: Uuid::from_u128(4), - phone_number: None, - name: "Agent Smith".to_string(), - verified: Default::default(), - profile_key: vec![0; 32], - expire_timer: 0, - inbox_position: 0, - avatar: None, - expire_timer_version: 0, + phone_number: None, + username: Some("Test2".to_string()), + email: None, + is_blocked: false, + given_name: None, + family_name: None, + about: None, + about_emoji: None, + disappearing_messages_timer: Timer::Off, + secrets: ftypes::ContactSecrets { profile_key: None }, + } +} + +fn main_thread_contact_3() -> Contact { + ftypes::Contact { + id: ContactId { + account_id: AccountId::from_raw(Uuid::from_u128(3)), + in_group: Some(main_thread().group().expect("Main thread to be a group").id), }, + phone_number: None, + username: Some("Test3".to_string()), + email: None, + is_blocked: false, + given_name: None, + family_name: None, + about: None, + about_emoji: None, + disappearing_messages_timer: Timer::Off, + secrets: ftypes::ContactSecrets { profile_key: None }, + } +} + +fn fun_contact_1() -> Contact { + ftypes::Contact { + id: ContactId { + account_id: AccountId::from_raw(Uuid::from_u128(4)), + in_group: None, + }, + phone_number: None, + username: Some("Fun1".to_string()), + email: None, + is_blocked: false, + given_name: None, + family_name: None, + about: None, + about_emoji: None, + disappearing_messages_timer: Timer::Off, + secrets: ftypes::ContactSecrets { profile_key: None }, + } +} + +fn fun_contact_2() -> Contact { + ftypes::Contact { + id: ContactId { + account_id: AccountId::from_raw(Uuid::from_u128(5)), + in_group: None, + }, + phone_number: None, + username: Some("Fun2".to_string()), + email: None, + is_blocked: false, + given_name: None, + family_name: None, + about: None, + about_emoji: None, + disappearing_messages_timer: Timer::Off, + secrets: ftypes::ContactSecrets { profile_key: None }, + } +} + +fn fun_contact_3() -> Contact { + ftypes::Contact { + id: ContactId { + account_id: AccountId::from_raw(Uuid::from_u128(6)), + in_group: None, + }, + phone_number: None, + username: Some("Fun3".to_string()), + email: None, + is_blocked: false, + given_name: None, + family_name: None, + about: None, + about_emoji: None, + disappearing_messages_timer: Timer::Off, + secrets: ftypes::ContactSecrets { profile_key: None }, + } +} + +#[derive(Default)] +struct ConstructMessageAdditional { + reactions: Vec<(AccountId, &'static str)>, + quote: Option, +} + +fn construct_message>( + thread: ThreadId, + sender: AccountId, + offset: Duration, + body: S, + additional: ConstructMessageAdditional, +) -> RichMessage { + let base = chrono::Utc::now() + .with_time(NaiveTime::from_hms_milli_opt(10, 0, 0, 0).expect("Naive time")) + .single() + .expect("Single base"); + + ftypes::RichMessage { + id: MessageId { + sender, + timestamp: base + offset, + sub_id: ftypes::MessageSubId::Primary, + }, + thread, + metadata: ftypes::MessageMetadata { + needs_receipt: false, + unidentified_sender: false, + was_plaintext: false, + server_guid: None, + sender_device_id: DeviceId::try_from(1).expect("Device id"), + }, + content: ftypes::RichMessageContent { + inner: ftypes::MainMessageContent::Text(ftypes::TextMessage { + body: Some(body.as_ref().to_string()), + attachments: vec![], + quote: additional.quote.map(|q| Quote { + quoted_message: q, + body: None, + attachments: vec![], + body_ranges: vec![], + kind: QuoteKind::Normal, + }), + body_ranges: vec![], + previews: vec![], + contacts: vec![], + is_view_once: false, + }), + reactions: additional + .reactions + .iter() + .map(|r| ftypes::Message { + id: MessageId { + sender: r.0, + timestamp: base + offset, + sub_id: MessageSubId::Primary, + }, + thread, + metadata: ftypes::MessageMetadata { + needs_receipt: false, + unidentified_sender: false, + was_plaintext: false, + server_guid: None, + sender_device_id: DeviceId::try_from(1).expect("Device id"), + }, + content: ReactionMessage { + reaction_to_message: MessageId { + sender, + timestamp: base + offset, + sub_id: ftypes::MessageSubId::Primary, + }, + emoji: r.1.to_string(), + operation: ftypes::ReactionOperation::Add, + }, + }) + .collect(), + receipts: vec![], + deletion: None, + edits: vec![], + vote: vec![], + terminate_poll: None, + }, + } +} + +fn messages() -> Vec { + let announcement = construct_message( + main_thread().id(), + developer().id.account_id, + Duration::minutes(1), + "Test Message", + ConstructMessageAdditional { + reactions: vec![ + (main_thread_contact_1().id.account_id, "🎉"), + (main_thread_contact_2().id.account_id, "❤️"), + (main_thread_contact_3().id.account_id, "🚀"), + ], + ..Default::default() + }, + ); + let announcement_id = announcement.id; + vec![ + // Main thread + announcement, + construct_message( + main_thread().id(), + main_thread_contact_1().id.account_id, + Duration::minutes(2), + "Noice", + Default::default(), + ), + construct_message( + main_thread().id(), + main_thread_contact_2().id.account_id, + Duration::minutes(3), + "Reply", + ConstructMessageAdditional { + quote: Some(announcement_id), + ..Default::default() + }, + ), + // Fun + construct_message( + fun_thread_1().id(), + fun_contact_1().id.account_id, + Duration::minutes(-10), + "Fun1", + Default::default(), + ), + construct_message( + fun_thread_2().id(), + fun_contact_2().id.account_id, + Duration::minutes(-20), + "Fun2", + Default::default(), + ), + construct_message( + fun_thread_3().id(), + fun_contact_3().id.account_id, + Duration::minutes(-30), + "Fun3", + Default::default(), + ), ] } impl super::Manager { - #[cfg(feature = "screenshot")] pub async fn init>(&self, _p: &P) -> Result<(), crate::ApplicationError> { log::trace!("Init manager for screenshots"); - self.init_channels().await; - self.setup_receive_message_loop().await?; - self.imp().finished_setup.set(true); - self.notify("finished-setup"); - - #[cfg(feature = "screenshot-setup")] - { - let (tx_decision, rx_decision) = futures::channel::oneshot::channel(); - let to_send = OnceCell::new(); - let _ = to_send.set(tx_decision); - self.emit_by_name::<()>( - "setup-result", - &[&BoxedAnyObject::new(SetupResult::Pending(to_send))], - ); - let _ = rx_decision.await; - self.emit_by_name::<()>( - "setup-result", - &[&BoxedAnyObject::new(SetupResult::DisplayLinkQR( - "https://mobile.schmidhuberj.de/".try_into().unwrap(), - ))], - ); - gtk::glib::timeout_future_seconds(2).await; - self.emit_by_name::<()>( - "setup-result", - &[&BoxedAnyObject::new(SetupResult::Finished)], - ); - } + self.queue_empty().await; Ok(()) } - #[cfg(feature = "screenshot")] - pub async fn setup_receive_message_loop(&self) -> Result<(), PresageError> { - log::trace!("Setup receive loop for screenshots"); - let channels = self.imp().channels.borrow(); - - for msg in self.dummy_messages().await { - self.emit_by_name::<()>("message", &[&msg]); - if let Some(stored_channel) = channels.get(&msg.channel().thread()) { - log::debug!("Message from a already existing channel"); - let _ = stored_channel.new_message(msg).await; - } - } - Ok(()) + pub(super) fn account_id(&self) -> AccountId { + developer().id.account_id } - #[cfg(feature = "screenshot")] - pub fn profile_name(&self) -> String { - "You".to_string() + pub fn is_primary(&self) -> bool { + false } - #[cfg(feature = "screenshot")] - pub async fn message( + pub(super) async fn message_inner( + &self, + id: MessageId, + ) -> Result, ftypes::Error> { + Ok(messages().into_iter().find(|m| m.id == id)) + } + + pub(super) async fn messages_inner( + &self, + thread: ThreadId, + range: impl RangeBounds>, + ) -> Result>, ftypes::Error> { + Ok(futures::stream::iter( + messages() + .into_iter() + .filter(|m| m.thread == thread && range.contains(&m.id.timestamp)) + .map(Ok) + .rev() + .collect::>(), + )) + } + + pub(super) async fn channels(&self) -> Result, ftypes::Error> { + Ok(threads()) + } + + pub async fn devices(&self) -> Result, ftypes::Error> { + todo!() + } + + pub(super) async fn thread_avatar( &self, thread: &Thread, - timestamp: u64, - ) -> Result, ApplicationError> { - Ok(self - .dummy_messages() - .await - .into_iter() - .find(|m| m.timestamp() == timestamp)) - } - - #[cfg(feature = "screenshot")] - pub async fn messages( - &self, - thread: &Thread, - from: Option, - ) -> Result, ApplicationError> { - Ok(std::iter::empty()) - } - - #[cfg(feature = "screenshot")] - pub(super) fn uuid(&self) -> Uuid { - Uuid::nil() - } - - #[cfg(feature = "screenshot")] - pub(super) async fn retrieve_profile_by_uuid( - &self, - _uuid: Uuid, - _profile_key: ProfileKey, - ) -> Result { - Ok(Profile { - name: None, - about: Some( - "I am the Developer. I thus may write here what I want. (Insert evil laughter)\nLorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet." - .to_string(), - ), - about_emoji: Some("🤯️".to_string()), - avatar: None, - unrestricted_unidentified_access: false, - }) - } - - #[cfg(feature = "screenshot")] - pub(super) async fn retrieve_profile_avatar_by_uuid( - &self, - uuid: Uuid, - _profile_key: ProfileKey, - ) -> Result>, PresageError> { - if uuid == Uuid::from_u128(2) { - let screenshot_file = gtk::gio::File::for_uri("resource:///icon.svg"); - Ok(Some( - screenshot_file - .load_bytes(None::.as_ref()) - .expect("Failed to load icon bytes") - .0 - .to_vec(), - )) + ) -> Result, ftypes::Error> { + if thread.id() == main_thread().id() { + Ok(Some(Texture::from_resource("/icon.svg"))) } else { Ok(None) } } - #[cfg(feature = "screenshot")] - pub(super) async fn get_profile_key_by_id( + pub(super) async fn contact_avatar( &self, - id: libsignal_service::protocol::ServiceId, - ) -> Result, ApplicationError> { - Ok(dummy_presage_contacts() - .into_iter() - .find(|c| c.uuid == id.raw_uuid()) - .and_then(|c| c.profile_key.try_into().ok()) - .map(|p| ProfileKey { bytes: p })) - } - - #[cfg(feature = "screenshot")] - pub(super) async fn get_contact_by_id( - &self, - id: libsignal_service::protocol::ServiceId, - ) -> Result, ApplicationError> { - Ok(dummy_presage_contacts() - .into_iter() - .find(|c| c.uuid == id.raw_uuid())) - } - - #[cfg(feature = "screenshot")] - pub(super) async fn retrieve_group_avatar( - &self, - context: GroupContextV2, - ) -> Result>, PresageError> { + _contact: &Contact, + ) -> Result, ftypes::Error> { Ok(None) } - #[cfg(feature = "screenshot")] - pub async fn devices(&self) -> Result, PresageError> { - use chrono::{TimeDelta, Utc}; - - let now = Utc::now(); - let base_time = now - .with_time(chrono::NaiveTime::from_hms_opt(13, 24, 0).unwrap()) - .unwrap(); - - Ok(vec![ - DeviceInfo { - id: DeviceId::new(1).unwrap(), - registration_id: 0, - name: Some("Flare (Desktop)".to_string()), - created_at: base_time - TimeDelta::days(10), - last_seen: base_time - TimeDelta::days(1), - }, - DeviceInfo { - id: DeviceId::new(2).unwrap(), - registration_id: 0, - name: Some("Flare (PinePhone)".to_string()), - created_at: base_time - TimeDelta::days(7), - last_seen: base_time, - }, - DeviceInfo { - id: DeviceId::new(3).unwrap(), - registration_id: 0, - name: Some("Flare (Another)".to_string()), - created_at: base_time - TimeDelta::days(7), - last_seen: base_time, - }, - DeviceInfo { - id: DeviceId::new(4).unwrap(), - registration_id: 0, - name: Some("Flare (Another2)".to_string()), - created_at: base_time - TimeDelta::days(7), - last_seen: base_time, - }, - DeviceInfo { - id: DeviceId::new(5).unwrap(), - registration_id: 0, - name: Some("Flare (Another3)".to_string()), - created_at: base_time - TimeDelta::days(7), - last_seen: base_time, - }, - ]) - } - - #[cfg(feature = "screenshot")] - pub fn is_primary(&self) -> bool { - true - } - - #[cfg(feature = "screenshot")] - pub async fn upload_attachments( + pub(super) async fn contact_from_id_inner( &self, - attachments: Vec<(AttachmentSpec, Vec)>, - ) -> Result>, PresageError> { - Ok(vec![Ok(AttachmentPointer::default())]) - } - - #[cfg(feature = "screenshot")] - async fn dummy_messages(&self) -> Vec { - let now = DateTime::now_utc().expect("Now to be expressable as DateTime"); - let base_time = DateTime::from_utc(now.year(), now.month(), now.day_of_month(), 11, 0, 0.0) - .expect("Base time to be expressable as DateTime"); - let base_minute: u64 = (base_time.to_unix() / 60).try_into().unwrap(); - - let msg_replied = msg!( - self, - "Flare 0.19.0 was now released. This release will now show a loading screen on startup while it receives messages. Besides that, there have also been many minor UI changes, like a reduced spacing between reactions and their corresponding mesages, as well as a decreased spacing between messages sent shortly after another.", - 2, - GROUP_ID, - 18 + base_minute - ); - let msg_reply = msg!( - self, - "Updating now! The UI changes already look good in the screenshots.", - 1, - GROUP_ID, - 20 + base_minute - ); - msg_reply - .clone() - .downcast::() - .unwrap() - .set_quote(&msg_replied.clone().downcast::().unwrap()); - msg_replied - .clone() - .downcast::() - .unwrap() - .react(&ReactionMessage::from_reaction( - &self.dummy_contacts().await[0], - &self.dummy_channels().await[GROUP_ID], - 26 + base_minute, - &self, - Reaction { - emoji: Some("🎉🚀".to_string()), - remove: Some(false), - target_author_aci: None, - target_author_aci_binary: None, - target_sent_timestamp: None, - }, - )); - - let msg_screenshot = msg!(self, "", 2, GROUP_ID, 19 + base_minute); - let screenshot_file = gtk::gio::File::for_uri("resource:///icon.svg"); - let attachment = crate::backend::Attachment::from_file(screenshot_file, self); - msg_screenshot - .clone() - .downcast::() - .unwrap() - .add_attachment(attachment); - - vec![ - msg_replied, - // msg_screenshot, - msg_reply, - msg!( - self, - "Indeed, the new release looks magnificent.", - 1, - GROUP_ID, - 24 + base_minute - ), - msg!(self, "Glad you like it.", 2, GROUP_ID, 25 + base_minute), - msg!(self, "YAY!", 0, GROUP_ID, 27 + base_minute), - msg!(self, "Greetings", 2, 2, base_minute - 100), - msg!(self, "It's a trap!", 3, 3, 1 + base_minute), - msg!(self, "Hello, Mr. Anderson", 4, 4, 2 + base_minute), - ] - } - - #[cfg(feature = "screenshot")] - async fn dummy_contacts(&self) -> Vec { - let mut result = vec![]; - for c in dummy_presage_contacts() { - let contact = Contact::from_contact(c, self); - let channel = Channel::from_contact_or_group(contact.clone(), &None, self).await; - contact.set_channel(Some(&channel)); - contact.update_profile_name_and_avatar().await; - result.push(contact); - } - result - } - - #[cfg(feature = "screenshot")] - async fn dummy_channels(&self) -> Vec { - let mut result = vec![]; - for con in self.dummy_contacts().await { - result.push(Channel::from_contact_or_group(con, &None, self).await); - } - result.push( - Channel::from_group( - Group { - title: "Mobile Linux Group".to_string(), - avatar: "".to_string(), - disappearing_messages_timer: None, - access_control: None, - revision: 0, - members: dummy_presage_contacts() - .into_iter() - .map(|c| presage::model::groups::Member { - aci: c.uuid.into(), - role: libsignal_service::groups_v2::Role::Unknown, - profile_key: ProfileKey { - bytes: c.profile_key.try_into().unwrap_or_default(), - }, - joined_at_revision: 0, - }) - .collect(), - pending_members: vec![], - requesting_members: vec![], - invite_link_password: vec![], - description: None, - }, - &GroupContextV2 { - master_key: Some(vec![2; 32]), - revision: None, - group_change: None, - }, - self, - ) - .await, - ); - result - } - - #[cfg(feature = "screenshot")] - pub async fn init_channels(&self) { - for channel in self.dummy_channels().await { - self.emit_by_name::<()>("channel", &[&channel]); - let mut channels = self.imp().channels.borrow_mut(); - channel.initialize_avatar().await; - channels.insert(channel.thread(), channel); - } + id: ContactId, + ) -> Result, ftypes::Error> { + Ok(contacts().into_iter().find(|c| c.id == id)) } } diff --git a/src/backend/manager.rs b/src/backend/manager.rs index 47c3dd4..04d148e 100644 --- a/src/backend/manager.rs +++ b/src/backend/manager.rs @@ -1,29 +1,37 @@ use crate::prelude::*; +use std::cell::OnceCell; +use std::hash::{DefaultHasher, Hash, Hasher}; +#[cfg(not(feature = "screenshot"))] +use std::ops::RangeBounds; use std::{collections::HashMap, ops::Bound, path::Path}; -use gio::Application; +#[cfg(not(feature = "screenshot"))] +use chrono::{DateTime, Utc}; +use fmanager::SignalClient; +use ftypes::AccountId; +use ftypes::ContactId; +use ftypes::DeviceId; +use ftypes::DeviceInfo; +use ftypes::MessageId; +use ftypes::TextMessage; +use ftypes::Thread; +use ftypes::ThreadId; +use ftypes::error::ErrorContext; +use ftypes::{AttachmentPointer, ErrorKind}; +use futures::Stream; use gio::Settings; -use libsignal_service::protocol::DeviceId; -use libsignal_service::{ - Profile, - content::ContentBody, - proto::{AttachmentPointer, DataMessage, GroupContextV2}, - protocol::ServiceId, - sender::{AttachmentSpec, AttachmentUploadError}, - websocket::account::DeviceInfo, -}; +use gio::glib::{BoxedAnyObject, Bytes}; +use gio::{Application, Notification}; +use gtk::gdk::Texture; +use gtk::gdk_pixbuf::Pixbuf; use oo7::Keyring; -use presage::model::groups::Group; -use presage::store::{ContentsStore, StateStore, Thread}; use rand::distr::SampleString; use url::Url; -use super::{Channel, Contact, Message, manager_thread::ManagerThread}; -use crate::backend::message::{DisplayMessage, DisplayMessageExt}; -use crate::{ApplicationError, gspawn, tspawn}; +use super::{Channel, Contact, Message}; +use crate::{ApplicationError, tspawn}; -const MESSAGE_BOUND: usize = 100; const MESSAGES_INITIAL_LOAD: usize = 1; const SCHEMA_ATTRIBUTE: &str = "xdg:schema"; const ATTRIBUTE_PASSWORD: (&str, &str) = ("type", "password"); @@ -32,13 +40,97 @@ const SECRET_LENGTH: usize = 64; gtk::glib::wrapper! { /// The manager is the core of the logic of Flare. /// - /// It is mostly a wrapper around [ManagerThread] (which is itself a wrapper around [presage::Manager]). + /// It is mostly a wrapper around [fmanager::Manager]. /// It also has other functions, like caching the channels which are in use or sending notifications. pub struct Manager(ObjectSubclass); } -type StoreType = presage_store_sqlite::SqliteStore; -type PresageError = presage::Error; +type Store = fstore::DirectPresageWrapperStore; +type ManagerHandle = fmanager::ManagerHandle; + +pub type SetupConfirmation = String; + +#[derive(Debug)] +pub enum SetupResult { + /// Setup must make a decision. Either register as primary device or link device. + Pending(OnceCell>), + + /// The manager is pending a SMS confirmation code. + Confirm(OnceCell>), + /// Display QR code to link. + DisplayLinkQR(Url), + /// Everything is finished + Finished, +} + +impl SignalClient for Manager { + async fn request_setup_decision(&self) -> fmanager::SetupDecision { + let (sender, receiver) = futures::channel::oneshot::channel(); + let cell = OnceCell::new(); + cell.set(sender).expect("Failed to set OnceCell"); + self.emit_by_name::<()>( + "setup-result", + &[&BoxedAnyObject::new(SetupResult::Pending(cell))], + ); + receiver.await.expect("Failed to receive setup decision") + } + + async fn request_qrcode_scan(&self, url: Url) { + self.emit_by_name::<()>( + "setup-result", + &[&BoxedAnyObject::new(SetupResult::DisplayLinkQR(url))], + ); + } + + async fn request_confirmation_code(&self) -> String { + let (sender, receiver) = futures::channel::oneshot::channel(); + let cell = OnceCell::new(); + cell.set(sender).expect("Failed to set OnceCell"); + self.emit_by_name::<()>( + "setup-result", + &[&BoxedAnyObject::new(SetupResult::Confirm(cell))], + ); + receiver.await.expect("Failed to receive confirmation code") + } + + async fn setup_complete(&self, handle: ManagerHandle) { + log::info!("Received setup-complete from the backend"); + self.imp().internal.borrow_mut().replace(handle); + self.emit_by_name::<()>( + "setup-result", + &[&BoxedAnyObject::new(SetupResult::Finished)], + ); + self.notify("is-primary"); + } + + async fn queue_empty(&self) { + log::info!("Received queue-empty from the backend"); + self.imp().finished_setup.set(true); + if self.imp().channels.borrow().is_empty() { + self.init_channels().await; + + // TODO: Send pending notifications. + self.notify("finished-setup"); + } + } + + async fn error(&self, error: ftypes::Error) { + for context in dbg!(&error).context() { + if let ErrorContext::Message(msg) = context { + self.on_message_error(msg).await; + } + } + + self.emit_by_name::<()>( + "error", + &[&BoxedAnyObject::new(ApplicationError::from(error))], + ); + } + + fn on_message(&self, message: ftypes::ReceivedMessage) -> impl Future { + self.on_message_maybe_pending(message, false) + } +} /// Query the encryption password from the keyring, storing one if none exists. async fn encryption_password() -> Result { @@ -82,7 +174,7 @@ async fn encryption_password() -> Result { } /// Creating the configuration store at the specified path. -async fn config_store>(p: &P) -> Result { +async fn config_store>(p: &P) -> Result { let path = p.as_ref(); log::trace!("Initialize config store at {}", path.to_string_lossy()); @@ -109,14 +201,9 @@ async fn config_store>(p: &P) -> Result>(&self, p: &P) -> Result<(), ApplicationError> { + let p = p.as_ref().to_owned(); + let s = self.clone(); + + // Activate the tokio runtime, and a local set. + let _guard = crate::TOKIO_RUNTIME.enter(); + let local = tokio::task::LocalSet::new(); + local + .run_until(async move { + log::trace!("Setting up the config store"); + let config_store = config_store(&p) + .await + .expect("Failed to set up config store"); + + log::trace!("Setting up the manager"); + let manager = fmanager::Manager::new(config_store, s); + manager.receive().await + }) + .await?; + + Ok(()) + } + + pub async fn shutdown(&self) { + self.internal().shutdown_receive().await + } + + async fn on_message_maybe_pending(&self, message: ftypes::ReceivedMessage, pending: bool) { + log::trace!("Received message {:?}; pending: {}", message.id, pending); + if let Some(msg) = message.as_rich_message() { + log::trace!("Message is a rich message"); + let message = Message::from_ftypes(msg, self) + .await + .expect("Failed to construct message"); + + self.imp().last_message_datetime.replace(message.datetime()); + self.notify("last-message-datetime"); + + message.set_pending(pending); + + let channel = message.channel(); + channel + .new_message(message) + .await + .expect("Failed to add message to the channel"); + } else if let Some(msg) = message.as_amendment_message() { + log::trace!("Message is an amendment message"); + // TODO: Error? + let Ok(Some(channel)) = self.channel_from_id(msg.thread).await else { + log::warn!("Got an amendment message from an unknown thread; ignoring"); + return; + }; + + for amended_message in msg.amends() { + // TODO: Sender? + let Some(amended) = channel + .timeline() + .get_by_timestamp(amended_message.timestamp) + else { + log::trace!("Got an amendment for an unloaded message; ignoring"); + continue; + }; + + amended.add_amendment(&msg); + } + } else { + match &message.content { + ftypes::ReceivedMessageContent::Meta(ftypes::MetaMessageContent::Typing( + typing, + )) => { + let Ok(Some(channel)) = self.channel_from_id(message.thread).await else { + log::warn!("Got a typing message from an unknown thread; ignoring"); + return; + }; + + let Ok(Some(sender)) = self.contact_from_id(message.sender()).await else { + log::warn!("Got a typing message from an unknown contact; ignoring"); + return; + }; + + match typing.action { + ftypes::TypingAction::Started => { + channel.add_user_typing(crate::backend::channel::TypingNotification { + sender, + timestamp: typing.timestamp.timestamp_millis() as u64, + }); + } + ftypes::TypingAction::Stopped => { + channel.remove_user_typing(sender); + } + } + } + ftypes::ReceivedMessageContent::Meta(ftypes::MetaMessageContent::GroupChange( + change, + )) => { + if let Err(e) = self.update_channel(ThreadId::Group(change.group)).await { + log::error!( + "Failed to update group after change due to an error; ignoring: {e}" + ); + } + } + ftypes::ReceivedMessageContent::Meta( + ftypes::MetaMessageContent::ContactChange(change), + ) => { + if change.contact.in_group.is_none() + && let Err(e) = self + .update_channel(ThreadId::Contact(change.contact.account_id)) + .await + { + log::error!( + "Failed to update contact after change due to an error; ignoring: {e}" + ); + } + + if let Err(e) = self.update_contact(change.contact).await { + log::error!( + "Failed to update contact after change due to an error; ignoring: {e}" + ); + } + } + _ => { + log::warn!( + "Ignoring a message that cannot be handled by the frontend for now: {message:#?}" + ); + } + } + } + } + + async fn on_message_error(&self, message: &ftypes::ReceivedMessage) { + if let Some(message) = message.as_rich_message() { + let Ok(Some(channel)) = self.channel_from_id(message.thread).await else { + log::warn!("Could not find channel of erroring message; ignoring the error"); + return; + }; + let Some(message) = channel.timeline().get_by_timestamp(message.id.timestamp) else { + log::trace!("Cannot find message causing error in timeline; ignoring the error"); + return; + }; + + message.set_pending(false); + message.set_error(true); + } else if let Some(message) = message.as_amendment_message() { + for amends in message.amends() { + let Ok(Some(channel)) = self.channel_from_id(message.thread).await else { + log::warn!("Could not find channel of erroring message; ignoring the error"); + return; + }; + let Some(timeline_message) = channel.timeline().get_by_timestamp(amends.timestamp) + else { + log::trace!( + "Cannot find message causing error in timeline; ignoring the error" + ); + return; + }; + timeline_message.remove_amendment(&message); + } + } + // Ignore errors on meta messages, at least for now. + } + pub fn settings(&self) -> Settings { self.imp().settings.clone() } + pub fn send_notification_for_channel( + &self, + id: Option, + channel: Channel, + sender: Contact, + body: String, + ) { + if sender.is_self() || body.is_empty() { + // Skip notifications for messages sent from self or empty messages. + return; + } + let (notification_title, notification_body) = if channel.is_group() { + (channel.title(), format!("{}: {}", sender.title(), body)) + } else { + (sender.title(), body) + }; + + let icon = Pixbuf::from_resource("/icon.svg").expect("Flare to have an application icon"); + + let notification = Notification::new(¬ification_title); + notification.set_body(Some(¬ification_body)); + notification.set_icon(&icon); + notification.set_category(Some("im.received")); + notification.set_default_action_and_target_value( + "app.notification-clicked", + Some(&self.channel_to_notification_variant(&channel).to_variant()), + ); + + self.send_notification(id, ¬ification); + } + pub fn send_notification(&self, id: Option, notification: &gio::Notification) { if self.imp().settings.boolean("notifications") && let Some(application) = self.application() @@ -172,8 +459,8 @@ impl Manager { pub async fn clear_registration(&self) -> Result<(), ApplicationError> { log::trace!("Clearing the manager"); - let mut store = self.store(); - tspawn!(async move { store.clear_registration().await }) + let internal = self.internal(); + tspawn!(async move { internal.clear_registration().await }) .await .expect("Failed to spawn tokio")?; Ok(()) @@ -181,8 +468,8 @@ impl Manager { pub async fn clear_contents(&self) -> Result<(), ApplicationError> { log::trace!("Clearing the manager content"); - let mut store = self.store(); - tspawn!(async move { store.clear_contents().await }) + let internal = self.internal(); + tspawn!(async move { internal.clear_contents().await }) .await .expect("Failed to spawn tokio")?; Ok(()) @@ -190,21 +477,17 @@ impl Manager { pub async fn clear_messages(&self) -> Result<(), ApplicationError> { log::trace!("Clearing messages from the manager"); - let mut store = self.store(); - tspawn!(async move { store.clear_messages().await }) + let internal = self.internal(); + tspawn!(async move { internal.clear_messages().await }) .await .expect("Failed to spawn tokio")?; Ok(()) } - pub async fn clear_channel_messages(&self, channel: &Channel) -> Result<(), ApplicationError> { - crate::trace!( - "Clearing channel messages the manager for: {}", - channel.title() - ); - let thread = channel.thread(); - let mut store = self.store(); - tspawn!(async move { store.clear_thread(&thread).await }) + pub async fn clear_channel_messages(&self, thread: ThreadId) -> Result<(), ApplicationError> { + log::trace!("Clearing channel messages the manager for thread {thread:?}",); + let internal = self.internal(); + tspawn!(async move { internal.clear_thread_messages(thread).await }) .await .expect("Failed to spawn tokio")?; Ok(()) @@ -216,584 +499,334 @@ impl Manager { captcha: S, ) -> Result<(), ApplicationError> { let token = token.as_ref().to_owned(); - let mut captcha = captcha.as_ref().to_owned(); - if captcha.starts_with("signalcaptcha://") { - log::trace!("Captcha is the full link. Remove unneeded thigs."); - // The warning is deactivated by default on newer Rust versions, and the applied fix does not even compile. - #[allow(clippy::assigning_clones)] - if let Some((_, c)) = captcha.split_once(".challenge.") { - captcha = c.to_owned(); - } else { - log::warn!("Splitting the captcha was not successfull. Assuming it is fine"); - } - } - crate::trace!( - "Submitting recaptcha challenge with token {} and captcha {}", - token, - captcha - ); + let captcha = Url::try_from(captcha.as_ref()).map_err(|e| { + ErrorKind::Protocol + .caused_by(e) + .with_message("captcha has invalid format; expected a `signalcaptcha://` link") + })?; let internal = self.internal(); - tspawn!(async move { internal.submit_recaptcha_challenge(token, captcha).await }) - .await - .expect("Failed to spawn tokio")?; + tspawn!(async move { + internal + .submit_captcha(ftypes::CaptchaToken(token), captcha) + .await + }) + .await + .expect("Failed to spawn tokio")?; Ok(()) } #[cfg(not(feature = "screenshot"))] - pub async fn message( + async fn message_inner( &self, - thread: &Thread, - timestamp: u64, - ) -> Result, ApplicationError> { - crate::trace!( - "Querying message by thread: {:?}, timestamp: {}", - thread, - timestamp - ); - let content = { - let store = self.store(); - let thread = thread.clone(); - let content = tspawn!(async move { store.message(&thread, timestamp).await }) - .await - .expect("Failed to spawn tokio")?; - if let Some(content) = content { - Ok::<_, ApplicationError>(Some(content)) - } else { - Ok(None) - } - }?; - if let Some(content) = content { - let msg = Message::from_content(content, self).await; - log::trace!( - "Found message queried: {}", - msg.as_ref() - .and_then(|t| t.dynamic_cast_ref::()) - .and_then(|t| t.textual_description()) - .unwrap_or("No Text".to_string()) - ); - Ok(msg) + id: MessageId, + ) -> Result, ftypes::Error> { + self.internal().message(id).await + } + pub async fn message(&self, id: MessageId) -> Result, ApplicationError> { + crate::trace!("Querying message by id: {id:?}",); + if let Some(message) = self.message_inner(id).await? { + Ok(Some(Message::from_ftypes(message, self).await?)) } else { Ok(None) } } #[cfg(not(feature = "screenshot"))] - pub async fn messages( + async fn messages_inner( &self, - thread: &Thread, + thread: ThreadId, + range: impl RangeBounds>, + ) -> Result>, ftypes::Error> { + Ok(async_stream::stream! { + let internal = self.internal(); + for await m in internal.messages(thread, range).await? { + yield m; + } + }) + } + + pub async fn messages<'a>( + &'a self, + thread: &ThreadId, from: Option, - ) -> Result, ApplicationError> { + ) -> Result> + 'a, ApplicationError> { + use chrono::{TimeZone, Utc}; + use futures::StreamExt; + crate::trace!("Querying message by thread: {:?}, from {:?}", thread, from); - let internal = self.internal(); - let thread = thread.clone(); - Ok(tspawn!(async move { - internal - .messages( + let thread = *thread; + Ok(async_stream::stream! { + let result = self + .messages_inner( thread, ( Bound::Unbounded, - from.map(Bound::Excluded).unwrap_or(Bound::Unbounded), + from.map(|f| { + Bound::Excluded( + Utc.timestamp_millis_opt(f as i64) + .single() + .expect("Valid timestamp"), + ) + }) + .unwrap_or(Bound::Unbounded), ), ) - .await + .await? + + .then(move |m| async move { + Message::from_ftypes(m.expect("Failed to load message from the store"), self).await + }); + + for await m in result { + yield m; + } }) - .await - .expect("Failed to spawn tokio")? - // .rev() - .filter_map(|o| o.ok())) } - /// Asynchronously initialize the manager. This will never return unless there is an error. - /// - /// This includes: - /// - Setting up the configuration store. - /// - Constructing the manager thread, reacting to any setup results or errors that happen. - /// - Loading the stored channels. - /// - Listening for messages or errors and propagating them to the correct channels. - #[cfg(not(feature = "screenshot"))] - pub async fn init>(&self, p: &P) -> Result<(), ApplicationError> { - use futures::channel::{mpsc, oneshot}; - use futures::{FutureExt, StreamExt, select}; - use gdk::glib::BoxedAnyObject; - - let config_store = config_store(p).await?; - - log::trace!("Setting up the config store"); - self.imp() - .config_store - .swap(&RefCell::new(Some(config_store.clone()))); - - log::trace!("Setting up the manager"); - let (setup_results_tx, mut setup_results_rx) = - futures::channel::mpsc::channel(MESSAGE_BOUND); - let (error_tx, error_rx) = oneshot::channel(); - - let (send_content, mut receive_content) = mpsc::unbounded(); - let (send_error, mut receive_error) = mpsc::channel(MESSAGE_BOUND); - - gspawn!(clone!( - #[weak(rename_to = s)] - self, - async move { - log::trace!("Awaiting for setup results"); - while let Some(result) = setup_results_rx.next().await { - s.emit_by_name::<()>("setup-result", &[&BoxedAnyObject::new(result)]); - } - } - )); - - let internal = ManagerThread::new( - config_store, - setup_results_tx, - error_tx, - send_content, - send_error, - ) - .await; - - log::trace!("Awaiting for error linking"); - match error_rx.await { - Ok(err) => { - log::error!("Got error linking device: {}", err); - return Err(err.into()); - } - Err(_e) => log::trace!("Manager setup successful"), - } - - if internal.is_none() - && let Some(error_opt) = receive_error.next().await - { - log::error!("Got error after linking device: {}", error_opt); - return Err(error_opt); - } - - self.imp().internal.swap(&RefCell::new(internal)); - - // Check again if is primary, after setup is successful. - self.notify("is-primary"); - - crate::info!("Own uuid: {:?}", self.uuid()); - log::debug!("Start receiving messages"); - 'outer: loop { - use presage::model::messages::Received; - select! { - // Receive errors. - error_opt = receive_error.next().fuse() => { - if error_opt.is_none() { - break 'outer; - } - self.emit_by_name::<()>("error", &[&BoxedAnyObject::new(error_opt.unwrap())]); - } - // Receive messages. - msg_opt = receive_content.next().fuse() => { - match msg_opt { - None => { break 'outer }, - Some(Received::QueueEmpty) => { - if !self.imp().finished_setup.get() { - self.imp().finished_setup.set(true); - self.init_channels().await; - self.notify("finished-setup"); - - if let Some(application) = self.application() { - let mut pending_notifications = self.imp().pending_notifications.borrow_mut(); - for (id, notification) in &*pending_notifications { - application.send_notification(id.as_deref(), notification); - } - pending_notifications.clear(); - } - } - }, - Some(Received::Contacts) => { - log::trace!("Received contacts"); - self.init_channels().await; - } - Some(Received::Content(msg)) => { - let message = Message::from_content(*msg, self).await; - if message.is_none() { - log::trace!("Manager ignoring empty message"); - continue; - } - let message = message.unwrap(); - - self.imp().last_message_datetime.replace(message.property("datetime")); - self.notify("last-message-datetime"); - - let channel = message.channel(); - let channel = { - let channels = self.imp().channels.borrow(); - crate::debug!("Got from channel: {}", channel.property::("title")); - if let Some(stored_channel) = channels.get(&channel.thread()) { - log::debug!("Message from a already existing channel"); - stored_channel.clone() - } else { - drop(channels); - log::debug!("Got a message from a new channel"); - self.emit_by_name::<()>("channel", &[&channel]); - let mut channels_mut = self.imp().channels.borrow_mut(); - channels_mut.insert(channel.thread(), channel.clone()); - channel - } - }; - if channel.new_message(message).await.is_err() { - break 'outer; - } - log::debug!("Emitting message"); - } - } - } - complete => break, - }; - } - Ok(()) + fn internal(&self) -> ManagerHandle { + self.internal_opt() + .expect("Internal ManagerHandle to be set") } - fn store(&self) -> StoreType { - self.imp() - .config_store - .borrow() - .as_ref() - .expect(" store to be set up") - .clone() - } - - #[cfg(not(feature = "screenshot"))] - pub fn profile_name(&self) -> String { - self.internal() - .retrieve_profile() - .and_then(|p| p.name) - .as_ref() - .map(crate::utils::format_profile_name) - .unwrap_or_else(|| gettextrs::gettext("You")) - } - - fn internal(&self) -> ManagerThread { - self.imp().internal() + fn internal_opt(&self) -> Option { + self.imp().internal.borrow().as_ref().cloned() } pub fn available_channels(&self) -> Vec { self.imp().channels.borrow().values().cloned().collect() } - pub(super) async fn channel_from_uuid_or_group( - &self, - uuid: ServiceId, - group: &Option, - ) -> Channel { - let thread = if let Some(group) = group { - Thread::Group( - group - .master_key() - .try_into() - .expect("Group master key to have the correct length"), - ) - } else { - Thread::Contact(uuid) - }; - - // Try and use already cached channel - if let Some(cached) = self.imp().channels.borrow().get(&thread).cloned() { - return cached; + #[cfg(not(feature = "screenshot"))] + async fn thread_avatar(&self, thread: &Thread) -> Result, ftypes::Error> { + match self.internal().thread_avatar(thread).await { + Ok(Some(bytes)) => { + // TODO: Maybe don't ignore the error? + let texture = Texture::from_bytes(&Bytes::from_owned(bytes)).ok(); + Ok(texture) + } + Ok(None) => Ok(None), + Err(e) => Err(e), } - - 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"); - self.emit_by_name::<()>("channel", &[&channel]); - channel.clone() - }); - - // No need to initialize avatar or last messages in here, will be done when initializing contacts. - - channel } - pub fn channel_from_thread(&self, thread: Thread) -> Option { - let known_channels = self.imp().channels.borrow_mut(); - known_channels.get(&thread).cloned() - } - - pub fn channel_from_group_id(&self, id: &[u8]) -> Option { - self.imp() - .channels - .borrow() - .iter() - .filter_map(|(thread, value)| { - if let Thread::Group(key) = thread { - Some((key, value)) - } else { - None - } - }) - .find(|(master_key, _value)| { - GroupSecretParams::derive_from_master_key(GroupMasterKey::new(**master_key)) - .get_group_identifier() - == id - }) - .map(|(_master_key, value)| value) - .cloned() + // TODO: Deduplicate with thread avatar + #[cfg(not(feature = "screenshot"))] + async fn contact_avatar( + &self, + contact: &ftypes::Contact, + ) -> Result, ftypes::Error> { + match self.internal().contact_avatar(contact).await { + Ok(Some(bytes)) => { + // TODO: Maybe don't ignore the error? + let texture = Texture::from_bytes(&Bytes::from_owned(bytes)).ok(); + Ok(texture) + } + Ok(None) => Ok(None), + Err(e) => Err(e), + } } #[cfg(not(feature = "screenshot"))] - pub async fn list_contacts(&self) -> Vec { - let store = self.store(); - tspawn!(async move { store.contacts().await }) - .await - .expect("Failed to spawn tokio") - .map(|i| { - i.filter_map(|c| c.ok().map(|c| Contact::from_contact(c, self))) - .collect() - }) - .unwrap_or_default() + async fn channels(&self) -> Result, ftypes::Error> { + self.internal().threads().await } - pub fn self_contact(&self) -> Contact { - let presage_contact = presage::model::contacts::Contact { - uuid: self.uuid(), - // TODO: Get own phone number? - phone_number: None, - name: "".to_string(), - verified: Default::default(), - profile_key: vec![], - expire_timer: 0, - expire_timer_version: 0, - inbox_position: 0, - avatar: None, - }; - Contact::from_contact(presage_contact, self) - } - - #[cfg(not(feature = "screenshot"))] pub async fn init_channels(&self) -> bool { log::trace!("Trying to initialize channels"); - let mut to_load = vec![]; + // TODO: Error handling. + let threads = self.channels().await.expect("Failed to query threads"); - // Construct all channels in parallel. - let channels_futures = self.list_contacts().await.into_iter().map(|c| async move { - let channel = Channel::from_contact_or_group(c.clone(), &None, self).await; - c.set_channel(Some(&channel)); - channel - }); - let loaded_channels = futures::future::join_all(channels_futures).await; + let channels: HashMap<_, _> = threads + .into_iter() + .map(|t| (t.id(), Channel::from_ftypes(t, self))) + .collect(); + *self.imp().channels.borrow_mut() = channels.clone(); - // Storing loaded cannels. Extra block around to drop `known_channels` before `await`. - { - let mut known_channels = self.imp().channels.borrow_mut(); - to_load.extend(loaded_channels.clone()); - for channel in loaded_channels { - known_channels.entry(channel.thread()).or_insert_with(|| { - log::trace!("Got a contact from the storage"); - self.emit_by_name::<()>("channel", &[&channel]); - channel - }); - } - } + let mut fut = vec![]; - // Note: Groups need channels to be finished initializing first due to loading participants; we cannot combine them. - - let store = self.store(); - // TODO: Error handling? - if let Ok(groups) = tspawn!(async move { store.groups().await }) - .await - .expect("Failed to spawn tokio") - { - // Construct all groups in parallel. - let groups = groups - .into_iter() - .map_while(|v| v.ok()) - .map(|(key, group)| async move { - let revision = group.revision; - Channel::from_group( - group, - &GroupContextV2 { - master_key: Some(key.into()), - revision: Some(revision), - group_change: None, - }, - self, - ) - .await - }); - let loaded_channels = futures::future::join_all(groups).await; - - // Store loaded channels. Extra block around to drop `known_channels` before `await`. - { - let mut known_channels = self.imp().channels.borrow_mut(); - to_load.extend(loaded_channels.clone()); - for channel in loaded_channels { - known_channels.entry(channel.thread()).or_insert_with(|| { - self.emit_by_name::<()>("channel", &[&channel]); - channel - }); + for (_, c) in channels { + self.emit_by_name::<()>("channel", &[&c]); + let s = self.clone(); + fut.push(async move { + c.load_last(MESSAGES_INITIAL_LOAD).await; + log::info!("Query avatar for {:?}", c.thread()); + // TODO: Don't ignore errors? + if let Ok(Some(avatar)) = s.thread_avatar(&c.inner()).await { + c.set_avatar(avatar); } - } + }); } - // For each channel, load initial messages and initialize avatars, all in parallel. - let futures = to_load.iter().map(|c| { - futures::future::join(c.load_last(MESSAGES_INITIAL_LOAD), c.initialize_avatar()) - }); - futures::future::join_all(futures).await; + futures::future::join_all(fut).await; - !to_load.is_empty() + true + } + + pub async fn update_channel(&self, id: ThreadId) -> Result<(), ftypes::Error> { + log::info!("Channel with id {:?} changed; reloading", id); + let Some(channel) = self.channel_from_id(id).await? else { + log::warn!("Cannot find the channel to update in flare; ignoring"); + return Ok(()); + }; + let Some(fchannel) = self.internal().thread(id).await? else { + log::warn!("Cannot find the channel to update in flare-backend; ignoring"); + return Ok(()); + }; + + dbg!(channel.inner()); + channel.set_ftypes(fchannel); + dbg!(channel.inner()); + + // TODO: Don't ignore errors? + if let Ok(Some(avatar)) = self.thread_avatar(&channel.inner()).await { + channel.set_avatar(avatar); + } + Ok(()) + } + + pub async fn update_contact(&self, id: ContactId) -> Result<(), ftypes::Error> { + log::info!("Contact with id {:?} changed; reloading", id); + let Some(contact) = self.contact_from_id(id).await? else { + log::warn!("Cannot find the contact to update in flare; ignoring"); + return Ok(()); + }; + let Some(fcontact) = self.internal().contact(id).await? else { + log::warn!("Cannot find the contact to update in flare-backend; ignoring"); + return Ok(()); + }; + + contact.set_ftypes(fcontact); + + // TODO: Don't ignore errors? + if let Ok(Some(avatar)) = self.contact_avatar(&contact.inner()).await { + contact.set_avatar(avatar); + } + Ok(()) + } + + pub(super) async fn channel_from_id( + &self, + id: ThreadId, + ) -> Result, ftypes::Error> { + let channel = { + let channels = self.imp().channels.borrow(); + channels.get(&id).cloned() + }; + match channel { + Some(c) => Ok(Some(c)), + None => { + let channel = self + .internal() + .thread(id) + .await? + .map(|t| Channel::from_ftypes(t, self)); + if let Some(channel) = &channel { + // Don't load last messages; if the channel was not available from initial loading, no previous messages exist yet. + log::info!("Query avatar for {:?}", channel.thread()); + // TODO: Don't ignore errors? + if let Ok(Some(avatar)) = self.thread_avatar(&channel.inner()).await { + channel.set_avatar(avatar); + } + self.imp() + .channels + .borrow_mut() + .insert(channel.thread(), channel.clone()); + } + Ok(channel) + } + } + } + + #[cfg(not(feature = "screenshot"))] + pub(super) async fn contact_from_id_inner( + &self, + id: ContactId, + ) -> Result, ftypes::Error> { + self.internal().contact(id).await + } + + pub(super) async fn contact_from_id( + &self, + id: ContactId, + ) -> Result, ftypes::Error> { + let contact = { + let contacts = self.imp().contacts.borrow(); + contacts.get(&id).cloned() + }; + match contact { + Some(c) => Ok(Some(c)), + None => { + let contact = self + .contact_from_id_inner(id) + .await? + .map(|t| Contact::from_ftypes(t, self)); + if let Some(contact) = &contact { + log::info!("Query avatar for {:?}", contact.account_id()); + // TODO: Don't ignore errors? + if let Ok(Some(avatar)) = self.contact_avatar(&contact.inner()).await { + contact.set_avatar(avatar); + } + self.imp() + .contacts + .borrow_mut() + .insert(contact.inner().id, contact.clone()); + } + Ok(contact) + } + } } } impl Manager { - pub(super) async fn get_group_v2( - &self, - master_key: [u8; 32], - ) -> Result, ::Error> { - log::trace!("`Manager::get_group_v2`start"); - let internal = self.internal(); - let r = tspawn!(async move { internal.get_group_v2(master_key).await }) - .await - .expect("Failed to spawn tokio"); - log::trace!("`Manager::get_group_v2`finished"); - r - } - - #[cfg(not(feature = "screenshot"))] - pub(super) async fn retrieve_profile_by_uuid( - &self, - uuid: Uuid, - profile_key: ProfileKey, - ) -> Result { - log::trace!("`Manager::retrieve_profile_by_uuid` start"); - let internal = self.internal(); - let r = tspawn!(async move { internal.retrieve_profile_by_uuid(uuid, profile_key).await }) - .await - .expect("Failed to spawn tokio"); - log::trace!("`Manager::retrieve_profile_by_uuid` finished"); - r - } - - #[cfg(not(feature = "screenshot"))] - pub(super) async fn retrieve_profile_avatar_by_uuid( - &self, - uuid: Uuid, - profile_key: ProfileKey, - ) -> Result>, PresageError> { - log::trace!("`Manager::retrieve_profile_avatar_by_uuid` start"); - let internal = self.internal(); - let r = tspawn!(async move { - internal - .retrieve_profile_avatar_by_uuid(uuid, profile_key) - .await - }) - .await - .expect("Failed to spawn tokio"); - log::trace!("`Manager::retrieve_profile_avatar_by_uuid` finished"); - r - } - - #[cfg(not(feature = "screenshot"))] - pub(super) async fn retrieve_group_avatar( - &self, - context: GroupContextV2, - ) -> Result>, PresageError> { - log::trace!("`Manager::retrieve_group_avatar` start"); - let internal = self.internal(); - let r = tspawn!(async move { internal.retrieve_group_avatar(context).await }) - .await - .expect("Failed to spawn tokio"); - log::trace!("`Manager::retrieve_group_avatar` finished"); - r - } - - pub(super) async fn send_message( - &self, - recipient_addr: impl Into + std::clone::Clone + Send + 'static, - message: impl Into, - timestamp: u64, - ) -> Result<(), ApplicationError> { + pub async fn send_message(&self, thread: ThreadId, message: TextMessage) { log::trace!("`Manager::send_message` start"); - let body = message.into(); let internal = self.internal(); - let r = tspawn!(async move { - internal - .send_message(recipient_addr.clone(), body.clone(), timestamp) - .await - }) - .await - .expect("Failed to spawn tokio"); - log::trace!("`Manager::send_message`finished"); - Ok(r?) + let r = tspawn!(async move { internal.send_text_message(thread, message,).await }) + .await + .expect("Failed to spawn tokio"); + log::trace!("`Manager::send_message` finished"); + self.on_message_maybe_pending(r, true).await; + } + + pub async fn send_reaction(&self, thread: ThreadId, message: MessageId, reaction: String) { + log::trace!("`Manager::send_reaction` start"); + let internal = self.internal(); + let r = tspawn!(async move { internal.send_reaction(thread, message, reaction).await }) + .await + .expect("Failed to spawn tokio"); + log::trace!("`Manager::send_reaction` finished"); + self.on_message_maybe_pending(r, true).await; + } + + pub async fn send_delete_for_everyone(&self, thread: ThreadId, message: MessageId) { + log::trace!("`Manager::send_delete_for_everyone` start"); + let internal = self.internal(); + let r = tspawn!(async move { internal.send_delete_for_everyone(thread, message).await }) + .await + .expect("Failed to spawn tokio"); + log::trace!("`Manager::send_delete_for_everyone` finished"); + self.on_message_maybe_pending(r, true).await; } pub(super) async fn send_session_reset( &self, - recipient_addr: impl Into + Send + 'static, - timestamp: u64, + _recipient_addr: AccountId, + _timestamp: u64, ) -> Result<(), ApplicationError> { log::trace!("`Manager::send_session_reset` start"); - let internal = self.internal(); - let r = - tspawn!(async move { internal.send_session_reset(recipient_addr, timestamp).await }) - .await - .expect("Failed to spawn tokio"); + // TODO log::trace!("`Manager::send_session_reset` finished"); - Ok(r?) - } - - pub(super) async fn send_message_to_group( - &self, - group_key: Vec, - message: DataMessage, - timestamp: u64, - ) -> Result<(), ApplicationError> { - log::trace!("`Manager::send_message_to_group` start"); - let internal = self.internal(); - let r = tspawn!(async move { - internal - .send_message_to_group(group_key, message.clone(), timestamp) - .await - }) - .await - .expect("Failed to spawn tokio"); - log::trace!("`Manager::send_message_to_group` finish"); - Ok(r?) - } - - #[cfg(not(feature = "screenshot"))] - pub(super) async fn get_contact_by_id( - &self, - id: ServiceId, - ) -> Result, ApplicationError> { - log::trace!("`Manager::get_contact_by_id` start"); - let store = self.store(); - let r = tspawn!(async move { store.contact_by_id(&id).await }) - .await - .expect("Failed to spawn tokio"); - log::trace!("`Manager::get_contact_by_id` finished"); - Ok(r?) - } - - #[cfg(not(feature = "screenshot"))] - pub(super) async fn get_profile_key_by_id( - &self, - id: ServiceId, - ) -> Result, ApplicationError> { - log::trace!("`Manager::get_profile_key_by_id` start"); - let store = self.store(); - let r = tspawn!(async move { store.profile_key(&id).await }) - .await - .expect("Failed to spawn tokio"); - log::trace!("`Manager::get_profile_key_by_id` finished"); - Ok(r?) + Ok(()) } pub(super) async fn get_attachment( &self, attachment_pointer: &AttachmentPointer, - ) -> Result, PresageError> { + ) -> Result, ftypes::Error> { log::trace!("`Manager::get_attachment` start"); let internal = self.internal(); let attachment_pointer = attachment_pointer.clone(); - let r = tspawn!(async move { internal.get_attachment(&attachment_pointer).await }) + let r = tspawn!(async move { internal.attachment(attachment_pointer).await }) .await .expect("Failed to spawn tokio"); log::trace!("`Manager::get_attachment` finished"); @@ -801,35 +834,20 @@ impl Manager { } #[cfg(not(feature = "screenshot"))] - pub(super) fn uuid(&self) -> Uuid { - self.internal().uuid() - } - - #[cfg(not(feature = "screenshot"))] - pub async fn upload_attachments( - &self, - attachments: Vec<(AttachmentSpec, Vec)>, - ) -> Result>, PresageError> { - log::trace!("`Manager::upload_attachment` start"); - let internal = self.internal(); - let r = tspawn!(async move { internal.upload_attachments(attachments).await }) - .await - .expect("Failed to spawn tokio"); - log::trace!("`Manager::upload_attachment` finished"); - r + pub(super) fn account_id(&self) -> AccountId { + self.internal().own_account_id() } pub async fn request_contacts_sync(&self) -> Result<(), ApplicationError> { - log::trace!("`Manager::request_contacts_sync` start"); + log::trace!("Requesting contacts sync",); let internal = self.internal(); - let r = tspawn!(async move { internal.request_contacts().await }) + tspawn!(async move { internal.request_contacts_sync().await }) .await .expect("Failed to spawn tokio"); - log::trace!("`Manager::request_contacts_sync` finished"); - Ok(r?) + Ok(()) } - pub async fn link_secondary(&self, url: Url) -> Result<(), PresageError> { + pub async fn link_secondary(&self, url: Url) -> Result<(), ftypes::Error> { log::trace!("`Manager::link_secondary` start"); let internal = self.internal(); let r = tspawn!(async move { internal.link_secondary(url).await }) @@ -839,7 +857,7 @@ impl Manager { r } - pub async fn unlink_secondary(&self, id: DeviceId) -> Result<(), PresageError> { + pub async fn unlink_secondary(&self, id: DeviceId) -> Result<(), ftypes::Error> { log::trace!("`Manager::unlink_secondary` start"); let internal = self.internal(); let r = tspawn!(async move { internal.unlink_secondary(id).await }) @@ -850,10 +868,10 @@ impl Manager { } #[cfg(not(feature = "screenshot"))] - pub async fn devices(&self) -> Result, PresageError> { + pub async fn devices(&self) -> Result, ftypes::Error> { log::trace!("`Manager::devices` start"); let internal = self.internal(); - let r = tspawn!(async move { internal.devices().await }) + let r = tspawn!(async move { internal.linked_devices().await }) .await .expect("Failed to spawn tokio"); log::trace!("`Manager::devices` finished"); @@ -862,38 +880,44 @@ impl Manager { #[cfg(not(feature = "screenshot"))] pub fn is_primary(&self) -> bool { + self.internal_opt().is_some_and(|h| h.is_primary_device()) + } + + pub fn channel_to_notification_variant(&self, channel: &Channel) -> u64 { + let thread = channel.thread(); + let mut hasher = DefaultHasher::new(); + thread.hash(&mut hasher); + hasher.finish() + } + + pub fn notification_variant_to_channel(&self, variant: u64) -> Option { self.imp() - .internal + .channels .borrow() - .as_ref() - .and_then(|r| r.registration_type()) - == Some(presage::manager::RegistrationType::Primary) + .iter() + .filter(|(_thread, channel)| self.channel_to_notification_variant(channel) == variant) + .map(|(_, channel)| channel.clone()) + .next() } } mod imp { - use crate::prelude::*; + use crate::{backend::Contact, prelude::*}; use std::collections::HashMap; + use ftypes::{ContactId, ThreadId}; use gio::{Application, Notification, Settings, glib::DateTime}; use glib::{BoxedAnyObject, ParamSpec, ParamSpecBoolean, Value}; use crate::{ - backend::{Channel, Message, manager_thread::ManagerThread}, + backend::{Channel, Message}, config::BASE_ID, }; - use presage::store::Thread; pub struct Manager { - pub(super) internal: RefCell>, - pub(super) config_store: RefCell>, - #[cfg(feature = "screenshot")] - pub(in super::super) channels: RefCell>, - #[cfg(not(feature = "screenshot"))] - pub(super) channels: RefCell>, - #[cfg(feature = "screenshot")] - pub(in super::super) finished_setup: Cell, - #[cfg(not(feature = "screenshot"))] + pub(super) internal: RefCell>, + pub(super) channels: RefCell>, + pub(super) contacts: RefCell>, pub(super) finished_setup: Cell, pub(super) pending_notifications: RefCell, Notification)>>, pub(super) last_message_datetime: RefCell>, @@ -905,8 +929,8 @@ mod imp { fn default() -> Self { Self { internal: Default::default(), - config_store: Default::default(), channels: Default::default(), + contacts: Default::default(), finished_setup: Default::default(), pending_notifications: Default::default(), last_message_datetime: Default::default(), @@ -916,16 +940,6 @@ mod imp { } } - impl Manager { - pub(super) fn internal(&self) -> ManagerThread { - self.internal - .borrow() - .as_ref() - .expect("Manager internal not yet set") - .clone() - } - } - #[glib::object_subclass] impl ObjectSubclass for Manager { const NAME: &'static str = "FlManager"; @@ -982,49 +996,3 @@ mod imp { } } } - -#[cfg(test)] -mod tests { - use super::*; - use libsignal_service::prelude::Uuid; - use libsignal_service::protocol::ServiceId; - - /// Regression test for the channel-cache miss in - /// [`Manager::channel_from_uuid_or_group`]. - /// - /// The function previously always built and returned a fresh - /// `Channel` GObject, even when one for the same thread already - /// lived in the cache. Property notifications (e.g. `is-typing` - /// for incoming `TypingMessage`s) were therefore emitted on an - /// orphan instance the UI was not bound to, and indicators - /// silently never reflected the change. This test asserts the - /// invariant that a cached thread short-circuits to the cached - /// `Channel` instance. - #[test] - fn cached_thread_returns_cached_channel() { - gtk::init().expect("gtk to initialize"); - let manager: Manager = glib::Object::new(); - let cached: Channel = glib::Object::new(); - let uuid = Uuid::from_u128(0x1); - let thread = Thread::Contact(ServiceId::Aci(uuid.into())); - - manager - .imp() - .channels - .borrow_mut() - .insert(thread, cached.clone()); - - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .expect("tokio current-thread runtime to build"); - let result = runtime - .block_on(manager.channel_from_uuid_or_group(ServiceId::Aci(uuid.into()), &None)); - - assert!( - cached.eq(&result), - "channel_from_uuid_or_group must return the cached `Channel` instance \ - so notify-signals reach widgets bound to it" - ); - } -} diff --git a/src/backend/manager_thread.rs b/src/backend/manager_thread.rs deleted file mode 100644 index 1f6a885..0000000 --- a/src/backend/manager_thread.rs +++ /dev/null @@ -1,812 +0,0 @@ -//! The [ManagerThread] is a [presage::Manager] running on its own tokio thread. -//! -//! This is required as the tokio and glib runtimes do not play together nicely (i.e. this can lead to crashes). -//! Furthermore, one cannot just send [presage::Manager] to a separate tokio thread and send back results each time one -//! runs a method on it as [presage::Manager] is `!Sync`. Therefore, this workaround exists which runs the manager in a -//! separate thread and communication between threads happens via channels. Those communications are two-way, -//! [crate::backend::Manger] to [ManagerThread] happens via sending a [Command] over a channel, this command includes a -//! channel back where the result is sent. This way is also accompanied by methods of [ManagerThread] that are similar -//! to those of [presage::Manager]. The other way is with a separate channel which has to be given to [ManagerThread] -//! when it is constructed. - -use std::sync::Arc; -use std::{cell::OnceCell, ops::Bound}; - -use futures::channel::{mpsc, oneshot}; -use futures::{FutureExt, SinkExt, StreamExt, join, select}; -use gio::prelude::NetworkMonitorExt; -use libsignal_service::protocol::DeviceId; -use libsignal_service::{ - Profile, - configuration::SignalServers, - content::ContentBody, - prelude::{ProfileKey, Uuid, phonenumber}, - proto::{AttachmentPointer, DataMessage, GroupContextV2}, - protocol::ServiceId, - sender::{AttachmentSpec, AttachmentUploadError}, - websocket::account::DeviceInfo, -}; -use presage::model::messages::Received; -use presage::{ - Manager, - manager::{Registered, RegistrationOptions, RegistrationType}, - model::groups::Group, - store::{ContentsStore, Thread}, -}; -use presage_store_sqlite::SqliteStore as Store; -use tokio::sync::RwLock; -use url::Url; - -use crate::ApplicationError; -use crate::prelude::*; - -const MESSAGE_BOUND: usize = 10; -const OFFLINE_SLEEP_TIMEOUT: u64 = 15; - -type Error = presage::Error<::Error>; - -// TODO: Reconsider ignoring in the future, but probably does not make any huge difference. -#[allow(clippy::large_enum_variant)] -enum Command { - Uuid(oneshot::Sender), - SubmitRecaptchaChallenge(String, String, oneshot::Sender>), - RetrieveProfileByUuid(Uuid, ProfileKey, oneshot::Sender>), - RetrieveProfile(oneshot::Sender>), - GetGroupV2( - [u8; 32], - oneshot::Sender, ::Error>>, - ), - SendSessionReset(ServiceId, u64, oneshot::Sender>), - SendMessage( - ServiceId, - Box, - u64, - oneshot::Sender>, - ), - SendMessageToGroup( - Vec, - Box, - u64, - oneshot::Sender>, - ), - GetAttachment(AttachmentPointer, oneshot::Sender, Error>>), - UploadAttachments( - Vec<(AttachmentSpec, Vec)>, - oneshot::Sender>, Error>>, - ), - Messages( - Thread, - (Bound, Bound), - oneshot::Sender< - Result< - ::MessagesIter, - Error, - >, - >, - ), - RegistrationType(oneshot::Sender), - LinkSecondary(Url, oneshot::Sender>), - UnlinkSecondary(DeviceId, oneshot::Sender>), - Devices(oneshot::Sender, Error>>), - RequestContacts(oneshot::Sender>), - RetrieveProfileAvatarByUuid( - Uuid, - ProfileKey, - oneshot::Sender>, Error>>, - ), - RetrieveGroupAvatar( - GroupContextV2, - oneshot::Sender>, Error>>, - ), -} - -/// Message from the UI to the manager thread how to do setup. -#[derive(Debug)] -pub enum SetupDecision { - /// Server, Device Name - Link(SignalServers, String), - /// Server, Phone Number, Captcha - Register(SignalServers, phonenumber::PhoneNumber, String), -} - -pub type SetupConfirmation = String; - -/// Message from the manager thread to the UI on what steps need to be taken for setting up the device. -#[derive(Debug)] -pub enum SetupResult { - /// Setup must make a decision. Either register as primary device or link device. - Pending(OnceCell>), - /// The manager is pending a SMS confirmation code. - Confirm(OnceCell>), - /// Display QR code to link. - DisplayLinkQR(Url), - /// Everything is finished - Finished, -} - -impl std::fmt::Debug for Command { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Uuid(_) => f.debug_tuple("Uuid").field(&format_args!("_")).finish(), - _ => f.write_str("Unknown"), - } - } -} - -pub struct ManagerThread { - /// Sending commands to the thread. - command_sender: mpsc::Sender, - /// Cache for how the device is registered (primary or secondary). - registration_type: Option, - /// Cache for the own UUID. - uuid: Uuid, - /// Cache for the own profile. - profile: Option, -} - -impl Clone for ManagerThread { - fn clone(&self) -> Self { - Self { - command_sender: self.command_sender.clone(), - registration_type: self.registration_type.clone(), - uuid: self.uuid, - profile: self.profile.clone(), - } - } -} - -impl ManagerThread { - /// Construct a new [ManagerThread]. - /// - /// The parameters are as follows: - /// - /// - `config_store`: The [Store] to store data to. - /// - `setup_callback`: If setup is required, the required steps will be sent over this channel. - /// - `error_callback`: If an error during setup happens, it will be sent through this channel. If no error happened, the channel will be closed. - /// - `content`: The messages received by the thread. - /// - `error`: Any errors receiving messages. - pub async fn new( - config_store: Store, - setup_callback: futures::channel::mpsc::Sender, - error_callback: futures::channel::oneshot::Sender, - content: mpsc::UnboundedSender, - error: mpsc::Sender, - ) -> Option { - let (mut sender, receiver) = mpsc::channel(MESSAGE_BOUND); - let thread = std::thread::Builder::new() - .name("ManagerThread".into()) - // Note: Increased stack size required, otherwise this thread can use too much of it and crash Flare. - .stack_size(8 * 1024 * 1024); - let _ = thread.spawn(move || { - let mut error_clone = error.clone(); - let panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - crate::TOKIO_RUNTIME.block_on(async move { - // XXX: Make sure the initial sync is finished (requires upstream presage changes). - // See https://github.com/whisperfish/presage/pull/212. - let manager_receive = setup_manager(config_store, setup_callback).await; - if let Ok(manager_receive) = manager_receive { - log::trace!("Starting command loop"); - drop(error_callback); - command_loop(manager_receive, receiver, content, error).await; - } else { - let e = manager_receive.err().unwrap(); - log::trace!("Got error: {}", e); - error_callback.send(e).expect("Failed to send error") - } - }); - })); - if let Err(_e) = panic { - log::error!("Manager-thread paniced"); - tokio::runtime::Runtime::new() - .expect("Failed to setup runtime") - .block_on(async move { - error_clone - .send(ApplicationError::ManagerThreadPanic) - .await - .expect("Failed to send error"); - }); - } - }); - - // Fill cache - - let (sender_uuid, receiver_uuid) = oneshot::channel(); - if sender.send(Command::Uuid(sender_uuid)).await.is_err() { - return None; - } - let uuid = receiver_uuid.await; - - if uuid.is_err() { - return None; - } - - let (sender_registration_type, receiver_registration_type) = oneshot::channel(); - if sender - .send(Command::RegistrationType(sender_registration_type)) - .await - .is_err() - { - return None; - } - let registration_type = receiver_registration_type.await; - - if registration_type.is_err() { - return None; - } - - let (sender_profile, receiver_profile) = oneshot::channel(); - if sender - .send(Command::RetrieveProfile(sender_profile)) - .await - .is_err() - { - return None; - } - let profile = receiver_profile.await; - - if profile.is_err() { - return None; - } - - Some(Self { - command_sender: sender, - uuid: uuid.unwrap(), - registration_type: Some(registration_type.unwrap()), - profile: profile.unwrap().ok(), - }) - } -} - -// These methods mostly mimic the [presage::Manager]. -impl ManagerThread { - pub fn uuid(&self) -> Uuid { - self.uuid - } - - pub fn registration_type(&self) -> Option { - self.registration_type.clone() - } - - pub async fn submit_recaptcha_challenge( - &self, - token: String, - captcha: String, - ) -> Result<(), Error> { - let (sender, receiver) = oneshot::channel(); - self.command_sender - .clone() - .send(Command::SubmitRecaptchaChallenge(token, captcha, sender)) - .await - .expect("Command sending failed"); - receiver.await.expect("Callback receiving failed") - } - - pub async fn retrieve_profile_by_uuid( - &self, - uuid: Uuid, - profile_key: ProfileKey, - ) -> Result { - let (sender, receiver) = oneshot::channel(); - self.command_sender - .clone() - .send(Command::RetrieveProfileByUuid(uuid, profile_key, sender)) - .await - .expect("Command sending failed"); - receiver.await.expect("Callback receiving failed") - } - - pub fn retrieve_profile(&self) -> Option { - self.profile.clone() - } - - pub async fn get_group_v2( - &self, - group_master_key: [u8; 32], - ) -> Result, ::Error> { - let (sender, receiver) = oneshot::channel(); - self.command_sender - .clone() - .send(Command::GetGroupV2(group_master_key, sender)) - .await - .expect("Command sending failed"); - receiver.await.expect("Callback receiving failed") - } - - pub async fn send_message( - &self, - recipient_addr: impl Into, - message: impl Into, - timestamp: u64, - ) -> Result<(), Error> { - let (sender, receiver) = oneshot::channel(); - self.command_sender - .clone() - .send(Command::SendMessage( - recipient_addr.into(), - Box::new(message.into()), - timestamp, - sender, - )) - .await - .expect("Command sending failed"); - receiver.await.expect("Callback receiving failed") - } - - pub async fn send_session_reset( - &self, - recipient_addr: impl Into, - timestamp: u64, - ) -> Result<(), Error> { - let (sender, receiver) = oneshot::channel(); - self.command_sender - .clone() - .send(Command::SendSessionReset( - recipient_addr.into(), - timestamp, - sender, - )) - .await - .expect("Command sending failed"); - receiver.await.expect("Callback receiving failed") - } - - pub async fn send_message_to_group( - &self, - group_key: Vec, - message: DataMessage, - timestamp: u64, - ) -> Result<(), Error> { - let (sender, receiver) = oneshot::channel(); - self.command_sender - .clone() - .send(Command::SendMessageToGroup( - group_key, - Box::new(message), - timestamp, - sender, - )) - .await - .expect("Command sending failed"); - receiver.await.expect("Callback receiving failed") - } - - pub async fn get_attachment( - &self, - attachment_pointer: &AttachmentPointer, - ) -> Result, Error> { - let (sender, receiver) = oneshot::channel(); - self.command_sender - .clone() - .send(Command::GetAttachment(attachment_pointer.clone(), sender)) - .await - .expect("Command sending failed"); - receiver.await.expect("Callback receiving failed") - } - - pub async fn upload_attachments( - &self, - attachments: Vec<(AttachmentSpec, Vec)>, - ) -> Result>, Error> { - let (sender, receiver) = oneshot::channel(); - self.command_sender - .clone() - .send(Command::UploadAttachments(attachments, sender)) - .await - .expect("Command sending failed"); - receiver.await.expect("Callback receiving failed") - } - - pub async fn messages( - &self, - thread: Thread, - range: (Bound, Bound), - ) -> Result< - ::MessagesIter, - Error, - > { - let (sender, receiver) = oneshot::channel(); - self.command_sender - .clone() - .send(Command::Messages(thread, range, sender)) - .await - .expect("Command sending failed"); - receiver.await.expect("Callback receiving failed") - } - - pub async fn link_secondary(&self, url: Url) -> Result<(), Error> { - let (sender, receiver) = oneshot::channel(); - self.command_sender - .clone() - .send(Command::LinkSecondary(url, sender)) - .await - .expect("Command sending failed"); - receiver.await.expect("Callback receiving failed") - } - - pub async fn unlink_secondary(&self, id: DeviceId) -> Result<(), Error> { - let (sender, receiver) = oneshot::channel(); - self.command_sender - .clone() - .send(Command::UnlinkSecondary(id, sender)) - .await - .expect("Command sending failed"); - receiver.await.expect("Callback receiving failed") - } - - pub async fn devices(&self) -> Result, Error> { - let (sender, receiver) = oneshot::channel(); - self.command_sender - .clone() - .send(Command::Devices(sender)) - .await - .expect("Command sending failed"); - receiver.await.expect("Callback receiving failed") - } - - pub async fn request_contacts(&self) -> Result<(), Error> { - let (sender, receiver) = oneshot::channel(); - self.command_sender - .clone() - .send(Command::RequestContacts(sender)) - .await - .expect("Command sending failed"); - receiver.await.expect("Callback receiving failed") - } - - pub async fn retrieve_profile_avatar_by_uuid( - &self, - uuid: Uuid, - profile_key: ProfileKey, - ) -> Result>, Error> { - let (sender, receiver) = oneshot::channel(); - self.command_sender - .clone() - .send(Command::RetrieveProfileAvatarByUuid( - uuid, - profile_key, - sender, - )) - .await - .expect("Command sending failed"); - receiver.await.expect("Callback receiving failed") - } - - pub async fn retrieve_group_avatar( - &self, - context: GroupContextV2, - ) -> Result>, Error> { - let (sender, receiver) = oneshot::channel(); - self.command_sender - .clone() - .send(Command::RetrieveGroupAvatar(context, sender)) - .await - .expect("Command sending failed"); - receiver.await.expect("Callback receiving failed") - } -} - -/// Setting up the manager if required. -async fn setup_manager( - config_store: Store, - mut setup_sender: futures::channel::mpsc::Sender, -) -> Result, Error> { - if let Ok(manager) = presage::Manager::load_registered(config_store.clone()).await { - log::debug!("The configuration store is already valid, loading a registered account"); - setup_sender - .send(SetupResult::Finished) - .await - .expect("Failed to send setup results"); - Ok(manager) - } else { - log::debug!("The config store is not valid yet, sending decision via channel."); - - let (tx_decision, rx_decision) = futures::channel::oneshot::channel(); - let to_send = OnceCell::new(); - let _ = to_send.set(tx_decision); - setup_sender - .send(SetupResult::Pending(to_send)) - .await - .expect("Failed to send setup results"); - match rx_decision.await.expect("Callback receiving failed") { - SetupDecision::Link(servers, name) => { - let (tx_link, rx_link) = futures::channel::oneshot::channel(); - let (_, mut manager) = join!( - async { - let link = rx_link.await.expect("Failed to receive link callback"); - setup_sender - .send(SetupResult::DisplayLinkQR(link)) - .await - .expect("Failed to send setup results"); - }, - presage::Manager::link_secondary_device( - config_store.clone(), - servers, - name, - tx_link, - ) - ); - // Contact sync in current implementation might not - // be operational. The application does not handle this well. - // ex.: https://github.com/whisperfish/presage/issues/320 - // https://github.com/hoehermann/purple-presage/issues/40 - // Most importantly, the method was never reliable: - // ex.: https://github.com/AsamK/signal-cli/issues/1293 - // Working implementations of the protocol appear to rely mostly - // on SyncStorage with shared master key besides SyncMessage. - // https://signal.miraheze.org/wiki/Storage_Service - if let Ok(manager) = &mut manager - && let Err(e) = manager.request_contacts().await - { - log::error!("Failed to sync contacts after linking: {}", e); - } - setup_sender - .send(SetupResult::Finished) - .await - .expect("Failed to send setup results"); - manager - } - SetupDecision::Register(servers, phonenumber, captcha) => { - let (tx_confirm, rx_confirm) = futures::channel::oneshot::channel(); - let manager = presage::Manager::register( - config_store.clone(), - RegistrationOptions { - signal_servers: servers, - phone_number: phonenumber, - use_voice_call: false, - captcha: Some(&captcha[..]), - force: false, - }, - ) - .await?; - // TODO: Error handling? - setup_sender - .send(SetupResult::Confirm(tx_confirm.into())) - .await - .expect("Failed to send setup results"); - let confirmation = rx_confirm - .await - .expect("Failed to receive confirm callback"); - let manager = manager.confirm_verification_code(&confirmation).await; - setup_sender - .send(SetupResult::Finished) - .await - .expect("Failed to send setup results"); - manager - } - } - } -} - -/// The command loop where messages are received and commands from the UI thread are executed. -async fn command_loop( - manager: Manager, - mut receiver: mpsc::Receiver, - mut content: mpsc::UnboundedSender, - mut error: mpsc::Sender, -) { - let manager = Arc::new(RwLock::new(manager)); - let local = tokio::task::LocalSet::new(); - local.run_until(async move { - 'outer: loop { - let msgs: Result<_, presage::Error<::Error>> = manager - .write() - .await - .receive_messages() - .await; - - let (need_loop_restart_sender, mut need_loop_restart_receiver) = mpsc::unbounded(); - let network_monitor = gio::NetworkMonitor::default(); - network_monitor.connect_network_available_notify(clone!( - #[strong] - need_loop_restart_sender, - move |m| { - if m.is_network_available() { - let _ = need_loop_restart_sender.unbounded_send(()); - } - } - )); - - match msgs { - Ok(messages) => { - futures::pin_mut!(messages); - let mut next_msg = messages.next().fuse(); - - loop { - let msg = next_msg.await; - if let Some(msg) = msg { - if matches!(&msg, Received::QueueEmpty) { - log::info!("Got queue empty signal from presage; start to allow commands."); - if content.send(msg).await.is_err() { - log::info!("Failed to send message to `Manager`, exiting") - } - break; - } else if content.send(msg).await.is_err() { - log::info!("Failed to send message to `Manager`, exiting"); - break 'outer; - } - } else { - log::error!("Message stream finished. Restarting command loop."); - continue 'outer; - } - next_msg = messages.next().fuse(); - } - - let mut next_msg = messages.next().fuse(); - - loop { - select! { - // Receiving a message. - msg = next_msg => { - if let Some(msg) = msg { - if content.send(msg).await.is_err() { - log::info!("Failed to send message to `Manager`, exiting"); - break 'outer; - } - } else { - log::error!("Message stream finished. Restarting command loop."); - break; - } - next_msg = messages.next().fuse(); - }, - // Receiving a command. - cmd = receiver.next() => { - if let Some(cmd) = cmd { - tokio::task::spawn_local(handle_command(manager.clone(), cmd)); - } - }, - // The network status changed; restart the loop to restart the signal websockets. - _ = need_loop_restart_receiver.next() => { - log::trace!("Network changed. Restarting command loop."); - break; - }, - complete => { - log::trace!("Command loop complete. Restarting command loop."); - break - }, - } - } - } - Err(e) => { - log::error!("Got error receiving: {}, {:?}", e, e); - let e = e.into(); - // Don't send no-internet errors, Flare is able to handle them automatically. - if !matches!(e, ApplicationError::NoInternet) { - error.send(e).await.expect("Callback sending failed"); - } - - // Handle all commands while sleeping; this ensures Flare can start up without internet. - loop { - select! { - cmd = receiver.next().fuse() => { - if let Some(cmd) = cmd { - tokio::task::spawn_local(handle_command(manager.clone(), cmd)); - } - }, - _ = tokio::time::sleep(std::time::Duration::from_secs(OFFLINE_SLEEP_TIMEOUT)).fuse() => { - break; - } - } - } - } - } - log::debug!("Websocket closed, trying again"); - } - log::info!("Exiting `ManagerThread::command_loop`"); - }).await; -} - -async fn handle_command(manager: Arc>>, command: Command) { - log::trace!("Got command: {:#?}", command); - match command { - // XXX: Uuid should not be used anymore. - // XXX: Don't use nil. - Command::Uuid(callback) => callback - .send( - manager - .read() - .await - .registration_data() - .service_ids - .aci() - .into(), - ) - .expect("Callback sending failed"), - Command::SubmitRecaptchaChallenge(token, captcha, callback) => callback - .send( - manager - .read() - .await - .submit_recaptcha_challenge(&token, &captcha) - .await, - ) - .expect("Callback sending failed"), - Command::RetrieveProfileByUuid(uuid, profile_key, callback) => callback - .send( - manager - .write() - .await - .retrieve_profile_by_uuid(uuid, profile_key) - .await, - ) - .expect("Callback sending failed"), - Command::RetrieveProfile(callback) => callback - .send(manager.write().await.retrieve_profile().await) - .expect("Callback sending failed"), - Command::GetGroupV2(master_key, callback) => callback - .send(manager.read().await.store().group(master_key).await) - .map_err(|_| ()) - .expect("Callback sending failed"), - Command::SendSessionReset(recipient_address, timestamp, callback) => callback - .send( - manager - .write() - .await - .send_session_reset(&recipient_address, timestamp) - .await, - ) - .expect("Callback sending failed"), - Command::SendMessage(recipient_address, message, timestamp, callback) => callback - .send( - manager - .write() - .await - .send_message(recipient_address, *message, timestamp) - .await, - ) - .expect("Callback sending failed"), - Command::SendMessageToGroup(group_key, message, timestamp, callback) => callback - .send( - manager - .write() - .await - .send_message_to_group(&group_key[..], *message, timestamp) - .await, - ) - .expect("Callback sending failed"), - Command::GetAttachment(attachment, callback) => callback - .send(manager.read().await.get_attachment(&attachment).await) - .expect("Callback sending failed"), - Command::UploadAttachments(attachments, callback) => callback - .send(manager.read().await.upload_attachments(attachments).await) - .expect("Callback sending failed"), - Command::Messages(thread, range, callback) => { - // XXX: Cannot format iterator. - let _ = callback.send( - manager - .read() - .await - .store() - .messages(&thread, range) - .await - .map_err(presage::Error::Store), - ); - } - Command::RegistrationType(callback) => callback - .send(manager.read().await.registration_type()) - .expect("Callback sending failed"), - Command::LinkSecondary(url, callback) => callback - .send(manager.write().await.link_secondary(url).await) - .expect("Callback sending failed"), - Command::UnlinkSecondary(id, callback) => callback - .send(manager.read().await.unlink_secondary(id).await) - .expect("Callback sending failed"), - Command::Devices(callback) => callback - .send(manager.read().await.devices().await) - .expect("Callback sending failed"), - Command::RequestContacts(callback) => callback - .send(manager.write().await.request_contacts().await) - .expect("Callback sending failed"), - Command::RetrieveProfileAvatarByUuid(uuid, profile_key, callback) => callback - .send( - manager - .write() - .await - .retrieve_profile_avatar_by_uuid(uuid, profile_key) - .await, - ) - .expect("Callback sending failed"), - Command::RetrieveGroupAvatar(context, callback) => callback - .send(manager.write().await.retrieve_group_avatar(context).await) - .expect("Callback sending failed"), - } -} diff --git a/src/backend/message.rs b/src/backend/message.rs new file mode 100644 index 0000000..9478bed --- /dev/null +++ b/src/backend/message.rs @@ -0,0 +1,464 @@ +use super::{Contact, Manager}; +use crate::backend::{Attachment, Channel}; +use crate::prelude::*; +use ftypes::{AmendmentMessage, MessageId, ReactionOperation}; +use gtk::pango::{AttrColor, AttrFontDesc, AttrInt, AttrList, Attribute, FontDescription, Weight}; + +use glib::{ + Object, + subclass::{prelude::ObjectImpl, types::ObjectSubclassIsExt}, +}; + +const MENTION_CHAR: char = '@'; +const MENTION_COLOR: (u16, u16, u16) = (0, 0, u16::MAX); + +/// At least 4 minutes need to pass such that for two messages from the same sender, the second one will +/// also show avatar and sender title. +const MESSAGE_SENT_SHOW_NAME_DURATION: u64 = 4 * 60 * 1000; +/// At least 1 minute need to pass such that for two messages from the same sender, the second one will +/// also show the timestamp. +const MESSAGE_SENT_SHOW_TIMESTAMP_DURATION: u64 = 60 * 1000; + +glib::wrapper! { + pub struct Message(ObjectSubclass); +} + +impl Message { + pub(super) async fn from_ftypes( + inner: ftypes::RichMessage, + manager: &Manager, + ) -> Result { + let channel = manager + .channel_from_id(inner.thread) + .await? + .expect("Message to have a channel"); + let contact = manager + .contact_from_id(inner.sender()) + .await? + .expect("Message to have a contact"); + + let quote = match &inner.content.inner { + ftypes::MainMessageContent::Text(text_message) => text_message.quote.as_ref(), + _ => None, + }; + let quote = if let Some(quote) = quote { + // TODO: Error? + Box::pin(manager.message(quote.quoted_message)) + .await + .ok() + .flatten() + } else { + None + }; + + let s = Object::builder::() + .property("manager", manager) + .property("sender", &contact) + .property("channel", &channel) + .property("quote", "e) + .property("read", channel.property::("is-active")) + .build(); + s.imp().inner.borrow_mut().replace(inner); + s.init_data().await; + Ok(s) + } + + pub(super) fn add_amendment(&self, amendment: &AmendmentMessage) { + let inner = self.inner(); + *self.imp().inner.borrow_mut() = Some(inner.amend(amendment.clone())); + gspawn!(clone!( + #[strong(rename_to = s)] + self, + #[strong] + amendment, + async move { + s.init_data().await; + s.notify_changed(); + + if let ftypes::AmendmentMessageContent::Reaction(reaction) = &amendment.content + && reaction.operation == ReactionOperation::Add + && s.sender().is_self() + && s.manager().settings().boolean("notify-reactions") + { + let manager = s.manager(); + + let Ok(Some(channel)) = manager.channel_from_id(amendment.thread).await else { + return; + }; + let Ok(Some(sender)) = manager.contact_from_id(amendment.sender()).await else { + return; + }; + manager.send_notification_for_channel( + None, + channel, + sender, + gettextrs::gettext("Reacted {} to a message") + .replace("{}", &reaction.emoji), + ); + } + } + )); + } + + pub(super) fn remove_amendment(&self, amendment: &AmendmentMessage) { + let inner = self.inner(); + *self.imp().inner.borrow_mut() = Some(inner.remove_amendment(amendment.clone())); + gspawn!(clone!( + #[strong(rename_to = s)] + self, + async move { + s.init_data().await; + s.notify_changed(); + } + )); + } + + fn notify_changed(&self) { + self.notify_is_deleted(); + self.channel().notify_last_message(); + self.notify_textual_description(); + self.notify_body(); + self.notify_reactions(); + self.notify_has_reactions(); + self.notify_is_unsupported(); + } + + // A unique ID of the message, built from the timestamp and sender. + pub fn uid(&self) -> Option { + let timestamp = self.timestamp(); + let sender_uuid = self.sender().account_id().0.raw_uuid(); + Some(format!("{timestamp:x}{sender_uuid:x}")) + } + + pub fn inner(&self) -> ftypes::RichMessage { + self.imp() + .inner + .borrow() + .clone() + .expect("Message to have an inner ftypes message") + } + + pub fn mark_as_read(&self) -> bool { + let this = self.imp(); + let read = *this.read.borrow(); + if !read { + this.read.replace(true); + self.notify("read"); + return true; + } + false + } + + fn text_message(&self) -> Option { + let ftypes::MainMessageContent::Text(message) = self.inner().content.inner else { + return None; + }; + Some(message) + } + + pub(super) async fn init_data(&self) { + let obj = self.imp(); + self.prepare_format_body().await; + + // Load attachments in parallel. + // TODO: Stickers + let attachment_futures = self + .text_message() + .into_iter() + .flat_map(|m| m.attachments) + .map(|pointer| async move { Attachment::from_ftypes(&pointer, &self.manager()).await }); + let attachments = futures::future::join_all(attachment_futures).await; + obj.attachments.swap(&RefCell::new(attachments)); + } + + pub fn attachments(&self) -> Vec { + self.imp().attachments.borrow().clone() + } + + pub fn quote_timestamp(&self) -> Option { + self.quote().map(|q| q.timestamp()) + } + + async fn prepare_format_body(&self) { + let (body, attrs) = self.format_body().await; + *self.imp().formatted_body.borrow_mut() = body; + *self.imp().message_attributes.borrow_mut() = attrs; + self.notify_message_attributes(); + self.notify_body(); + } + + /// Formats the message body based on its ranges, e.g. to insert mention names. + /// + /// Returns the resulting strings and an [AttrList] that can be used in labels to highlight areas. + async fn format_body(&self) -> (Option, AttrList) { + let Some(text_message) = self.text_message() else { + return (None, AttrList::new()); + }; + let Some(mut body) = text_message.body.clone() else { + return (None, AttrList::new()); + }; + let mut ranges = text_message.body_ranges.clone(); + + if ranges.is_empty() { + return (Some(body), AttrList::new()); + } + + ranges.sort_unstable_by_key(|r| r.start); + + let attrs = AttrList::new(); + + let mut offset = 0; + for r in ranges { + match r.associated_value { + ftypes::AssociatedValue::Mention(contact_id) => { + // TODO: Error? + let Ok(Some(contact)) = self.manager().contact_from_id(contact_id).await else { + log::trace!( + "Body got a mention for which the contact could not be found; ignoring" + ); + continue; + }; + let title = contact.title(); + let replacement = format!("{}{}", MENTION_CHAR, title); + let index_start = r.start + offset; + let index_end = r.start + r.length + offset; + body.replace_range(index_start..index_end, &replacement); + + let index_start_highlight = index_start; + let index_end_highlight = index_start_highlight + replacement.len(); + let (red, green, blue) = MENTION_COLOR; + let mut highlight = AttrColor::new_foreground(red, green, blue); + highlight.set_start_index(index_start_highlight as u32); + highlight.set_end_index(index_end_highlight as u32); + attrs.insert(highlight); + + offset += replacement.len() - r.length; + } + ftypes::AssociatedValue::Style(style) => { + let attr: Option = match style { + ftypes::BodyStyle::None => None, + ftypes::BodyStyle::Bold => { + Some((*AttrInt::new_weight(Weight::Bold)).clone()) + } + ftypes::BodyStyle::Italic => Some( + (*AttrFontDesc::new(&FontDescription::from_string("italic"))).clone(), + ), + ftypes::BodyStyle::Spoiler => None, + ftypes::BodyStyle::Strikethrough => { + Some((*AttrInt::new_strikethrough(true)).clone()) + } + ftypes::BodyStyle::Monospace => Some( + (*AttrFontDesc::new(&FontDescription::from_string("monospace"))) + .clone(), + ), + }; + + if let Some(mut attr) = attr { + let index_start_highlight = r.start + offset; + let index_end_highlight = index_start_highlight + r.length; + attr.set_start_index(index_start_highlight as u32); + attr.set_end_index(index_end_highlight as u32); + attrs.insert(attr); + } + } + } + } + + (Some(body), attrs) + } + + /// Send a notification if needed. + /// + /// Sending a notification is skipped if: + /// - The message is from self, or + /// - The message has no textual description + pub fn send_notification(&self) { + self.manager().send_notification_for_channel( + self.uid(), + self.channel(), + self.sender(), + self.textual_description().unwrap_or_default(), + ); + } + + pub fn flash_requires_attention(&self) { + self.set_property("requires-attention", true); + gspawn!(clone!( + #[strong(rename_to = s)] + self, + async move { + glib::timeout_future_seconds(2).await; + s.set_property("requires-attention", false); + } + )); + } + + pub fn id(&self) -> MessageId { + self.inner().id + } + + /// Days since 01.01.1970 + pub fn day_timestamp(&self) -> u64 { + self.timestamp() / (1000 * 60 * 60 * 24) + } + + pub fn update_show_header(&self, previous: Option<&Message>) { + if let Some(msg) = previous.and_then(|p| p.downcast_ref::()) { + // Show header if: + // - The sender of the previous message is different to the sender of this message, or + // - Some time has passed between the two messages. + // Furthermore, never show headers for messages sent by self. + self.set_show_header( + (self.sender().account_id() != msg.sender().account_id() + || self.timestamp() >= msg.timestamp() + MESSAGE_SENT_SHOW_NAME_DURATION) + && self.sender().account_id() != self.manager().account_id(), + ); + } else { + self.set_show_header(true); + } + } + + pub fn update_show_timestamp(&self, next: Option<&Message>) { + if let Some(msg) = next.and_then(|p| p.downcast_ref::()) { + // Show timestamp if: + // - The sender of the previous message is different to the sender of this message, or + // - Some time has passed between the two messages. + self.set_show_timestamp( + self.sender().account_id() != msg.sender().account_id() + || msg.timestamp() >= self.timestamp() + MESSAGE_SENT_SHOW_TIMESTAMP_DURATION, + ); + } else { + self.set_show_timestamp(true); + } + } +} + +mod imp { + use std::marker::PhantomData; + + use gtk::pango::AttrList; + + use crate::backend::{Attachment, Manager}; + + use super::*; + + #[derive(Debug, Default, glib::Properties)] + #[properties(wrapper_type = super::Message)] + pub struct Message { + #[property(get, set, construct_only, type = Contact)] + sender: RefCell>, + #[property(get, set, construct_only, type = Channel)] + channel: RefCell>, + #[property(get, set, construct_only)] + pub(super) read: RefCell, + #[property(get, set)] + pub(super) pending: RefCell, + #[property(get, set)] + pub(super) error: RefCell, + + #[property(name = "timestamp", type = u64, get = |s: &Self| s.inner.borrow().as_ref().map(|m| m.id.timestamp.timestamp_millis()).unwrap_or_default() as u64)] + #[property(name = "reactions", get = Self::reactions, type = String, default = Some(""))] + #[property(name = "has-reactions", get = Self::has_reactions, type = bool)] + #[property(name = "is-unsupported", get = Self::is_unsupported, type = bool)] + #[property(name = "is-deleted", get = Self::is_deleted, type = bool)] + pub(super) inner: RefCell>, + + #[property(get, set, construct_only, type = Manager)] + manager: RefCell>, + + #[property(get, set, nullable)] + pub(super) quote: RefCell>, + + pub(super) attachments: RefCell>, + + #[property(get = Self::textual_description)] + pub(super) textual_description: PhantomData>, + #[property(name = "body", get)] + pub(super) formatted_body: RefCell>, + #[property(get)] + pub(super) message_attributes: RefCell, + #[property(get, set)] + pub(super) requires_attention: Cell, + + #[property(get, set)] + show_header: Cell, + #[property(get, set)] + show_timestamp: Cell, + + #[property(get = Self::datetime)] + datetime: PhantomData>, + } + + impl Message { + fn reactions(&self) -> String { + self.obj() + .inner() + .active_reactions() + .iter() + .map(|r| r.content.emoji.to_owned()) + .collect() + } + + fn has_reactions(&self) -> bool { + !self.obj().inner().active_reactions().is_empty() + } + + fn is_unsupported(&self) -> bool { + !matches!( + self.obj().inner().content.inner, + ftypes::MainMessageContent::Text(_) | ftypes::MainMessageContent::Deleted + ) + } + + fn is_deleted(&self) -> bool { + self.obj().inner().content.deletion.is_some() + } + + fn textual_description(&self) -> Option { + let obj = self.obj(); + if let Some(body) = obj.body() + && !body.is_empty() + { + body.lines().next().map(|s| s.to_owned()) + } else { + let attachments = self.attachments.borrow(); + + let formatter = if attachments.iter().all(|a| a.is_image()) { + gettextrs::ngettext("Sent an image", "Sent {} images", attachments.len() as u32) + } else if attachments.iter().all(|a| a.is_video()) { + gettextrs::ngettext("Sent an video", "Sent {} videos", attachments.len() as u32) + } else if attachments.iter().all(|a| a.is_audio()) { + gettextrs::ngettext( + "Sent a voice message", + "Sent {} voice messages", + attachments.len() as u32, + ) + } else { + gettextrs::ngettext("Sent a file", "Sent {} files", attachments.len() as u32) + }; + + Some(formatter.replace("{}", &attachments.len().to_string())) + } + } + + // Time as [glib::DateTime]. Should not return `None`, but just in case. + fn datetime(&self) -> Option { + glib::DateTime::from_unix_utc( + (self.obj().timestamp() / 1000) + .try_into() + .unwrap_or_default(), + ) + .ok() + .and_then(|d| d.to_local().ok()) + } + } + + #[glib::object_subclass] + impl ObjectSubclass for Message { + const NAME: &'static str = "FlMessage"; + type Type = super::Message; + } + + #[glib::derived_properties] + impl ObjectImpl for Message {} +} diff --git a/src/backend/message/call_message.rs b/src/backend/message/call_message.rs deleted file mode 100644 index 67f8218..0000000 --- a/src/backend/message/call_message.rs +++ /dev/null @@ -1,121 +0,0 @@ -use crate::prelude::*; - -use crate::backend::timeline::TimelineItem; - -use libsignal_service::content::CallMessage as PreCallMessage; - -use crate::backend::{Channel, Contact}; - -use super::{DisplayMessage, Manager, Message}; - -#[derive(Debug, Hash, Eq, PartialEq, Clone, Copy, glib::Enum, Default)] -#[repr(u32)] -#[enum_type(name = "FlCallMessageType")] -pub enum CallMessageType { - #[default] - Offer, - Answer, - Hangup, - Busy, -} - -impl TryFrom<&PreCallMessage> for CallMessageType { - type Error = (); - fn try_from(p: &PreCallMessage) -> Result { - if p.offer.is_some() { - Ok(CallMessageType::Offer) - } else if p.hangup.is_some() { - Ok(CallMessageType::Hangup) - } else if p.answer.is_some() { - Ok(CallMessageType::Answer) - } else if p.busy.is_some() { - Ok(CallMessageType::Busy) - } else { - Err(()) - } - } -} - -gtk::glib::wrapper! { - /// A CallMessage represents a call event of some type [CallMessageType]. - pub struct CallMessage(ObjectSubclass) @extends Message, DisplayMessage, TimelineItem; -} - -impl CallMessage { - pub fn from_call( - sender: &Contact, - channel: &Channel, - timestamp: u64, - manager: &Manager, - call: PreCallMessage, - ) -> Option { - let call_type = CallMessageType::try_from(&call).ok()?; - let s: Self = Object::builder::() - .property("sender", sender) - .property("channel", channel) - .property("timestamp", timestamp) - .property("manager", manager) - .property("call-type", call_type) - .build(); - s.imp().call.swap(&RefCell::new(Some(call))); - Some(s) - } -} - -mod imp { - use super::*; - - use crate::backend::Message; - use crate::backend::message::{ - DisplayMessage, MessageImpl, display_message::DisplayMessageImpl, - }; - use crate::backend::timeline::{TimelineItem, TimelineItemImpl}; - - #[derive(Default, glib::Properties)] - #[properties(wrapper_type = super::CallMessage)] - pub struct CallMessage { - pub(super) call: RefCell>, - - #[property(get, set, construct_only, default = CallMessageType::default(), builder(CallMessageType::default()))] - pub(super) call_type: RefCell, - } - - #[glib::object_subclass] - impl ObjectSubclass for CallMessage { - const NAME: &'static str = "FlCallMessage"; - type Type = super::CallMessage; - type ParentType = DisplayMessage; - } - - impl DisplayMessageImpl for CallMessage { - fn textual_description(&self, obj: &super::CallMessage) -> Option { - let sender: Contact = obj.property("sender"); - - match (obj.call_type(), sender.is_self()) { - (CallMessageType::Offer, false) => Some(gettextrs::gettext("Incoming call")), - (CallMessageType::Offer, true) => Some(gettextrs::gettext("Outgoing call")), - (CallMessageType::Answer, _) => Some(gettextrs::gettext("Call started")), - (CallMessageType::Hangup, _) => Some(gettextrs::gettext("Call ended")), - (CallMessageType::Busy, false) => Some(gettextrs::gettext("Call declined")), - (CallMessageType::Busy, true) => Some(gettextrs::gettext("Unanswered call")), - } - } - } - - impl TimelineItemImpl for CallMessage { - fn update_show_header(&self, obj: &Self::Type, previous: Option<&TimelineItem>) { - let upcast = obj.upcast_ref::(); - upcast.imp().update_show_header(upcast, previous); - } - - fn update_show_timestamp(&self, obj: &Self::Type, previous: Option<&TimelineItem>) { - let upcast = obj.upcast_ref::(); - upcast.imp().update_show_timestamp(upcast, previous); - } - } - - impl MessageImpl for CallMessage {} - - #[glib::derived_properties] - impl ObjectImpl for CallMessage {} -} diff --git a/src/backend/message/deletion_message.rs b/src/backend/message/deletion_message.rs deleted file mode 100644 index c2651f1..0000000 --- a/src/backend/message/deletion_message.rs +++ /dev/null @@ -1,65 +0,0 @@ -use crate::backend::timeline::TimelineItem; -use crate::backend::{Channel, Contact}; -use crate::prelude::*; - -use libsignal_service::proto::data_message::Delete; - -use super::{Manager, Message}; - -gtk::glib::wrapper! { - /// A deletion message is propagated to mark a message as deleted. - pub struct DeletionMessage(ObjectSubclass) @extends Message, TimelineItem; -} - -impl DeletionMessage { - pub fn from_delete( - sender: &Contact, - channel: &Channel, - timestamp: u64, - manager: &Manager, - deletion: Delete, - ) -> Self { - let s: Self = Object::builder::() - .property("sender", sender) - .property("channel", channel) - .property("timestamp", timestamp) - .property("manager", manager) - .build(); - s.imp().deletion.swap(&RefCell::new(Some(deletion))); - s - } - - pub fn deletion(&self) -> Delete { - self.imp() - .deletion - .borrow() - .expect("`DeletionMessage` to have a `Delete`") - } - - pub fn target_timestamp(&self) -> u64 { - self.deletion().target_sent_timestamp() - } -} - -mod imp { - use crate::backend::{Message, message::MessageImpl, timeline::TimelineItemImpl}; - use crate::prelude::*; - - use libsignal_service::proto::data_message::Delete; - - #[derive(Default)] - pub struct DeletionMessage { - pub(super) deletion: RefCell>, - } - - #[glib::object_subclass] - impl ObjectSubclass for DeletionMessage { - const NAME: &'static str = "FlDeletionMessage"; - type Type = super::DeletionMessage; - type ParentType = Message; - } - - impl TimelineItemImpl for DeletionMessage {} - impl MessageImpl for DeletionMessage {} - impl ObjectImpl for DeletionMessage {} -} diff --git a/src/backend/message/display_message.rs b/src/backend/message/display_message.rs deleted file mode 100644 index 4cd5e7f..0000000 --- a/src/backend/message/display_message.rs +++ /dev/null @@ -1,196 +0,0 @@ -use crate::prelude::*; - -use crate::backend::timeline::{TimelineItem, TimelineItemImpl}; - -use gdk_pixbuf::Pixbuf; -use gio::Notification; - -use super::{Message, MessageExt, MessageImpl}; - -glib::wrapper! { - /// A DisplayMessage is a message that should be shown to the user within the UI of Flare and which may also be notified about. - pub struct DisplayMessage(ObjectSubclass) @extends Message, TimelineItem; -} - -impl DisplayMessage { - /// Send a notification if needed. - /// - /// Sending a notification is skipped if: - /// - The message is from self, or - /// - The message has no textual description - pub fn send_notification(&self) { - let sender = self.sender(); - let channel = self.channel(); - let body = self.textual_description(); - if sender.is_self() || body.is_none() || body.as_ref().unwrap().is_empty() { - // Skip notifications for messages sent from self or empty messages. - return; - } - let (notification_title, notification_body) = if channel.group_context().is_some() { - ( - channel.title(), - format!("{}: {}", sender.title(), body.unwrap_or_default()), - ) - } else { - (sender.title(), body.unwrap_or_default()) - }; - - let icon = Pixbuf::from_resource("/icon.svg").expect("Flare to have an application icon"); - - let notification = Notification::new(¬ification_title); - notification.set_body(Some(¬ification_body)); - notification.set_icon(&icon); - notification.set_category(Some("im.received")); - notification.set_default_action_and_target_value( - "app.notification-clicked", - Some(&channel.notification_variant().to_variant()), - ); - - let manager = self.manager(); - let uid = self.uid(); - manager.send_notification(uid, ¬ification); - } -} - -pub trait DisplayMessageExt: 'static { - fn textual_description(&self) -> Option; - fn flash_requires_attention(&self); -} - -impl> DisplayMessageExt for O { - fn textual_description(&self) -> Option { - imp::display_message_textual_description(self.upcast_ref()) - } - - fn flash_requires_attention(&self) { - self.set_property("requires-attention", true); - gspawn!(clone!( - #[strong(rename_to = s)] - self, - async move { - glib::timeout_future_seconds(2).await; - s.set_property("requires-attention", false); - } - )); - } -} - -pub trait DisplayMessageImpl: ObjectImpl { - fn textual_description(&self, _obj: &Self::Type) -> Option { - None - } -} - -unsafe impl IsSubclassable for DisplayMessage -where - T: DisplayMessageImpl + MessageImpl + TimelineItemImpl, - T::Type: IsA + IsA + IsA, -{ - fn class_init(class: &mut glib::Class) { - Self::parent_class_init::(class.upcast_ref_mut()); - - let klass = class.as_mut(); - - klass.textual_description = textual_description_trampoline::; - } -} - -fn textual_description_trampoline(this: &DisplayMessage) -> Option -where - T: ObjectSubclass + DisplayMessageImpl, - T::Type: IsA, -{ - let this = this.downcast_ref::().unwrap(); - this.imp().textual_description(this) -} - -mod imp { - use crate::prelude::*; - - use gio::glib::ParamSpecBoolean; - use glib::{ - ParamSpec, ParamSpecString, - subclass::types::{ClassStruct, ObjectSubclass}, - }; - - use super::DisplayMessageExt; - use crate::backend::timeline::{TimelineItem, TimelineItemImpl}; - use crate::backend::{Message, message::MessageImpl}; - - #[repr(C)] - pub struct DisplayMessageClass { - pub parent_class: glib::Class, - pub textual_description: fn(&super::DisplayMessage) -> Option, - } - - unsafe impl ClassStruct for DisplayMessageClass { - type Type = DisplayMessage; - } - - pub(super) fn display_message_textual_description( - this: &super::DisplayMessage, - ) -> Option { - let klass = this.class(); - (klass.as_ref().textual_description)(this) - } - - #[derive(Debug, Default)] - pub struct DisplayMessage { - pub(super) requires_attention: Cell, - } - - #[glib::object_subclass] - impl ObjectSubclass for DisplayMessage { - const NAME: &'static str = "FlDisplayMessage"; - type Type = super::DisplayMessage; - type ParentType = Message; - type Class = DisplayMessageClass; - } - - impl ObjectImpl for DisplayMessage { - fn properties() -> &'static [ParamSpec] { - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![ - ParamSpecString::builder("textual-description") - .read_only() - .build(), - ParamSpecBoolean::builder("requires-attention").build(), - ] - }); - - PROPERTIES.as_ref() - } - - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - match pspec.name() { - "requires-attention" => self.requires_attention.set( - value - .get() - .expect("requires-attention parameter to be boolean"), - ), - _ => unimplemented!(), - } - } - - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - match pspec.name() { - "textual-description" => self.obj().textual_description().to_value(), - "requires-attention" => self.requires_attention.get().to_value(), - _ => unimplemented!(), - } - } - } - - impl TimelineItemImpl for DisplayMessage { - fn update_show_header(&self, obj: &Self::Type, previous: Option<&TimelineItem>) { - let upcast = obj.upcast_ref::(); - upcast.imp().update_show_header(upcast, previous); - } - fn update_show_timestamp(&self, obj: &Self::Type, previous: Option<&TimelineItem>) { - let upcast = obj.upcast_ref::(); - upcast.imp().update_show_timestamp(upcast, previous); - } - } - - impl MessageImpl for DisplayMessage {} -} diff --git a/src/backend/message/mod.rs b/src/backend/message/mod.rs deleted file mode 100644 index 11ccd7c..0000000 --- a/src/backend/message/mod.rs +++ /dev/null @@ -1,495 +0,0 @@ -mod call_message; -mod deletion_message; -mod display_message; -mod reaction_message; -mod text_message; - -pub use call_message::{CallMessage, CallMessageType}; -pub use deletion_message::DeletionMessage; -pub use display_message::{DisplayMessage, DisplayMessageExt}; -pub use reaction_message::ReactionMessage; -pub use text_message::TextMessage; - -use super::{ - Contact, Manager, - timeline::{TimelineItem, TimelineItemImpl}, -}; -use crate::backend::{Channel, channel::TypingNotification}; -use crate::prelude::*; - -use glib::{ - Object, - subclass::{ - prelude::ObjectImpl, - types::{IsSubclassable, ObjectSubclassIsExt}, - }, -}; -use std::cell::RefMut; - -use libsignal_service::proto::typing_message::Action; -use libsignal_service::{ - content::ContentBody, - proto::{DataMessage, SyncMessage, sync_message::Sent}, -}; - -/// At least 4 minutes need to pass such that for two messages from the same sender, the second one will -/// also show avatar and sender title. -const MESSAGE_SENT_SHOW_NAME_DURATION: u64 = 4 * 60 * 1000; -/// At least 1 minute need to pass such that for two messages from the same sender, the second one will -/// also show the timestamp. -const MESSAGE_SENT_SHOW_TIMESTAMP_DURATION: u64 = 60 * 1000; - -glib::wrapper! { - /// A generic message. Note that not every message needs to be displayed to the user, e.g. deletion messages. - /// - /// In general, a [Message] is just something holding [Content]. - pub struct Message(ObjectSubclass) - @extends TimelineItem; -} - -impl Message { - /// Big function to create all different message types from the Content. - pub(super) async fn from_content(content: Content, manager: &Manager) -> Option { - log::trace!("Trying to build a message from content"); - let metadata = &content.metadata; - - let body = &content.body; - let timestamp = metadata.timestamp; - - match body { - // A normal text message. - ContentBody::DataMessage(message) - if message.reaction.is_none() && message.delete.is_none() => - { - let channel = manager - .channel_from_uuid_or_group(metadata.sender, &message.group_v2) - .await; - - let contact = channel - .participant_by_uuid(metadata.sender.raw_uuid()) - .await; - - if contact.is_blocked() { - log::debug!("Got message from a blocked contact. Ignoring"); - return None; - } - - if message.body.is_none() - && message.attachments.is_empty() - && message.sticker.is_none() - { - return None; - } - let s: TextMessage = Object::builder::() - .property("manager", manager) - .property("sender", &contact) - .property("channel", &channel) - .property("timestamp", timestamp) - .property("read", channel.property::("is-active")) - .build(); - s.init_data(message, manager).await; - Some(s.upcast()) - } - // A normal text message, sent from another device. - ContentBody::SynchronizeMessage(SyncMessage { - sent: - Some( - sent @ Sent { - message: Some(message), - .. - }, - ), - .. - }) if message.reaction.is_none() && message.delete.is_none() => { - let channel = manager - .channel_from_uuid_or_group( - sent.parse_destination_service_id() - .unwrap_or(metadata.sender), - &message.group_v2, - ) - .await; - let contact = channel - .participant_by_uuid(metadata.sender.raw_uuid()) - .await; - if contact.is_blocked() { - log::debug!("Got message from a blocked contact. Ignoring"); - return None; - } - if message.body.is_none() - && message.attachments.is_empty() - && message.sticker.is_none() - { - return None; - } - let s: TextMessage = Object::builder::() - .property("manager", manager) - .property("sender", &contact) - .property("channel", &channel) - .property("timestamp", timestamp) - .property("read", channel.property::("is-active")) - .build(); - s.init_data(message, manager).await; - Some(s.upcast()) - } - // A reaction message. - ContentBody::DataMessage(message) if message.reaction.is_some() => { - let channel = manager - .channel_from_uuid_or_group(metadata.sender, &message.group_v2) - .await; - let contact = channel - .participant_by_uuid(metadata.sender.raw_uuid()) - .await; - if contact.is_blocked() { - log::debug!("Got message from a blocked contact. Ignoring"); - return None; - } - Some( - ReactionMessage::from_reaction( - &contact, - &channel, - timestamp, - manager, - message.reaction.as_ref().unwrap().clone(), - ) - .upcast(), - ) - } - // A reaction message, sent from another device. - ContentBody::SynchronizeMessage(SyncMessage { - sent: - Some( - sent @ Sent { - message: Some(message), - .. - }, - ), - .. - }) if message.reaction.is_some() => { - let channel = manager - .channel_from_uuid_or_group( - sent.parse_destination_service_id() - .unwrap_or(metadata.sender), - &message.group_v2, - ) - .await; - let contact = channel - .participant_by_uuid(metadata.sender.raw_uuid()) - .await; - if contact.is_blocked() { - log::debug!("Got message from a blocked contact. Ignoring"); - return None; - } - Some( - ReactionMessage::from_reaction( - &contact, - &channel, - timestamp, - manager, - message.reaction.as_ref().unwrap().clone(), - ) - .upcast(), - ) - } - // A deletion message. - ContentBody::DataMessage(message) if message.delete.is_some() => { - let channel = manager - .channel_from_uuid_or_group(metadata.sender, &message.group_v2) - .await; - let contact = channel - .participant_by_uuid(metadata.sender.raw_uuid()) - .await; - if contact.is_blocked() { - log::debug!("Got message from a blocked contact. Ignoring"); - return None; - } - log::trace!("Got a deletion message"); - Some( - DeletionMessage::from_delete( - &contact, - &channel, - timestamp, - manager, - *message.delete.as_ref().unwrap(), - ) - .upcast(), - ) - } - // A deletion message, sent from another device. - ContentBody::SynchronizeMessage(SyncMessage { - sent: - Some( - sent @ Sent { - message: Some(message), - .. - }, - ), - .. - }) if message.delete.is_some() => { - let channel = manager - .channel_from_uuid_or_group( - sent.parse_destination_service_id() - .unwrap_or(metadata.sender), - &message.group_v2, - ) - .await; - let contact = channel - .participant_by_uuid(metadata.sender.raw_uuid()) - .await; - if contact.is_blocked() { - log::debug!("Got message from a blocked contact. Ignoring"); - return None; - } - log::trace!("Got a deletion message"); - Some( - DeletionMessage::from_delete( - &contact, - &channel, - timestamp, - manager, - *message.delete.as_ref().unwrap(), - ) - .upcast(), - ) - } - // Call message. - ContentBody::CallMessage(c) => { - // TODO: Group calls? - let channel = manager - .channel_from_uuid_or_group(metadata.sender, &None) - .await; - let contact = channel - .participant_by_uuid(metadata.sender.raw_uuid()) - .await; - if contact.is_blocked() { - log::debug!("Got message from a blocked contact. Ignoring"); - return None; - } - CallMessage::from_call(&contact, &channel, timestamp, manager, c.clone()) - .map(|c| c.upcast()) - } - // Typing messages. - // Note that they are currently only implemented for contacts, this requires upstream updates to fix. - ContentBody::TypingMessage(t) => { - 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, - ) - }; - - let Some(channel) = channel else { - log::warn!("Got typing message from channel that cannot be found; aborting"); - return None; - }; - - let contact = channel - .participant_by_uuid(metadata.sender.raw_uuid()) - .await; - if contact.is_blocked() { - log::debug!("Got message from a blocked contact. Ignoring"); - } else { - match t.action() { - Action::Started => channel.add_user_typing(TypingNotification { - sender: contact, - timestamp: t.timestamp(), - }), - Action::Stopped => channel.remove_user_typing(contact), - }; - } - None - } - - // Currently unhandled messages - ContentBody::SynchronizeMessage(SyncMessage { read: read_arr, .. }) - if !read_arr.is_empty() => - { - log::trace!("Got currently unhandled read-message"); - for r in read_arr { - let Some(timestamp) = r.timestamp else { - continue; - }; - let Some(sender_uuid) = r.parse_sender_aci().map(Uuid::from) else { - continue; - }; - // Note: Matches the implementation of Message:uid. - let notification_id = format!("{timestamp:x}{sender_uuid:x}"); - manager.withdraw_notification(¬ification_id); - } - - None - } - ContentBody::SynchronizeMessage(SyncMessage { - viewed: viewed_arr, .. - }) if !viewed_arr.is_empty() => { - log::trace!("Got currently unhandled viewed-message"); - None - } - ContentBody::ReceiptMessage(_) => { - log::trace!("Got currently unhandled receipt-message"); - None - } - _ => { - log::debug!("Do not know what to do with the message: {:?}", content); - None - } - } - } - - // A unique ID of the message, built from the timestamp and sender. - pub fn uid(&self) -> Option { - let data = self.imp().data.borrow().clone()?; - let timestamp = data.timestamp?; - let sender_uuid = self.sender().uuid(); - Some(format!("{timestamp:x}{sender_uuid:x}")) - } - - fn set_internal_data(&self, data: Option) { - self.imp().data.swap(&RefCell::new(data)); - } - - fn internal_data(&self) -> Option { - self.imp().data.borrow().clone() - } - - fn internal_data_mut(&self) -> RefMut<'_, Option> { - self.imp().data.borrow_mut() - } - - fn mark_as_read(&self) -> bool { - let this = self.imp(); - let read = *this.read.borrow(); - if !read { - this.read.replace(true); - self.notify("read"); - return true; - } - false - } -} - -pub trait MessageExt: std::marker::Sized + glib::prelude::ObjectExt { - fn uid(&self) -> Option { - self.dynamic_cast_ref::() - .expect("`MessageExt` to dynamic cast to `Message`") - .uid() - } - - fn set_internal_data(&self, data: Option) { - self.dynamic_cast_ref::() - .expect("`MessageExt` to dynamic cast to `Message`") - .set_internal_data(data) - } - - fn internal_data(&self) -> Option { - self.dynamic_cast_ref::() - .expect("`MessageExt` to dynamic cast to `Message`") - .internal_data() - } - - fn internal_data_mut(&self) -> RefMut<'_, Option> { - self.dynamic_cast_ref::() - .expect("`MessageExt` to dynamic cast to `Message`") - .internal_data_mut() - } - - fn sender(&self) -> Contact { - self.property("sender") - } - - fn channel(&self) -> Channel { - self.property("channel") - } - - fn manager(&self) -> Manager { - self.property("manager") - } - - fn mark_as_read(&self) -> bool { - self.dynamic_cast_ref::() - .expect("`MessageExt` to dynamic cast to `Message`") - .mark_as_read() - } -} - -impl> MessageExt for O {} - -pub trait MessageImpl: ObjectImpl {} - -unsafe impl IsSubclassable for Message -where - T: MessageImpl + TimelineItemImpl, - T::Type: IsA + IsA, -{ -} - -mod imp { - use crate::backend::{Manager, timeline::TimelineItemExt}; - - use super::*; - - #[derive(Debug, Default, glib::Properties)] - #[properties(wrapper_type = super::Message)] - pub struct Message { - #[property(get, set, construct_only, type = Contact)] - sender: RefCell>, - #[property(get, set, construct_only, type = Channel)] - channel: RefCell>, - #[property(get, set, construct_only)] - pub(super) read: RefCell, - #[property(get, set)] - pub(super) pending: RefCell, - #[property(get, set)] - pub(super) error: RefCell, - - pub(super) data: RefCell>, - - #[property(get, set, construct_only, type = Manager)] - manager: RefCell>, - } - - #[glib::object_subclass] - impl ObjectSubclass for Message { - const NAME: &'static str = "FlMessage"; - type Type = super::Message; - type ParentType = TimelineItem; - } - - #[glib::derived_properties] - impl ObjectImpl for Message {} - - impl TimelineItemImpl for Message { - fn update_show_header(&self, obj: &Self::Type, previous: Option<&TimelineItem>) { - if let Some(msg) = previous.and_then(|p| p.downcast_ref::()) { - // Show header if: - // - The sender of the previous message is different to the sender of this message, or - // - Some time has passed between the two messages. - // Furthermore, never show headers for messages sent by self. - obj.set_show_header( - (obj.sender().uuid() != msg.sender().uuid() - || obj.timestamp() >= msg.timestamp() + MESSAGE_SENT_SHOW_NAME_DURATION) - && obj.sender().uuid() != obj.manager().uuid(), - ); - } else { - obj.set_show_header(true); - } - } - - fn update_show_timestamp(&self, obj: &Self::Type, next: Option<&TimelineItem>) { - if let Some(msg) = next.and_then(|p| p.downcast_ref::()) { - // Show timestamp if: - // - The sender of the previous message is different to the sender of this message, or - // - Some time has passed between the two messages. - obj.set_show_timestamp( - obj.sender().uuid() != msg.sender().uuid() - || msg.timestamp() - >= obj.timestamp() + MESSAGE_SENT_SHOW_TIMESTAMP_DURATION, - ); - } else { - obj.set_show_timestamp(true); - } - } - } -} diff --git a/src/backend/message/reaction_message.rs b/src/backend/message/reaction_message.rs deleted file mode 100644 index 3350b3a..0000000 --- a/src/backend/message/reaction_message.rs +++ /dev/null @@ -1,131 +0,0 @@ -use crate::backend::{Channel, Contact, timeline::TimelineItem}; -use crate::prelude::*; - -use gdk_pixbuf::Pixbuf; -use gettextrs::gettext; - -use libsignal_service::{content::Reaction, prelude::Uuid}; - -use super::{Manager, Message, MessageExt}; - -gtk::glib::wrapper! { - /// A ReactionMessage is propagated to add or remove a reaction from a [Message]. - pub struct ReactionMessage(ObjectSubclass) @extends Message, TimelineItem; -} - -impl ReactionMessage { - pub fn from_reaction( - sender: &Contact, - channel: &Channel, - timestamp: u64, - manager: &Manager, - reaction: Reaction, - ) -> Self { - let s: Self = Object::builder::() - .property("sender", sender) - .property("channel", channel) - .property("timestamp", timestamp) - .property("manager", manager) - .property("read", channel.property::("is-active")) - .build(); - s.imp().reaction.swap(&RefCell::new(Some(reaction))); - s - } - - pub fn reaction(&self) -> Reaction { - self.imp() - .reaction - .borrow() - .clone() - .expect("`ReactionMessage` to have a `Reaction`") - } - - pub fn emoji(&self) -> String { - self.reaction().emoji.unwrap_or_default() - } - - pub fn target_timestamp(&self) -> u64 { - self.reaction().target_sent_timestamp() - } - - pub fn target_uuid(&self) -> Uuid { - let reaction = self.reaction(); - reaction - .target_author_aci_binary - .as_ref() - .and_then(|t| t.clone().try_into().ok()) - .map(Uuid::from_bytes) - .or(Uuid::parse_str(reaction.target_author_aci()).ok()) - .expect("Reaction to have a valid target UUID") - } - - /// Send a notification for the reaction. - /// - /// This is skipped if: - /// - The reaction is sent from self. - /// - The reaction targets a message from someone else. - pub fn send_notification(&self) { - let sender = self.sender(); - let channel = self.channel(); - if sender.is_self() || self.target_uuid() != self.manager().uuid() { - // Skip notifications for reaction messages sent from self. - return; - } - - let (notification_title, notification_body) = if channel.group_context().is_some() { - // Translators: When receiving a reaction message in a group, this will be the text to format the notification. Do not translate the text in {}. - ( - channel.title(), - gettext("{sender} reacted {emoji} to a message.") - .replace("{sender}", &sender.title()) - .replace("{emoji}", &self.emoji()), - ) - } else { - // Translators: When receiving a reaction message in a 1-to-1 channel (the body, the sender will be in the notification title and is not included in the translation), this will be the text to format the notification. Do not translate the text in {}. - ( - sender.title(), - gettext("Reacted {emoji} to a message.") - .replace("{sender}", &sender.title()) - .replace("{emoji}", &self.emoji()), - ) - }; - - let icon = Pixbuf::from_resource("/icon.svg").expect("Flare to have an application icon"); - - let notification = gio::Notification::new(¬ification_title); - notification.set_body(Some(¬ification_body)); - notification.set_icon(&icon); - notification.set_category(Some("im.received")); - notification.set_default_action_and_target_value( - "app.notification-clicked", - Some(&channel.notification_variant().to_variant()), - ); - - let manager = self.manager(); - let uid = self.uid(); - manager.send_notification(uid, ¬ification); - } -} - -mod imp { - use crate::prelude::*; - use libsignal_service::content::Reaction; - - use crate::backend::{Message, message::MessageImpl, timeline::TimelineItemImpl}; - - #[derive(Default)] - pub struct ReactionMessage { - pub(super) reaction: RefCell>, - } - - #[glib::object_subclass] - impl ObjectSubclass for ReactionMessage { - const NAME: &'static str = "FlReactionMessage"; - type Type = super::ReactionMessage; - type ParentType = Message; - } - - impl TimelineItemImpl for ReactionMessage {} - impl MessageImpl for ReactionMessage {} - impl ObjectImpl for ReactionMessage {} -} diff --git a/src/backend/message/text_message.rs b/src/backend/message/text_message.rs deleted file mode 100644 index a9adb04..0000000 --- a/src/backend/message/text_message.rs +++ /dev/null @@ -1,449 +0,0 @@ -use crate::prelude::*; - -use libsignal_service::content::Reaction; -use libsignal_service::proto::DataMessage; -use libsignal_service::proto::body_range::AssociatedValue; -use libsignal_service::proto::data_message::Delete; -use pango::{AttrColor, AttrList}; - -use crate::backend::timeline::{TimelineItem, TimelineItemExt}; -use crate::backend::{Attachment, Channel, Contact}; - -use super::{DisplayMessage, Manager, Message, MessageExt, ReactionMessage}; - -gtk::glib::wrapper! { - /// A message which contains either text, attachments or both. - pub struct TextMessage(ObjectSubclass) @extends Message, DisplayMessage, TimelineItem; -} - -const MENTION_CHAR: char = '@'; -const MENTION_COLOR: (u16, u16, u16) = (0, 0, u16::MAX); - -impl TextMessage { - pub fn from_text_channel_sender>( - text: S, - channel: Channel, - sender: Contact, - manager: &Manager, - ) -> Self { - Self::from_text_channel_sender_timestamp( - text, - channel, - sender, - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .expect("Time went backwards") - .as_millis() as u64, - manager, - ) - } - - #[cfg(feature = "screenshot")] - pub fn pub_from_text_channel_sender_timestamp>( - text: S, - channel: Channel, - sender: Contact, - timestamp: u64, - manager: &Manager, - ) -> Self { - Self::from_text_channel_sender_timestamp(text, channel, sender, timestamp, manager) - } - - fn from_text_channel_sender_timestamp>( - text: S, - channel: Channel, - sender: Contact, - timestamp: u64, - manager: &Manager, - ) -> Self { - log::trace!("Trying to build a message from text"); - let s: Self = Object::builder::() - .property("manager", manager) - .property("channel", &channel) - .property("sender", &sender) - .property("timestamp", timestamp) - .build(); - - let text_owned = text.as_ref().to_owned(); - let body = if text_owned.is_empty() { - None - } else { - Some(text_owned) - }; - - let message = DataMessage { - body, - timestamp: Some(timestamp), - ..Default::default() - }; - - s.set_internal_data(Some(message)); - gspawn!(clone!( - #[strong] - s, - async move { s.prepare_format_body().await } - )); - s - } - - /// Asynchronously complete constructing the message. - pub(super) async fn init_data(&self, message: &DataMessage, manager: &Manager) { - let obj = self.imp(); - self.set_internal_data(Some(message.clone())); - self.prepare_format_body().await; - - // Load attachments in parallel. - let attachment_futures = message - .attachments - .iter() - .chain(message.sticker.as_ref().and_then(|s| s.data.as_ref())) - .map(|pointer| async move { Attachment::from_pointer(pointer, manager).await }); - let attachments = futures::future::join_all(attachment_futures).await; - obj.attachments.swap(&RefCell::new(attachments)); - } - - /// Apply a reaction message. - pub fn react(&self, reaction: &ReactionMessage) { - self.react_sender_reaction(reaction.sender().uuid(), reaction.reaction()); - } - - /// Apply a reaction from a sender. - fn react_sender_reaction(&self, sender: Uuid, reaction: Reaction) { - if reaction.remove.unwrap_or_default() { - self.imp().reactions.borrow_mut().remove(&sender); - } else { - let _ = self.imp().reactions.borrow_mut().insert(sender, reaction); - } - self.notify("reactions"); - } - - pub fn quote_timestamp(&self) -> Option { - self.internal_data() - .and_then(|d| d.quote) - .and_then(|q| q.id) - } - - /// Sends a deletion request for a message and marks the message as deleted. - pub async fn delete(&self) -> Result<(), crate::ApplicationError> { - crate::trace!("Delete a message with timestamp: {}", self.timestamp()); - let delete = Delete { - target_sent_timestamp: Some(self.timestamp()), - }; - - let timestamp = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .expect("Time went backwards") - .as_millis() as u64; - - let message = DataMessage { - timestamp: Some(timestamp), - delete: Some(delete), - ..Default::default() - }; - self.channel() - .send_internal_message(message, timestamp) - .await?; - - self.mark_as_deleted(); - self.channel().notify("last-message"); - - Ok(()) - } - - pub fn mark_as_deleted(&self) { - // TODO: Does not delete message from memory. - self.set_property("is-deleted", true); - } - - /// Send a reaction for a message and apply it. - pub async fn send_reaction>( - &self, - reaction: S, - ) -> Result<(), crate::ApplicationError> { - // Instead of adding a reaction, remove it if there is already a reaction. - let has_self_reaction = self - .imp() - .reactions - .borrow() - .contains_key(&self.manager().uuid()); - - let reaction_struct = Reaction { - emoji: Some(reaction.as_ref().to_owned()), - remove: Some(has_self_reaction), - target_author_aci: self - .sender() - .address() - .map(|a| a.raw_uuid()) - .map(|u| u.to_string()), - target_author_aci_binary: self - .sender() - .address() - .map(|a| a.raw_uuid()) - .map(|u| u.into_bytes().into()), - target_sent_timestamp: Some(self.timestamp()), - }; - self.react_sender_reaction(self.manager().uuid(), reaction_struct.clone()); - - let timestamp = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .expect("Time went backwards") - .as_millis() as u64; - - let message = DataMessage { - timestamp: Some(timestamp), - reaction: Some(reaction_struct), - ..Default::default() - }; - self.channel() - .send_internal_message(message, timestamp) - .await - } - - pub fn attachments(&self) -> Vec { - self.imp().attachments.borrow().clone() - } - - /// Adds the attachment to the message, but does not yet upload it. - pub fn add_attachment(&self, attachment: Attachment) { - log::trace!("Adding a attachment to a message"); - self.imp().attachments.borrow_mut().push(attachment); - } - - // Uploads all attachments. - pub async fn upload_attachments(&self) -> Result<(), crate::ApplicationError> { - let manager = self.manager(); - let attachments = self.imp().attachments.borrow().clone(); - let attachments_futures = attachments.iter().map(|a| a.as_upload_attachment()); - let attachments = futures::future::join_all(attachments_futures).await; - if attachments.is_empty() { - return Ok(()); - } - - log::trace!("Uploading {} attachment(s)", attachments.len()); - let upload_attachments_result = manager - .upload_attachments(attachments) - .await? - .into_iter() - .collect::, _>>()?; - - if let Some(data) = self.internal_data_mut().as_mut() { - data.attachments = upload_attachments_result; - } - - Ok(()) - } - - pub fn is_empty(&self) -> bool { - self.body().is_none() && self.attachments().is_empty() - } - - async fn prepare_format_body(&self) { - let (body, attrs) = self.format_body().await; - *self.imp().formatted_body.borrow_mut() = body; - *self.imp().message_attributes.borrow_mut() = attrs; - self.notify_message_attributes(); - self.notify_body(); - } - - /// Formats the message body based on its ranges, e.g. to insert mention names. - /// - /// Returns the resulting strings and an [AttrList] that can be used in labels to highlight areas. - /// Be carefull when editing this function and note that Signal uses UTF-16 byte offsets, while Rust uses UTF-8 byte offsets. - async fn format_body(&self) -> (Option, AttrList) { - let Some(body) = self.internal_data().and_then(|m| m.body) else { - return (None, AttrList::new()); - }; - let mut ranges = self - .internal_data() - .map(|m| m.body_ranges) - .unwrap_or_default(); - - if ranges.is_empty() { - return (Some(body), AttrList::new()); - } - - let channel = self.channel(); - - // Sort by growing start index - ranges.sort_unstable_by_key(|r| r.start()); - - let attrs = AttrList::new(); - - // Signal (Java) uses UTF-16 body and therefore also UTF-16 offsets, while Flare (Rust) uses UTF-8. Need to convert. - let body_utf16: Vec = body.encode_utf16().collect(); - - let mut result_utf8 = String::new(); - let mut index_utf16 = 0; - let mut index_utf8 = 0; - for r in ranges { - let start = r.start() as usize; - let end = start + r.length() as usize; - let uuid = match r.associated_value { - Some(AssociatedValue::MentionAci(u)) => u.parse().ok(), - Some(AssociatedValue::MentionAciBinary(u)) => { - u.try_into().ok().map(Uuid::from_bytes) - } - _ => None, - }; - let Some(uuid) = uuid else { - continue; - }; - let name = format!( - "{}{}", - MENTION_CHAR, - channel.participant_by_uuid(uuid).await.title() - ); - let to_add_body = String::from_utf16_lossy(&body_utf16[index_utf16..start]); - result_utf8.push_str(&to_add_body); - result_utf8.push_str(&name); - index_utf16 = end; - - let index_start_highlight = index_utf8 + to_add_body.len(); - index_utf8 += to_add_body.len() + name.len(); - let index_end_highlight = index_utf8; - - let (red, green, blue) = MENTION_COLOR; - let mut highlight = AttrColor::new_foreground(red, green, blue); - highlight.set_start_index(index_start_highlight as u32); - highlight.set_end_index(index_end_highlight as u32); - attrs.insert(highlight); - } - - if index_utf16 < body_utf16.len() { - result_utf8.push_str(&String::from_utf16_lossy(&body_utf16[index_utf16..])) - } - - (Some(result_utf8), attrs) - } -} - -mod imp { - use crate::prelude::*; - use libsignal_service::content::Reaction; - use pango::AttrList; - use presage::proto::data_message::Quote; - use std::collections::HashMap; - - use crate::backend::message::MessageExt; - use crate::backend::timeline::TimelineItemExt; - use crate::backend::{ - Attachment, - message::{DisplayMessage, MessageImpl, display_message::DisplayMessageImpl}, - }; - use crate::backend::{ - Message, - timeline::{TimelineItem, TimelineItemImpl}, - }; - - #[derive(Default, glib::Properties)] - #[properties(wrapper_type = super::TextMessage)] - pub struct TextMessage { - #[property(get, set = Self::set_quote)] - pub(super) quote: RefCell>, - - #[property(get = Self::reactions, type = String, default = Some(""))] - pub(super) reactions: RefCell>, - - pub(super) attachments: RefCell>, - - #[property(name = "body", get)] - pub(super) formatted_body: RefCell>, - #[property(get)] - pub(super) message_attributes: RefCell, - #[property(get, set)] - pub(super) is_deleted: RefCell, - } - - impl TextMessage { - fn reactions(&self) -> String { - let mut reaction_emojis: Vec = Vec::new(); - let mut reaction_counts: Vec = Vec::new(); - - // This counts the number of each emoji - self.reactions.borrow().values().for_each(|r| { - match reaction_emojis.iter().position(|e| e == r.emoji()) { - Some(index) => reaction_counts[index] += 1, - None => { - reaction_emojis.push(r.emoji().to_string()); - reaction_counts.push(1); - } - } - }); - - // And this creates a string with the emojis and their counts - reaction_emojis - .iter() - .zip(reaction_counts.iter()) - .map(|(emoji, &count)| { - if count > 1 { - format!("{emoji}{count}") - } else { - emoji.clone() - } - }) - .collect::>() - .join(" ") - } - - fn set_quote(&self, msg: &super::TextMessage) { - if let Some(data) = self.obj().internal_data_mut().as_mut() { - let sender = msg.sender().address(); - data.quote = Some(Quote { - id: Some(msg.timestamp()), - author_aci: sender.as_ref().map(|a| a.raw_uuid()).map(|u| u.to_string()), - text: msg.body(), - ..Default::default() - }); - } - self.quote.replace(Some(msg.clone())); - } - } - - #[glib::object_subclass] - impl ObjectSubclass for TextMessage { - const NAME: &'static str = "FlTextMessage"; - type Type = super::TextMessage; - type ParentType = DisplayMessage; - } - - impl TimelineItemImpl for TextMessage { - fn update_show_header(&self, obj: &Self::Type, previous: Option<&TimelineItem>) { - let upcast = obj.upcast_ref::(); - upcast.imp().update_show_header(upcast, previous); - } - fn update_show_timestamp(&self, obj: &Self::Type, previous: Option<&TimelineItem>) { - let upcast = obj.upcast_ref::(); - upcast.imp().update_show_timestamp(upcast, previous); - } - } - - impl DisplayMessageImpl for TextMessage { - fn textual_description(&self, obj: &super::TextMessage) -> Option { - if let Some(body) = obj.body() { - body.lines().next().map(|s| s.to_owned()) - } else { - let attachments = self.attachments.borrow(); - - let formatter = if attachments.iter().all(|a| a.is_image()) { - gettextrs::ngettext("Sent an image", "Sent {} images", attachments.len() as u32) - } else if attachments.iter().all(|a| a.is_video()) { - gettextrs::ngettext("Sent an video", "Sent {} videos", attachments.len() as u32) - } else if attachments.iter().all(|a| a.is_audio()) { - gettextrs::ngettext( - "Sent a voice message", - "Sent {} voice messages", - attachments.len() as u32, - ) - } else { - gettextrs::ngettext("Sent a file", "Sent {} files", attachments.len() as u32) - }; - - Some(formatter.replace("{}", &attachments.len().to_string())) - } - } - } - impl MessageImpl for TextMessage {} - - #[glib::derived_properties] - impl ObjectImpl for TextMessage {} -} diff --git a/src/backend/mod.rs b/src/backend/mod.rs index b709e0b..1d79cf3 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -3,7 +3,6 @@ mod channel; mod contact; mod device_info; mod manager; -mod manager_thread; pub mod message; mod server; pub mod timeline; @@ -12,8 +11,7 @@ pub use attachment::{Attachment, AttachmentType}; pub use channel::Channel; pub use contact::Contact; pub use device_info::DeviceInfo; -pub use manager::Manager; -pub use manager_thread::{SetupDecision, SetupResult}; +pub use manager::{Manager, SetupResult}; pub use message::Message; pub use server::Server; diff --git a/src/backend/server.rs b/src/backend/server.rs index ea94ddb..049fa9c 100644 --- a/src/backend/server.rs +++ b/src/backend/server.rs @@ -1,6 +1,6 @@ use crate::prelude::*; -use libsignal_service::configuration::SignalServers; +use ftypes::SignalServers; gtk::glib::wrapper! { /// Which server to use when linking. @@ -24,7 +24,7 @@ impl Server { mod imp { use crate::prelude::*; - use libsignal_service::configuration::SignalServers; + use ftypes::SignalServers; #[derive(glib::Properties)] #[properties(wrapper_type=super::Server)] diff --git a/src/backend/timeline/mod.rs b/src/backend/timeline.rs similarity index 90% rename from src/backend/timeline/mod.rs rename to src/backend/timeline.rs index 1ce6a24..b153837 100644 --- a/src/backend/timeline/mod.rs +++ b/src/backend/timeline.rs @@ -1,13 +1,9 @@ -pub mod timeline_item; +use chrono::{DateTime, Utc}; -use crate::prelude::*; - -pub use timeline_item::*; - -const TRIM_SIZE: usize = 1; +use crate::{backend::Message, prelude::*}; glib::wrapper! { - /// The timeline stores ordered [TimelineItem]s (i.e. [Message](crate::backend::Message)s) + /// The timeline stores ordered [Message](crate::backend::Message)s. pub struct Timeline(ObjectSubclass) @implements gio::ListModel, gtk::SectionModel; } @@ -24,15 +20,13 @@ impl Timeline { } /// Remove old messages to free memory. - pub fn trim_old(&self) { + pub fn trim_older_than(&self, date: DateTime) { let mut current_items = self.imp().list.borrow_mut(); - if current_items.len() > TRIM_SIZE { - let to_trim = current_items.len() - TRIM_SIZE; - let new = current_items.split_off(to_trim); - drop(current_items); - self.imp().list.replace(new); - self.items_changed(0, to_trim as u32, 0); - } + let prev_size = current_items.len(); + current_items.retain_mut(|m| m.id().timestamp >= date); + let after_size = current_items.len(); + drop(current_items); + self.items_changed(0, prev_size as u32, after_size as u32); } /// Completely clear the list. @@ -44,7 +38,8 @@ impl Timeline { self.items_changed(0, len as u32, 0); } - pub fn get_by_timestamp(&self, timestamp: u64) -> Option { + pub fn get_by_timestamp(&self, timestamp: DateTime) -> Option { + let timestamp = timestamp.timestamp_millis() as u64; let current_items = self.imp().list.borrow(); let index = current_items.binary_search_by_key(×tamp, |i| i.timestamp()); match index { @@ -53,12 +48,12 @@ impl Timeline { } } - pub fn iter_forwards(&self) -> impl Iterator + 'static { + pub fn iter_forwards(&self) -> impl Iterator + 'static { let current_items = self.imp().list.borrow(); current_items.clone().into_iter() } - pub fn iter_backwards(&self) -> impl Iterator + 'static { + pub fn iter_backwards(&self) -> impl Iterator + 'static { let current_items = self.imp().list.borrow(); current_items.clone().into_iter().rev() } @@ -130,7 +125,7 @@ impl Timeline { /// /// If there already exists a item with the same timestamp like in the list, the old item will /// be overwritten with the new one. - pub fn prepend(&self, items: Vec) { + pub fn prepend(&self, items: Vec) { if items.is_empty() { return; } @@ -193,7 +188,7 @@ impl Timeline { } /// Insert a timeline item that is probably at the back of the list. - pub fn append(&self, item: TimelineItem) { + pub fn append(&self, item: Message) { let mut current_items = self.imp().list.borrow_mut(); let number_of_items = current_items.len() as u32; @@ -233,11 +228,11 @@ mod imp { use crate::prelude::*; use std::collections::VecDeque; - use super::{TimelineItem, TimelineItemExt}; + use super::Message; #[derive(Debug, Default)] pub struct Timeline { - pub list: RefCell>, + pub list: RefCell>, } #[glib::object_subclass] @@ -251,7 +246,7 @@ mod imp { impl ListModelImpl for Timeline { fn item_type(&self) -> glib::Type { - TimelineItem::static_type() + Message::static_type() } fn n_items(&self) -> u32 { diff --git a/src/backend/timeline/timeline_item.rs b/src/backend/timeline/timeline_item.rs deleted file mode 100644 index 5661bb8..0000000 --- a/src/backend/timeline/timeline_item.rs +++ /dev/null @@ -1,158 +0,0 @@ -use crate::prelude::*; -use glib::subclass::types::IsSubclassable; - -glib::wrapper! { - /// A [TimelineItem] is anything that can be included in a [Timeline](super::Timeline). - pub struct TimelineItem(ObjectSubclass); -} - -pub trait TimelineItemExt: 'static + std::marker::Sized + glib::prelude::ObjectExt { - /// Set if this item should show the header based on the previous item. - fn update_show_header(&self, previous: Option<&TimelineItem>); - /// Set if this item should show the timestamp based on the next item. - fn update_show_timestamp(&self, next: Option<&TimelineItem>); - - /// Days since 01.01.1970 - fn day_timestamp(&self) -> u64 { - self.timestamp() / (1000 * 60 * 60 * 24) - } - - /// The timestamp in ms from 01.01.1970 - fn timestamp(&self) -> u64 { - self.property("timestamp") - } - - fn show_header(&self) -> bool { - self.property("show-header") - } - - fn set_show_header(&self, value: bool) { - self.set_property("show-header", value) - } - - fn show_timestamp(&self) -> bool { - self.property("show-timestamp") - } - - fn set_show_timestamp(&self, value: bool) { - self.set_property("show-timestamp", value) - } -} - -impl> TimelineItemExt for O { - fn update_show_header(&self, previous: Option<&TimelineItem>) { - imp::timeline_item_update_show_header(self.upcast_ref(), previous) - } - - fn update_show_timestamp(&self, previous: Option<&TimelineItem>) { - imp::timeline_item_update_show_timestamp(self.upcast_ref(), previous) - } -} - -pub trait TimelineItemImpl: ObjectImpl { - fn update_show_header(&self, _obj: &Self::Type, _previous: Option<&TimelineItem>) {} - fn update_show_timestamp(&self, _obj: &Self::Type, _previous: Option<&TimelineItem>) {} -} - -unsafe impl IsSubclassable for TimelineItem -where - T: TimelineItemImpl, - T::Type: IsA, -{ - fn class_init(class: &mut glib::Class) { - Self::parent_class_init::(class.upcast_ref_mut()); - - let klass = class.as_mut(); - - klass.update_show_header = update_show_header_trampoline::; - klass.update_show_timestamp = update_show_timestamp_trampoline::; - } -} - -fn update_show_header_trampoline(this: &TimelineItem, previous: Option<&TimelineItem>) -where - T: ObjectSubclass + TimelineItemImpl, - T::Type: IsA, -{ - let this = this.downcast_ref::().unwrap(); - this.imp().update_show_header(this, previous) -} - -fn update_show_timestamp_trampoline(this: &TimelineItem, next: Option<&TimelineItem>) -where - T: ObjectSubclass + TimelineItemImpl, - T::Type: IsA, -{ - let this = this.downcast_ref::().unwrap(); - this.imp().update_show_timestamp(this, next) -} - -mod imp { - use std::marker::PhantomData; - - use super::*; - - #[repr(C)] - pub struct TimelineItemClass { - pub parent_class: glib::object::ObjectClass, - pub update_show_header: fn(&super::TimelineItem, Option<&super::TimelineItem>), - pub update_show_timestamp: fn(&super::TimelineItem, Option<&super::TimelineItem>), - } - - unsafe impl ClassStruct for TimelineItemClass { - type Type = TimelineItem; - } - - pub(super) fn timeline_item_update_show_header( - this: &super::TimelineItem, - previous: Option<&super::TimelineItem>, - ) { - let klass = this.class(); - (klass.as_ref().update_show_header)(this, previous) - } - - pub(super) fn timeline_item_update_show_timestamp( - this: &super::TimelineItem, - next: Option<&super::TimelineItem>, - ) { - let klass = this.class(); - (klass.as_ref().update_show_timestamp)(this, next) - } - - #[derive(Debug, Default, glib::Properties)] - #[properties(wrapper_type = super::TimelineItem)] - pub struct TimelineItem { - #[property(get, set, construct_only)] - timestamp: Cell, - - #[property(get, set)] - show_header: Cell, - #[property(get, set)] - show_timestamp: Cell, - - #[property(get = Self::datetime)] - datetime: PhantomData>, - } - - impl TimelineItem { - // Time as [glib::DateTime]. Should not return `None`, but just in case. - fn datetime(&self) -> Option { - glib::DateTime::from_unix_utc( - (self.timestamp.get() / 1000).try_into().unwrap_or_default(), - ) - .ok() - .and_then(|d| d.to_local().ok()) - } - } - - #[glib::object_subclass] - impl ObjectSubclass for TimelineItem { - const NAME: &'static str = "FlTimelineItem"; - type Type = super::TimelineItem; - type ParentType = glib::Object; - type Class = TimelineItemClass; - } - - #[glib::derived_properties] - impl ObjectImpl for TimelineItem {} -} diff --git a/src/error.rs b/src/error.rs index e4c566a..1875158 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,16 +1,7 @@ -use std::{error::Error, fmt::Display}; +use std::fmt::Display; +use ftypes::error::ErrorActionRequired; use gtk::glib; -use libsignal_service as lss; -use lss::push_service::ServiceError; -use presage as p; - -const FAILED_TO_LOOK_UP_ADDRESS: &str = "failed to lookup address information"; -const NETWORK_UNREACHABLE: &str = "Network is unreachable"; -const TIMED_OUT: &str = "timed out"; -const REQWEST_ERROR: &str = "reqwest error"; - -type PresageError = presage::Error; #[derive(Debug)] pub enum ConfigurationError { @@ -41,55 +32,15 @@ impl std::error::Error for ConfigurationError {} #[derive(Debug)] pub enum ApplicationError { IOError(std::io::Error), - NoInternet, Glib(glib::Error), Libsecret(oo7::Error), - Db(presage_store_sqlite::SqliteStoreError), - UnauthorizedSignal, - // MessageSenderError is pretty big, put into `Box` to move it to the heap. - SendFailed(Box), - ReceiveFailed(libsignal_service::push_service::ServiceError), - FailedAttachmentUpload(libsignal_service::sender::AttachmentUploadError), - // PresageError is pretty big, put into `Box` to move it to the heap. - Presage(Box), ConfigurationError(ConfigurationError), - ManagerThreadPanic, + Backend(ftypes::Error), } -impl From for ApplicationError { - fn from(e: PresageError) -> Self { - match e { - p::Error::ServiceError(ServiceError::Unauthorized) => { - ApplicationError::UnauthorizedSignal - } - p::Error::Store(e) => ApplicationError::Db(e), - p::Error::ServiceError(ServiceError::WsError(e)) - if e.to_string().contains(FAILED_TO_LOOK_UP_ADDRESS) - || e.to_string().contains(NETWORK_UNREACHABLE) - || e.to_string().contains(TIMED_OUT) - || e.to_string().contains(REQWEST_ERROR) - || e.source().is_some_and(|s| { - s.to_string().contains(FAILED_TO_LOOK_UP_ADDRESS) - || s.to_string().contains(NETWORK_UNREACHABLE) - || s.to_string().contains(TIMED_OUT) - || s.to_string().contains(REQWEST_ERROR) - }) => - { - ApplicationError::NoInternet - } - p::Error::MessageSenderError(e) => match *e { - lss::sender::MessageSenderError::ServiceError(ServiceError::SendError { - reason: e, - }) if e.contains(FAILED_TO_LOOK_UP_ADDRESS) => ApplicationError::NoInternet, - _ => ApplicationError::SendFailed(e), - }, - p::Error::ServiceError(ServiceError::SendError { reason: e }) - if e.contains(FAILED_TO_LOOK_UP_ADDRESS) => - { - ApplicationError::NoInternet - } - _ => ApplicationError::Presage(Box::new(e)), - } +impl From for ApplicationError { + fn from(value: ftypes::Error) -> Self { + Self::Backend(value) } } @@ -111,72 +62,25 @@ impl From for ApplicationError { } } -impl From for ApplicationError { - fn from(e: presage_store_sqlite::SqliteStoreError) -> Self { - ApplicationError::Db(e) - } -} - -impl From for ApplicationError { - fn from(value: libsignal_service::sender::AttachmentUploadError) -> Self { - ApplicationError::FailedAttachmentUpload(value) - } -} - use gettextrs::gettext; impl std::fmt::Display for ApplicationError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ApplicationError::IOError(_) => writeln!(f, "{}", gettext("I/O Error.")), - ApplicationError::NoInternet => writeln!( - f, - "{}", - gettext("There does not seem to be a connection to the internet available.") - ), ApplicationError::Glib(_) => { writeln!(f, "{}", gettext("Something glib-related failed.")) } ApplicationError::Libsecret(_) => { writeln!(f, "{}", gettext("The communication with libsecret failed.")) } - ApplicationError::Db(_) => writeln!( - f, - "{}", - gettext( - "The backend database failed. Please restart the application or delete the database and relink the application." - ) - ), - ApplicationError::UnauthorizedSignal => writeln!( - f, - "{}", - gettext( - "You do not seem to be authorized with Signal. Please delete the database and relink the application." - ) - ), - ApplicationError::SendFailed(_) => { - writeln!(f, "{}", gettext("Sending a message failed.")) - } - ApplicationError::ReceiveFailed(_) => { - writeln!(f, "{}", gettext("Receiving a message failed.")) - } - ApplicationError::FailedAttachmentUpload(_) => { - writeln!(f, "{}", gettext("Failed to upload an attachment")) - } - ApplicationError::Presage(_) => writeln!( - f, - "{}", - gettext( - "Something unexpected happened with the signal backend. Please retry later." - ) - ), ApplicationError::ConfigurationError(_) => writeln!( f, "{}", gettext("The application seems to be misconfigured.") ), - ApplicationError::ManagerThreadPanic => { - writeln!(f, "{}", gettext("A part of the application crashed.")) + ApplicationError::Backend(_) => { + writeln!(f, "{}", gettext("Backend error")) } } } @@ -186,19 +90,8 @@ impl ApplicationError { pub fn more_information(&self) -> String { match self { ApplicationError::IOError(e) => format!("{e:#?}"), - ApplicationError::NoInternet => gettext("Please check your internet connection."), ApplicationError::Glib(e) => format!("{e:#?}"), ApplicationError::Libsecret(e) => format!("{e:#?}"), - ApplicationError::Db(e) => format!("{e:#?}"), - ApplicationError::UnauthorizedSignal => { - gettext("Please delete the database and relink the device.") - } - ApplicationError::SendFailed(e) => format!("{e:#?}"), - ApplicationError::ReceiveFailed(e) => format!("{e:#?}"), - ApplicationError::FailedAttachmentUpload(e) => { - format!("{e:#?}") - } - ApplicationError::Presage(e) => format!("{e:#?}"), ApplicationError::ConfigurationError(e) => match e { ConfigurationError::DbPathNoFolder(p) => { let s = gettext("The database path at {} is no folder."); @@ -212,8 +105,8 @@ impl ApplicationError { .replace("{error}", &e.to_string()) } }, - ApplicationError::ManagerThreadPanic => { - gettext("Please restart the application with logging and report this issue.") + ApplicationError::Backend(e) => { + format!("{e:#?}") } } } @@ -221,17 +114,10 @@ impl ApplicationError { pub fn should_report(&self) -> bool { match self { ApplicationError::IOError(_) => false, - ApplicationError::NoInternet => false, ApplicationError::Glib(_) => true, ApplicationError::Libsecret(_) => false, - ApplicationError::Db(_) => true, - ApplicationError::UnauthorizedSignal => false, - ApplicationError::SendFailed(_) => true, - ApplicationError::ReceiveFailed(_) => true, - ApplicationError::FailedAttachmentUpload(_) => true, - ApplicationError::Presage(_) => true, ApplicationError::ConfigurationError(_) => true, - ApplicationError::ManagerThreadPanic => true, + ApplicationError::Backend(e) => e.action_required() == ErrorActionRequired::Report, } } } diff --git a/src/gui/call_message_item.rs b/src/gui/call_message_item.rs deleted file mode 100644 index 0b01260..0000000 --- a/src/gui/call_message_item.rs +++ /dev/null @@ -1,80 +0,0 @@ -use crate::prelude::*; - -use glib::subclass::types::ObjectSubclassIsExt; - -use crate::backend::Contact; -use crate::backend::message::{CallMessage, CallMessageType}; - -gtk::glib::wrapper! { - /// A widget to display call events. - pub struct CallMessageItem(ObjectSubclass) - @extends gtk::Box,gtk::Widget, - @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget; -} - -impl CallMessageItem { - pub fn new(message: &CallMessage) -> Self { - log::trace!("Initializing `CallMessageItem`"); - let obj = Object::builder::() - .property("message", message) - .build(); - - let sender: Contact = message.property("sender"); - - let icon_name = match (message.call_type(), sender.is_self()) { - (CallMessageType::Offer, false) => "call-incoming-symbolic", - (CallMessageType::Offer, true) => "call-outcoming-symbolic", - (CallMessageType::Answer, _) => "call-start-symbolic", - (CallMessageType::Hangup, _) => "call-stop-symbolic", - (CallMessageType::Busy, _) => "call-missed-symbolic", - }; - - obj.imp().icon.set_icon_name(Some(icon_name)); - obj - } -} - -pub mod imp { - use crate::prelude::*; - - use glib::subclass::InitializingObject; - use gtk::CompositeTemplate; - - use crate::backend::{Manager, message::CallMessage}; - - #[derive(CompositeTemplate, Default, glib::Properties)] - #[properties(wrapper_type = super::CallMessageItem)] - #[template(resource = "/ui/call_message_item.ui")] - pub struct CallMessageItem { - #[template_child] - pub icon: TemplateChild, - - #[property(get, set)] - message: RefCell>, - - #[property(get, set, construct_only, type = Manager)] - manager: RefCell>, - } - - #[glib::object_subclass] - impl ObjectSubclass for CallMessageItem { - const NAME: &'static str = "FlCallMessageItem"; - type Type = super::CallMessageItem; - type ParentType = gtk::Box; - - fn class_init(klass: &mut Self::Class) { - Self::bind_template(klass); - Utility::bind_template_callbacks(klass); - } - - fn instance_init(obj: &InitializingObject) { - obj.init_template(); - } - } - - #[glib::derived_properties] - impl ObjectImpl for CallMessageItem {} - - impl WidgetImpl for CallMessageItem {} - impl BoxImpl for CallMessageItem {} -} diff --git a/src/gui/channel_item.rs b/src/gui/channel_item.rs index 4b0ba22..12aadf9 100644 --- a/src/gui/channel_item.rs +++ b/src/gui/channel_item.rs @@ -27,8 +27,7 @@ pub mod imp { use glib::subclass::InitializingObject; use gtk::{CompositeTemplate, Label}; - use crate::backend::message::{DisplayMessage, DisplayMessageExt, MessageExt}; - use crate::backend::{Channel, Manager}; + use crate::backend::{Channel, Manager, Message}; #[derive(CompositeTemplate, Default, glib::Properties)] #[properties(wrapper_type = super::ChannelItem)] @@ -49,7 +48,7 @@ pub mod imp { #[template_callback] fn format_last_message( &self, - message: Option, + message: Option, is_typing: bool, draft: String, ) -> String { @@ -67,7 +66,7 @@ pub mod imp { self.label_last_message.add_css_class("dim-label"); self.label_last_message.remove_css_class("accent"); - let is_group = self.obj().channel().group().is_some(); + let is_group = self.obj().channel().is_group(); if is_group { format!( "{}: {}", diff --git a/src/gui/channel_item_compact.rs b/src/gui/channel_item_compact.rs index f422797..5474681 100644 --- a/src/gui/channel_item_compact.rs +++ b/src/gui/channel_item_compact.rs @@ -36,29 +36,10 @@ pub mod imp { #[template_child] label_name: TemplateChild, - #[property(get, set = Self::set_channel)] + #[property(get, set)] channel: RefCell>, } - impl ChannelItemCompact { - fn set_channel(&self, chan: Option) { - let (part1, part2) = chan.as_ref().map(|c| c.name_parts()).unwrap_or_default(); - let (part1, part2) = ( - part1.map(|p| glib::markup_escape_text(&p)), - glib::markup_escape_text(&part2), - ); - - let format = if let Some(part1) = part1 { - format!("{part1} {part2}") - } else { - format!("{part2}") - }; - self.label_name.set_markup(&format); - - self.channel.replace(chan); - } - } - #[glib::object_subclass] impl ObjectSubclass for ChannelItemCompact { const NAME: &'static str = "FlChannelItemCompact"; diff --git a/src/gui/channel_list.rs b/src/gui/channel_list.rs index bfb2dca..a5b5686 100644 --- a/src/gui/channel_list.rs +++ b/src/gui/channel_list.rs @@ -119,7 +119,7 @@ pub mod imp { use std::marker::PhantomData; use crate::{ - backend::{Channel, timeline::timeline_item::TimelineItemExt}, + backend::Channel, gui::{channel_item::ChannelItem, components::Selection}, }; diff --git a/src/gui/channel_messages.rs b/src/gui/channel_messages.rs index 0d64d25..a8a9709 100644 --- a/src/gui/channel_messages.rs +++ b/src/gui/channel_messages.rs @@ -118,6 +118,7 @@ pub mod imp { use crate::prelude::*; + use ftypes::{Quote, TextMessage}; use glib::subclass::InitializingObject; use gtk::CompositeTemplate; use gtk::{FileDialog, PositionType, SignalListItemFactory}; @@ -127,8 +128,8 @@ pub mod imp { use crate::gui::components::ItemRow; use crate::gui::components::time_divider::TimeDivider; use crate::{ - backend::{Channel, Manager, message::TextMessage}, - gui::{error_dialog::ErrorDialog, message_item::MessageItem, text_entry::TextEntry}, + backend::{Channel, Manager, message::Message}, + gui::{message_item::MessageItem, text_entry::TextEntry}, }; #[derive(CompositeTemplate, Default, glib::Properties)] @@ -155,7 +156,7 @@ pub mod imp { #[property(get, set = Self::set_active_channel)] active_channel: RefCell>, #[property(get, set, nullable)] - reply_message: RefCell>, + reply_message: RefCell>, #[property(get, set, default = true)] sticky: Cell, @@ -233,9 +234,13 @@ pub mod imp { #[weak] obj, async move { - obj.set_loading(true); - channel.load_last(super::MESSAGES_REQUEST_LOAD).await; - obj.set_loading(false); + if !obj.loading() { + obj.set_loading(true); + channel.load_last(super::MESSAGES_REQUEST_LOAD).await; + obj.set_loading(false); + glib::timeout_future(std::time::Duration::from_millis(100)).await; + obj.load_if_screen_not_filled(); + } } )); } else { @@ -248,7 +253,7 @@ pub mod imp { #[template_callback] fn remove_reply(&self) { log::trace!("Unsetting reply message"); - self.obj().set_reply_message(None::); + self.obj().set_reply_message(None::); } #[template_callback] @@ -342,74 +347,70 @@ pub mod imp { #[template_callback] fn send_message(&self) { log::trace!("Got callback to send message"); + let obj = self.obj(); + let manager = obj.manager(); + // Don't send if not allowed to. This can happen if the entry was activated and not the button. if !self.button_send.is_sensitive() { return; } let text = self.text_entry.text(); self.text_entry.clear(); + + let Some(channel) = obj.active_channel() else { + log::warn!( + "Got callback to send message while no channel was active; this should not happen" + ); + return; + }; + let thread = channel.thread(); + + let quote = obj.reply_message().map(|m| Quote { + quoted_message: m.id(), + body: None, + attachments: vec![], + body_ranges: vec![], + kind: ftypes::QuoteKind::Normal, + }); + obj.set_reply_message(None::); + let attachments = { let mut att = self.attachments.borrow_mut(); let a = att.clone(); att.clear(); a }; - self.obj().notify("has-attachments"); - - if text.is_empty() && attachments.is_empty() { - log::warn!("Got requested to send empty message, skipping"); - } - while let Some(child) = self.box_attachments.first_child() { self.box_attachments.remove(&child); } + self.obj().notify("has-attachments"); - let obj = self.obj(); - if let Some(channel) = self.active_channel.borrow().as_ref() { - log::trace!("Constructing message"); - let manager = self.obj().manager(); - - let msg = TextMessage::from_text_channel_sender( - text, - channel.clone(), - manager.self_contact(), - &manager, - ); - - if let Some(quote) = obj.reply_message() { - log::trace!("Adding quote to message"); - msg.set_quote("e); - obj.set_reply_message(None::); + gspawn!(clone!( + #[strong] + manager, + #[strong] + attachments, + async move { + let attachments = futures::future::join_all( + attachments.iter().map(|a| a.as_upload_attachment()), + ) + .await; + manager + .send_message( + thread, + TextMessage { + body: Some(text), + attachments, + quote, + body_ranges: vec![], + previews: vec![], + contacts: vec![], + is_view_once: false, + }, + ) + .await } - - let obj = self.obj(); - gspawn!(clone!( - #[strong] - msg, - #[strong] - channel, - #[strong] - attachments, - #[strong] - obj, - async move { - log::trace!("Adding attachments to message: {}", attachments.len()); - for att in attachments { - msg.add_attachment(att); - } - log::trace!("Sending message"); - if let Err(e) = channel.send_message(msg.upcast()).await { - let root = obj - .root() - .expect("`ChannelMessages` to have a root") - .dynamic_cast::() - .expect("Root of `ChannelMessages` to be a `Window`."); - let dialog = ErrorDialog::new(&e, &root); - dialog.present(Some(&root)); - } - } - )); - } + )); } #[template_callback] @@ -419,10 +420,7 @@ pub mod imp { .expect("`ListBoxRow` to have a child") .dynamic_cast::() { - crate::trace!( - "Activated message: {}", - msg.message().body().unwrap_or_default() - ); + log::trace!("Activated message: {:?}", msg.message().id()); msg.open_popup(); } } @@ -445,9 +443,9 @@ pub mod imp { obj, #[upgrade_or_default] move |args| { - let msg = args[1].get::>().expect( - "Type of signal `reply` of `ItemRow` to be `TextMessage`.", - ); + let msg = args[1] + .get::>() + .expect("Type of signal `reply` of `ItemRow` to be `Message`."); obj.set_reply_message(msg); obj.imp().text_entry.grab_focus(); None @@ -505,7 +503,7 @@ pub mod imp { #[weak(rename_to = s)] self, move |_, _| { - s.obj().set_reply_message(None::); + s.obj().set_reply_message(None::); if let Some(channel) = s.active_channel.borrow().as_ref() { let draft = channel.property("draft"); s.text_entry.set_text(draft); diff --git a/src/gui/components/item_row.rs b/src/gui/components/item_row.rs index b2c20d3..93503de 100644 --- a/src/gui/components/item_row.rs +++ b/src/gui/components/item_row.rs @@ -2,13 +2,7 @@ use glib::SignalHandlerId; use crate::prelude::*; -use crate::{ - backend::{ - message::{CallMessage, TextMessage}, - timeline::TimelineItem, - }, - gui::{call_message_item::CallMessageItem, message_item::MessageItem}, -}; +use crate::{backend::message::Message, gui::message_item::MessageItem}; glib::wrapper! { /// A row in the channel messages holding e.g. a [MessageItem]. @@ -24,34 +18,26 @@ impl Default for ItemRow { } impl ItemRow { - fn timeline_item_to_widget(&self, item: &TimelineItem) -> Option { - if let Some(message) = item.dynamic_cast_ref::() { - let widget = MessageItem::new(message); - let handler = widget.connect_local( - "reply", - false, - clone!( - #[weak(rename_to = s)] - self, - #[upgrade_or_default] - move |args| { - let msg = args[1] - .get::() - .expect("Type of signal `reply` of `MessageItem` to be `TextMessage`."); - s.emit_by_name::<()>("reply", &[&msg]); - None - } - ), - ); - self.set_handler(handler); - Some(widget.dynamic_cast().unwrap()) - } else if let Some(message) = item.dynamic_cast_ref::() { - let widget = CallMessageItem::new(message); - Some(widget.dynamic_cast().unwrap()) - } else { - log::warn!("`ItemRow` was asked to display an unknown `TimelineItem`"); - None - } + fn message_item_to_widget(&self, message: &Message) -> Option { + let widget = MessageItem::from_ftypes(message); + let handler = widget.connect_local( + "reply", + false, + clone!( + #[weak(rename_to = s)] + self, + #[upgrade_or_default] + move |args| { + let msg = args[1] + .get::() + .expect("Type of signal `reply` of `MessageItem` to be `Message`."); + s.emit_by_name::<()>("reply", &[&msg]); + None + } + ), + ); + self.set_handler(handler); + Some(widget.dynamic_cast().unwrap()) } /// Set the pending handler of the ItemRow. @@ -62,7 +48,7 @@ impl ItemRow { } mod imp { - use crate::{backend::message::TextMessage, prelude::*}; + use crate::{backend::message::Message, prelude::*}; use glib::{ SignalHandlerId, @@ -70,8 +56,6 @@ mod imp { }; use gtk::CompositeTemplate; - use crate::backend::timeline::TimelineItem; - use std::cell::RefCell; #[derive(Debug, Default, CompositeTemplate)] @@ -99,7 +83,7 @@ mod imp { fn properties() -> &'static [glib::ParamSpec] { static PROPERTIES: Lazy> = Lazy::new(|| { vec![ - glib::ParamSpecObject::builder::("item") + glib::ParamSpecObject::builder::("item") .write_only() .build(), ] @@ -113,8 +97,8 @@ mod imp { match pspec.name() { "item" => { let v = value - .get::>() - .expect("ItemRow to only get TimelineItem"); + .get::>() + .expect("ItemRow to only get Message"); if let Some(handler) = self.handler.take() { if let Some(child) = obj.child() { @@ -126,7 +110,7 @@ mod imp { } } - let w = v.and_then(|v| obj.timeline_item_to_widget(&v)); + let w = v.and_then(|v| obj.message_item_to_widget(&v)); obj.set_child(w.as_ref()); } _ => unimplemented!(), @@ -141,7 +125,7 @@ mod imp { static SIGNALS: Lazy> = Lazy::new(|| -> Vec { vec![ Signal::builder("reply") - .param_types([TextMessage::static_type()]) + .param_types([Message::static_type()]) .build(), ] }); diff --git a/src/gui/components/time_divider.rs b/src/gui/components/time_divider.rs index c196d6a..2fc9dc3 100644 --- a/src/gui/components/time_divider.rs +++ b/src/gui/components/time_divider.rs @@ -23,7 +23,7 @@ mod imp { use gtk::CompositeTemplate; - use crate::backend::timeline::TimelineItem; + use crate::backend::message::Message; #[derive(Debug, Default, CompositeTemplate, glib::Properties)] #[properties(wrapper_type = super::TimeDivider)] @@ -33,11 +33,11 @@ mod imp { label_date: TemplateChild, #[property(set = Self::set_item)] - item: PhantomData, + item: PhantomData, } impl TimeDivider { - fn set_item(&self, item: Option) { + fn set_item(&self, item: Option) { let formatted = item .and_then(|v| v.datetime()) .and_then(|d| Utility::format_date_human(&d)); diff --git a/src/gui/linked_devices_window.rs b/src/gui/linked_devices_window.rs index 8715453..942b7d5 100644 --- a/src/gui/linked_devices_window.rs +++ b/src/gui/linked_devices_window.rs @@ -145,7 +145,7 @@ pub mod imp { log::debug!("Unlinking device: {} {}", device.id(), device.name()); if let Err(e) = manager .unlink_secondary( - libsignal_service::protocol::DeviceId::try_from(device.id()) + ftypes::DeviceId::try_from(device.id()) .expect("DeviceInfo to only hold valid device IDs"), ) .await diff --git a/src/gui/message_item.rs b/src/gui/message_item.rs index 21f504a..1b2deaf 100644 --- a/src/gui/message_item.rs +++ b/src/gui/message_item.rs @@ -3,8 +3,7 @@ use crate::prelude::*; use gio::{SimpleAction, SimpleActionGroup}; use regex::Regex; -use crate::backend::message::{MessageExt, TextMessage}; -use crate::backend::timeline::timeline_item::TimelineItemExt; +use crate::backend::message::Message; use crate::gui::attachment::Attachment; use crate::gui::components::*; @@ -17,11 +16,9 @@ glib::wrapper! { } impl MessageItem { - pub fn new(message: &TextMessage) -> Self { + pub fn from_ftypes(obj: &Message) -> Self { log::trace!("Initializing `MessageItem`"); - let s = Object::builder::() - .property("message", message) - .build(); + let s = Object::builder::().property("message", obj).build(); s.init_label_selectable(); s.setup_showheader(); @@ -104,7 +101,7 @@ impl MessageItem { actions.add_action(&action_open); self.bind_property("message", &action_delete, "enabled") - .transform_to(|_, msg: Option| msg.map(|m| m.sender().is_self())) + .transform_to(|_, msg: Option| msg.map(|m| m.sender().is_self())) .sync_create() .build(); @@ -160,13 +157,13 @@ impl MessageItem { } fn setup_quote(&self) { - if self.message().quote().is_some() { - self.imp().message_bubble.add_css_class("has-quote"); - } + // if self.message().quote().is_some() { + // self.imp().message_bubble.add_css_class("has-quote"); + // } } fn setup_text(&self) { - if self.message().body().is_some() { + if self.message().body().is_some_and(|b| !b.is_empty()) { self.imp().message_bubble.add_css_class("has-text"); self.imp().timestamp_img.set_visible(false); } else { @@ -191,7 +188,7 @@ impl MessageItem { } fn setup_from_group(&self) { - if self.message().channel().group().is_none() { + if self.message().channel().is_contact() { self.message().set_show_header(false); self.imp().avatar.set_visible(false); } @@ -201,7 +198,9 @@ impl MessageItem { lazy_static::lazy_static! { static ref RE: Regex = Regex::new(r"^[[\p{Emoji}--\p{Ascii}]\u{fe0f}\u{200d} ]+$").unwrap(); } - if self.message().body().is_some() && RE.is_match(self.message().body().unwrap().as_str()) { + if let Some(body) = self.message().body() + && RE.is_match(body.as_str()) + { self.imp().message_bubble.add_css_class("emoji"); } } @@ -319,7 +318,7 @@ impl MessageItem { pub mod imp { use std::marker::PhantomData; - use crate::{backend::message::DisplayMessageExt, prelude::*}; + use crate::{backend::Message, prelude::*}; use lazy_static::lazy_static; use regex::Regex; @@ -327,13 +326,9 @@ pub mod imp { use glib::subclass::{InitializingObject, Signal}; use gtk::{CompositeTemplate, EmojiChooser}; - use crate::{ - backend::message::TextMessage, - gui::{ - attachment::{Attachment, backend_to_gui}, - components::*, - error_dialog::ErrorDialog, - }, + use crate::gui::{ + attachment::{Attachment, backend_to_gui}, + components::*, }; use adw::subclass::prelude::BinImpl; @@ -368,8 +363,8 @@ pub mod imp { #[template_child] pub(super) timestamp: TemplateChild, - #[property(get, set = Self::set_message, type = TextMessage)] - message: RefCell>, + #[property(get, set = Self::set_message, type = Message)] + message: RefCell>, #[property(get, set, nullable)] pressed_attachment: RefCell>, #[property(get, set, default = true)] @@ -381,8 +376,6 @@ pub mod imp { #[property(get = Self::shows_media_loading)] shows_media_loading: PhantomData, - #[property(get = Self::has_reaction)] - has_reaction: PhantomData, } #[glib::object_subclass] @@ -407,14 +400,6 @@ pub mod imp { #[gtk::template_callbacks] impl MessageItem { - fn has_reaction(&self) -> bool { - self.message - .borrow() - .as_ref() - .map(|m| !m.reactions().is_empty()) - .unwrap_or_default() - } - fn shows_media_loading(&self) -> bool { self.message .borrow() @@ -424,20 +409,9 @@ pub mod imp { .any(|a| !a.loaded()) } - fn set_message(&self, msg: Option) { + fn set_message(&self, msg: Option) { let obj = self.obj(); if let Some(msg) = &msg { - msg.connect_notify_local( - Some("reactions"), - clone!( - #[weak] - obj, - move |_, _| { - log::trace!("MessageItem got reaction, updating `has-reaction`"); - obj.notify("has-reaction"); - } - ), - ); let attachments = msg.attachments(); let mut container = &self.box_attachments; @@ -488,7 +462,6 @@ pub mod imp { } } } - obj.notify("has-reaction"); self.message.replace(msg); } @@ -535,10 +508,10 @@ pub mod imp { pub(super) fn handle_reply(&self) { let obj = self.obj(); let msg = obj.message(); - crate::trace!( - "Replying to a message: {}", - msg.textual_description().unwrap_or("No Text".to_string()) - ); + // crate::trace!( + // "Replying to a message: {}", + // msg.textual_description().unwrap_or("No Text".to_string()) + // ); obj.emit_by_name::<()>("reply", &[&msg]); } @@ -546,8 +519,13 @@ pub mod imp { pub(super) fn handle_delete(&self) { let obj = self.obj(); let msg = obj.message(); + let manager = obj.manager(); - gspawn!(async move { msg.delete().await }); + gspawn!(async move { + manager + .send_delete_for_everyone(msg.channel().thread(), msg.id()) + .await; + }); } // Signal uses the old unicode for the heart emoji, which is recognized as a black heart by gtk. This function converts it to the standard red heart @@ -563,6 +541,7 @@ pub mod imp { #[template_callback] pub(super) fn handle_react(&self, emoji: String) { let obj = self.obj(); + let manager = obj.manager(); let msg = obj.message(); crate::trace!( @@ -576,19 +555,12 @@ pub mod imp { #[strong] msg, #[strong] - obj, + manager, async move { log::trace!("Sending message"); - if let Err(e) = msg.send_reaction(emoji).await { - let root = obj - .root() - .expect("`MessageItem` to have a root") - .dynamic_cast::() - .expect("Root of `ChannelMessages` to be a `Window`."); - let dialog = ErrorDialog::new(&e, &root); - dialog.present(Some(&root)); - } - obj.notify("has-reaction"); + manager + .send_reaction(msg.channel().thread(), msg.id(), emoji) + .await; } )); obj.imp().msg_menu.popdown(); @@ -600,10 +572,6 @@ pub mod imp { let display = gdk::Display::default().expect("there should be a display"); let clipboard = display.clipboard(); let msg = obj.message(); - crate::trace!( - "Copying message to clipboard: {}", - msg.textual_description().unwrap_or("No Text".to_string()) - ); if let Some(text) = msg.body() { clipboard.set_text(&text) } @@ -635,7 +603,7 @@ pub mod imp { static SIGNALS: Lazy> = Lazy::new(|| -> Vec { vec![ Signal::builder("reply") - .param_types([TextMessage::static_type()]) + .param_types([Message::static_type()]) .build(), ] }); diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 6b5cfd1..d8cb81d 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -1,5 +1,4 @@ mod attachment; -mod call_message_item; mod channel_info_dialog; mod channel_item; mod channel_item_compact; diff --git a/src/gui/setup_window.rs b/src/gui/setup_window.rs index afd2384..d261674 100644 --- a/src/gui/setup_window.rs +++ b/src/gui/setup_window.rs @@ -1,8 +1,8 @@ use crate::prelude::*; use crate::backend::{Server, SetupResult}; +use ftypes::SignalServers; use glib::subclass::types::ObjectSubclassIsExt; -use libsignal_service::configuration::SignalServers; use super::Window; @@ -46,6 +46,8 @@ pub mod imp { use adw::Toast; use adw::subclass::dialog::AdwDialogImplExt; + use fmanager::LinkSetup; + use fmanager::RegisterSetup; use gdk_pixbuf::Pixbuf; use gio::{ListStore, MemoryInputStream}; use glib::{Bytes, subclass::InitializingObject}; @@ -56,10 +58,11 @@ pub mod imp { use std::str::FromStr; - use libsignal_service::configuration::SignalServers; - use libsignal_service::prelude::phonenumber::PhoneNumber; + use fmanager::SetupDecision; + use ftypes::PhoneNumber; + use ftypes::SignalServers; - use crate::backend::{Server, SetupDecision, SetupResult}; + use crate::backend::{Server, SetupResult}; use crate::gui::Window; #[derive(CompositeTemplate, Default, glib::Properties)] @@ -169,14 +172,15 @@ pub mod imp { .settings() .set_string("link-device-name", &device_name); callback - .send(SetupDecision::Link( - self.dropdown_link_server + .send(SetupDecision::Link(LinkSetup { + servers: self + .dropdown_link_server .selected_item() .and_dynamic_cast::() .map(|s| s.server()) .unwrap_or(SignalServers::Production), device_name, - )) + })) .expect("Failed to send setup decision"); // XXX: Maybe display spinner afterwards? } @@ -207,15 +211,16 @@ pub mod imp { // Should always be given due to UI only being on that page after setup decision was asked for. if let Some(callback) = self.decision_callback.take() { callback - .send(SetupDecision::Register( - self.dropdown_primary_server + .send(SetupDecision::Register(RegisterSetup { + servers: self + .dropdown_primary_server .selected_item() .and_dynamic_cast::() .map(|s| s.server()) .unwrap_or(SignalServers::Production), - phone, + phone_number: phone, captcha, - )) + })) .expect("Failed to send setup decision"); // XXX: Maybe display spinner afterwards? } diff --git a/src/gui/utility.rs b/src/gui/utility.rs index 85fc1de..60b47cc 100644 --- a/src/gui/utility.rs +++ b/src/gui/utility.rs @@ -2,8 +2,8 @@ use crate::prelude::*; use std::str::FromStr; +use ftypes::PhoneNumber; use glib::{DateTime, Object}; -use libsignal_service::prelude::phonenumber::PhoneNumber; pub struct Utility {} diff --git a/src/gui/window.rs b/src/gui/window.rs index da981f7..64b9484 100644 --- a/src/gui/window.rs +++ b/src/gui/window.rs @@ -1,5 +1,4 @@ use gio::SimpleAction; -use presage::store::Thread; use crate::{backend::Channel, prelude::*}; @@ -32,20 +31,20 @@ impl Window { .property("application", app) .build(); - let action_notification_clicked = SimpleAction::new( - "notification-clicked", - Some(&<(u8, Vec)>::static_variant_type()), - ); + let action_notification_clicked = + SimpleAction::new("notification-clicked", Some(&u64::static_variant_type())); action_notification_clicked.connect_activate(clone!( #[weak] window, move |_, variant| { - let variant: (u8, Vec) = variant + let variant: u64 = variant .expect("Notification-clicked action to have attached data") .get() .expect("Variant of notification is of unexpected type"); - let thread = Channel::from_notification_variant(variant); - window.activate_channel(thread); + let manager = window.manager(); + if let Some(thread) = manager.notification_variant_to_channel(variant) { + window.activate_channel(thread); + } window.present(); } )); @@ -94,11 +93,8 @@ impl Window { } } - fn activate_channel(&self, thread: Thread) { - let channel = self - .property::("manager") - .channel_from_thread(thread); - self.imp().channel_list.set_active_channel(channel); + fn activate_channel(&self, channel: Channel) { + self.imp().channel_list.set_active_channel(Some(channel)); } pub(crate) fn settings(&self) -> gio::Settings { @@ -656,7 +652,6 @@ pub mod imp { crate::gui::channel_messages::ChannelMessages::ensure_type(); crate::gui::setup_window::SetupWindow::ensure_type(); crate::gui::error_dialog::ErrorDialog::ensure_type(); - crate::backend::timeline::TimelineItem::ensure_type(); crate::gui::new_channel_dialog::NewChannelDialog::ensure_type(); Self::bind_template(klass); @@ -771,6 +766,7 @@ pub mod imp { obj, #[upgrade_or_default] move |_, _| { + obj.imp().channel_list.scroll_up(); obj.imp().stack.set_visible_child_name("main"); } ), @@ -811,6 +807,14 @@ pub mod imp { log::warn!("Failed to save window state, {}", &err); } + gspawn!(clone!( + #[strong(rename_to = manager)] + self.obj().manager(), + async move { + manager.shutdown().await; + } + )); + self.parent_close_request() } } diff --git a/src/main.rs b/src/main.rs index 3759fea..6d1da93 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,7 +16,6 @@ mod utils; mod prelude { pub use adw::{prelude::*, subclass::prelude::*}; pub use glib::subclass::*; - pub use libsignal_service::prelude::*; pub use gtk::{gdk, gdk_pixbuf, gio, glib, pango}; diff --git a/src/utils.rs b/src/utils.rs index 7829cc7..4c905f9 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,7 +1,5 @@ use crate::prelude::*; -use libsignal_service::profile_name::ProfileName; - #[macro_export] macro_rules! gspawn { ($future:expr) => { @@ -22,14 +20,6 @@ pub fn is_flatpak() -> bool { file.query_exists(gio::Cancellable::NONE) } -pub fn format_profile_name(p: &ProfileName) -> String { - if let Some(family_name) = &p.family_name { - format!("{} {}", p.given_name, family_name) - } else { - p.given_name.clone() - } -} - pub fn chrono_to_glib_datetime(chrono: chrono::DateTime) -> Option { glib::DateTime::from_unix_utc(chrono.timestamp()) .ok()