Merge branch 'flare-backend' into 'master'

Port to new flare-backend

See merge request schmiddi-on-mobile/flare!224
This commit is contained in:
Julian Schmidhuber committed 2026-06-16 17:07:27 +00:00
commit dde5e778f0
43 files changed
+1992 -4840

No files matched your search

+4
View File
@@ -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
Generated
+76 -5
View File
@@ -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",
+9 -3
View File
@@ -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"
+1 -1
View File
@@ -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 <bool>,
template.channel as <$FlChannel>.draft as <string>
) as <string>;
@@ -26,5 +26,6 @@ template $FlChannelItemCompact: Box {
hexpand: true;
ellipsize: end;
use-markup: true;
label: bind template.channel as <$FlChannel>.title;
}
}
+2 -2
View File
@@ -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 <string>;
label: bind $attachment_placeholder(template.reply-message as <$FlMessage>.body) as <string>;
wrap: true;
wrap-mode: word_char;
lines: 2;
@@ -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 <int>;
height-request: bind $resize_height(
template.attachment as <$FlAttachmentObject>.width,
template.attachment as <$FlAttachmentObject>.height
) as <int>;
width-request: bind $resize_width(template.attachment as <$FlAttachmentObject>.width) as <int>;
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 <int>;
width-request: bind $resize_width(template.attachment as <$FlAttachmentObject>.width) as <int>;
overflow: hidden;
paintable: bind template.attachment as <$FlAttachmentObject>.image;
content-fit: scale_down;
}
}
}
+48 -18
View File
@@ -56,8 +56,6 @@ template $FlMessageItem: $ContextMenuBin {
"message-item",
]
visible: bind $not(template.message as <$FlTextMessage>.is-deleted) as <bool>;
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 <bool>;
visible: bind $is_some(template.message as <$FlMessage>.quote) as <bool>;
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 <string>
$markup_urls(template.message as <$FlMessage>.quote as <$FlMessage>.body) as <string>
) as <string>;
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 <string>;
timestamp: bind $format_time_human(template.message as <$FlMessage>.datetime) as <string>;
}
}
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 <string>;
attributes: bind template.message as <$FlTextMessage>.message-attributes;
label: bind $markup_urls(template.message as <$FlMessage>.body) as <string>;
attributes: bind template.message as <$FlMessage>.message-attributes;
visible: bind $not(template.message as <$FlMessage>.is-deleted) as <bool>;
indicators: $FlMessageIndicators timestamp {
timestamp: bind $format_time_human(template.message as <$FlTextMessage>.datetime) as <string>;
visible: bind $not_empty(template.message as <$FlTextMessage>.body) as <bool>;
timestamp: bind $format_time_human(template.message as <$FlMessage>.datetime) as <string>;
};
}
@@ -275,8 +305,8 @@ template $FlMessageItem: $ContextMenuBin {
"reaction",
]
label: bind $fix_emoji(template.message as <$FlTextMessage>.reactions) as <string>;
visible: bind template.has-reaction;
label: bind $fix_emoji(template.message as <$FlMessage>.reactions) as <string>;
visible: bind template.message as <$FlMessage>.has-reactions;
wrap-mode: word;
justify: left;
vexpand: false;
+2 -11
View File
@@ -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
+47 -35
View File
@@ -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<u8>) {
pub async fn as_upload_attachment(&self) -> AttachmentOrPointer {
let file = self.property::<File>("file");
let image = self.property::<Option<Texture>>("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(),
)
}
}
+98 -523
View File
@@ -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<imp::Channel>);
}
const EMPTY_MESSAGE_BODY: &str = "<empty>";
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<GroupContextV2>,
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::<Self>()
.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::<Self>()
.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::<DisplayMessage>())
.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::<DisplayMessage>() {
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::<TimelineItem>()
.expect("A 'DisplayMessage' to be a 'TimelineItem'")
i.dynamic_cast::<Message>()
.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<GroupContextV2> {
self.imp().group_context.borrow().clone()
}
pub fn group(&self) -> std::cell::Ref<'_, Option<Group>> {
self.imp().group.borrow()
}
pub fn contact(&self) -> Option<Contact> {
self.imp().contact.borrow().clone()
}
pub fn uuid(&self) -> Option<ServiceId> {
self.imp()
.contact
.borrow()
.as_ref()
.and_then(|c| c.address())
pub fn account_id(&self) -> Option<AccountId> {
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::<TextMessage>() {
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::<TextMessage>()
{
crate::trace!(
"Message {} quotes other message {}",
body,
quoted_msg
.property::<Option<String>>("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::<Vec<_>>()
);
for reaction in reactions {
message.react(reaction);
}
}
}
// Apply reactions or store them.
if let Some(reaction) = message.dynamic_cast_ref::<ReactionMessage>() {
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::<DisplayMessage>().ok());
if let Some(reacted_msg) = reacted_msg {
if let Some(reacted_msg) = reacted_msg.dynamic_cast_ref::<TextMessage>() {
crate::trace!(
"Reaction to message {}",
reacted_msg
.property::<Option<String>>("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::<DeletionMessage>()
.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::<DisplayMessage>().ok());
if let Some(deleted_msg) = deleted_msg {
if let Some(deleted_msg) = deleted_msg.dynamic_cast_ref::<TextMessage>() {
crate::trace!(
"Deletion to message {}",
deleted_msg
.property::<Option<String>>("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::<DisplayMessage>() {
if let Some(message) = message.dynamic_cast_ref::<Message>() {
self.imp().timeline.borrow().append(
message
.clone()
.dynamic_cast::<TimelineItem>()
.expect("A 'DisplayMessage' to be a 'TimelineItem'"),
.dynamic_cast::<Message>()
.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::<ReactionMessage>() {
if self.manager().settings().boolean("notify-reactions")
&& !message.property::<bool>("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::<DisplayMessage>() {
self.imp().timeline.borrow().append(
msg.clone()
.dynamic_cast::<TimelineItem>()
.expect("A 'DisplayMessage' to be a 'TimelineItem'"),
message
.clone()
.dynamic_cast::<Message>()
.expect("A 'Message' to be a 'Message'"),
);
}
if let Some(text_msg) = msg.dynamic_cast_ref::<TextMessage>() {
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::<Option<String>>("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::<Vec<_>>();
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<String> {
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(&notification.sender.uuid()).map(|t| t.timestamp)
if typing
.get(&notification.sender.account_id())
.map(|t| t.timestamp)
== Some(notification.timestamp)
{
typing.remove(&notification.sender.uuid());
typing.remove(&notification.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>, 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<DisplayMessage> {
fn first_unread_message(&self) -> Option<Message> {
self.imp()
.timeline
.borrow()
.iter_backwards()
.filter(|i| i.is::<DisplayMessage>())
.map_while(|m| {
let message = m.dynamic_cast::<DisplayMessage>().unwrap();
let message = m.dynamic_cast::<Message>().unwrap();
// We can stop at first read message
if !message.property::<bool>("read") {
return Some(message);
@@ -641,7 +278,7 @@ impl Channel {
.timeline
.borrow()
.iter_backwards()
.filter(|i| i.is::<DisplayMessage>() || i.is::<ReactionMessage>())
.filter(|i| i.is::<Message>())
.map_while(|m| {
let message = m.dynamic_cast::<Message>().unwrap();
// We can stop at first read message
@@ -652,37 +289,11 @@ impl Channel {
})
.collect()
}
pub fn notification_variant(&self) -> (u8, Vec<u8>) {
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<u8>)) -> 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<Option<Contact>>,
pub(super) group: RefCell<Option<Group>>,
pub(super) group_context: RefCell<Option<GroupContextV2>>,
#[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<Option<ftypes::Thread>>,
pub(super) participants: RefCell<Vec<Contact>>,
pub(super) pending_reactions: RefCell<HashMap<u64, Vec<ReactionMessage>>>,
pub(super) typing: RefCell<HashMap<Uuid, TypingNotification>>,
pub(super) typing: RefCell<HashMap<AccountId, TypingNotification>>,
#[property(name = "avatar", get = Self::avatar)]
pub(super) group_avatar: RefCell<Option<Paintable>>,
#[property(name = "avatar", get, set)]
pub(super) avatar: RefCell<Option<Paintable>>,
#[property(get)]
pub(super) timeline: RefCell<Timeline>,
@@ -718,11 +325,11 @@ mod imp {
pub(super) is_active: RefCell<bool>,
#[property(get = Self::last_message)]
pub(super) last_message: PhantomData<Option<DisplayMessage>>,
#[property(get = Self::title)]
pub(super) title: PhantomData<String>,
pub(super) last_message: PhantomData<Option<Message>>,
#[property(get = Self::is_contact)]
pub(super) is_contact: PhantomData<bool>,
#[property(get = Self::is_group)]
pub(super) is_group: PhantomData<bool>,
#[property(get = Self::is_self)]
pub(super) is_self: PhantomData<bool>,
/// In seconds. A value of 0 means messages don't disappear.
@@ -742,81 +349,49 @@ mod imp {
}
impl Channel {
fn avatar(&self) -> Option<Paintable> {
if let Some(contact) = self.contact.borrow().as_ref() {
contact.property("avatar")
} else {
self.group_avatar.borrow().clone()
}
}
fn last_message(&self) -> Option<DisplayMessage> {
fn last_message(&self) -> Option<Message> {
self.timeline
.borrow()
.iter_backwards()
.filter(|i| i.is::<DisplayMessage>())
.map(|m| m.dynamic_cast::<DisplayMessage>().unwrap())
.map(|m| m.dynamic_cast::<Message>().unwrap())
.find(|m| {
!(*m)
.clone()
.downcast::<TextMessage>()
.downcast::<Message>()
.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::<bool>("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<String> {
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<String> {
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<Vec<Signal>> = Lazy::new(|| -> Vec<Signal> {
vec![
Signal::builder("message")
.param_types([DisplayMessage::static_type()])
.param_types([Message::static_type()])
.build(),
]
});
+27 -238
View File
@@ -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::<Self>()
.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::<Self>()
.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<ServiceId> {
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<String> {
self.imp()
.contact
.borrow()
.as_ref()
.and_then(|c| c.phone_number.as_ref().map(|p| p.to_string()))
}
pub fn description(&self) -> Option<String> {
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>, 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<Option<presage::model::contacts::Contact>>,
pub(super) phonenumber: RefCell<Option<PhoneNumber>>,
pub(super) uuid: RefCell<Option<Uuid>>,
pub(super) profile: RefCell<Option<Profile>>,
#[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<Option<ftypes::Contact>>,
#[property(get, set)]
pub(crate) avatar: RefCell<Option<Paintable>>,
@@ -250,52 +80,11 @@ mod imp {
#[property(get = Self::is_self)]
is_self: PhantomData<bool>,
#[property(get = Self::title)]
title: PhantomData<String>,
}
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()
}
}
+1 -1
View File
@@ -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::<Self>()
.property("id", u8::from(info.id))
.property("name", info.name)
+377 -440
View File
@@ -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<presage_store_sqlite::SqliteStoreError>;
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::<Message>()
};
($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::<Message>()
}};
}
pub fn dummy_presage_contacts() -> Vec<LContact> {
fn threads() -> Vec<Thread> {
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<Contact> {
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<MessageId>,
}
fn construct_message<S: AsRef<str>>(
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<RichMessage> {
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<P: AsRef<Path>>(&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<Option<ftypes::RichMessage>, ftypes::Error> {
Ok(messages().into_iter().find(|m| m.id == id))
}
pub(super) async fn messages_inner(
&self,
thread: ThreadId,
range: impl RangeBounds<DateTime<Utc>>,
) -> Result<impl Stream<Item = Result<ftypes::RichMessage, ftypes::Error>>, ftypes::Error> {
Ok(futures::stream::iter(
messages()
.into_iter()
.filter(|m| m.thread == thread && range.contains(&m.id.timestamp))
.map(Ok)
.rev()
.collect::<Vec<_>>(),
))
}
pub(super) async fn channels(&self) -> Result<Vec<ftypes::Thread>, ftypes::Error> {
Ok(threads())
}
pub async fn devices(&self) -> Result<Vec<DeviceInfo>, ftypes::Error> {
todo!()
}
pub(super) async fn thread_avatar(
&self,
thread: &Thread,
timestamp: u64,
) -> Result<Option<Message>, 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<u64>,
) -> Result<impl Iterator<Item = Content>, 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<Profile, PresageError> {
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<Option<Vec<u8>>, 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::<gtk::gio::Cancellable>.as_ref())
.expect("Failed to load icon bytes")
.0
.to_vec(),
))
) -> Result<Option<Texture>, 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<Option<libsignal_service::prelude::ProfileKey>, 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<Option<presage::model::contacts::Contact>, 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<Option<Vec<u8>>, PresageError> {
_contact: &Contact,
) -> Result<Option<Texture>, ftypes::Error> {
Ok(None)
}
#[cfg(feature = "screenshot")]
pub async fn devices(&self) -> Result<Vec<DeviceInfo>, 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<u8>)>,
) -> Result<Vec<Result<AttachmentPointer, AttachmentUploadError>>, PresageError> {
Ok(vec![Ok(AttachmentPointer::default())])
}
#[cfg(feature = "screenshot")]
async fn dummy_messages(&self) -> Vec<Message> {
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::<TextMessage>()
.unwrap()
.set_quote(&msg_replied.clone().downcast::<TextMessage>().unwrap());
msg_replied
.clone()
.downcast::<TextMessage>()
.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::<TextMessage>()
.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<Contact> {
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<Channel> {
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<Option<ftypes::Contact>, ftypes::Error> {
Ok(contacts().into_iter().find(|c| c.id == id))
}
}
+616 -648
View File
File diff suppressed because it is too large. Load diff
-812
View File
@@ -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<<Store as presage::store::Store>::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<Uuid>),
SubmitRecaptchaChallenge(String, String, oneshot::Sender<Result<(), Error>>),
RetrieveProfileByUuid(Uuid, ProfileKey, oneshot::Sender<Result<Profile, Error>>),
RetrieveProfile(oneshot::Sender<Result<Profile, Error>>),
GetGroupV2(
[u8; 32],
oneshot::Sender<Result<Option<Group>, <Store as presage::store::Store>::Error>>,
),
SendSessionReset(ServiceId, u64, oneshot::Sender<Result<(), Error>>),
SendMessage(
ServiceId,
Box<ContentBody>,
u64,
oneshot::Sender<Result<(), Error>>,
),
SendMessageToGroup(
Vec<u8>,
Box<DataMessage>,
u64,
oneshot::Sender<Result<(), Error>>,
),
GetAttachment(AttachmentPointer, oneshot::Sender<Result<Vec<u8>, Error>>),
UploadAttachments(
Vec<(AttachmentSpec, Vec<u8>)>,
oneshot::Sender<Result<Vec<Result<AttachmentPointer, AttachmentUploadError>>, Error>>,
),
Messages(
Thread,
(Bound<u64>, Bound<u64>),
oneshot::Sender<
Result<
<presage_store_sqlite::SqliteStore as presage::store::ContentsStore>::MessagesIter,
Error,
>,
>,
),
RegistrationType(oneshot::Sender<RegistrationType>),
LinkSecondary(Url, oneshot::Sender<Result<(), Error>>),
UnlinkSecondary(DeviceId, oneshot::Sender<Result<(), Error>>),
Devices(oneshot::Sender<Result<Vec<DeviceInfo>, Error>>),
RequestContacts(oneshot::Sender<Result<(), Error>>),
RetrieveProfileAvatarByUuid(
Uuid,
ProfileKey,
oneshot::Sender<Result<Option<Vec<u8>>, Error>>,
),
RetrieveGroupAvatar(
GroupContextV2,
oneshot::Sender<Result<Option<Vec<u8>>, 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<futures::channel::oneshot::Sender<SetupDecision>>),
/// The manager is pending a SMS confirmation code.
Confirm(OnceCell<futures::channel::oneshot::Sender<SetupConfirmation>>),
/// 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<Command>,
/// Cache for how the device is registered (primary or secondary).
registration_type: Option<RegistrationType>,
/// Cache for the own UUID.
uuid: Uuid,
/// Cache for the own profile.
profile: Option<Profile>,
}
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<SetupResult>,
error_callback: futures::channel::oneshot::Sender<Error>,
content: mpsc::UnboundedSender<Received>,
error: mpsc::Sender<ApplicationError>,
) -> Option<Self> {
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<RegistrationType> {
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<Profile, Error> {
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<Profile> {
self.profile.clone()
}
pub async fn get_group_v2(
&self,
group_master_key: [u8; 32],
) -> Result<Option<Group>, <Store as presage::store::Store>::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<ServiceId>,
message: impl Into<ContentBody>,
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<ServiceId>,
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<u8>,
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<Vec<u8>, 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<u8>)>,
) -> Result<Vec<Result<AttachmentPointer, AttachmentUploadError>>, 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<u64>, Bound<u64>),
) -> Result<
<presage_store_sqlite::SqliteStore as presage::store::ContentsStore>::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<Vec<DeviceInfo>, 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<Option<Vec<u8>>, 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<Option<Vec<u8>>, 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<SetupResult>,
) -> Result<presage::Manager<Store, Registered>, 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<Store, Registered>,
mut receiver: mpsc::Receiver<Command>,
mut content: mpsc::UnboundedSender<Received>,
mut error: mpsc::Sender<ApplicationError>,
) {
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<<Store as presage::store::Store>::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<RwLock<Manager<Store, Registered>>>, 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"),
}
}
+464
View File
@@ -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<imp::Message>);
}
impl Message {
pub(super) async fn from_ftypes(
inner: ftypes::RichMessage,
manager: &Manager,
) -> Result<Self, ftypes::Error> {
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::<Message>()
.property("manager", manager)
.property("sender", &contact)
.property("channel", &channel)
.property("quote", &quote)
.property("read", channel.property::<bool>("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<String> {
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<ftypes::TextMessage> {
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<Attachment> {
self.imp().attachments.borrow().clone()
}
pub fn quote_timestamp(&self) -> Option<u64> {
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<String>, 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<Attribute> = 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::<super::Message>()) {
// 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::<super::Message>()) {
// 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<Option<Contact>>,
#[property(get, set, construct_only, type = Channel)]
channel: RefCell<Option<Channel>>,
#[property(get, set, construct_only)]
pub(super) read: RefCell<bool>,
#[property(get, set)]
pub(super) pending: RefCell<bool>,
#[property(get, set)]
pub(super) error: RefCell<bool>,
#[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<Option<ftypes::RichMessage>>,
#[property(get, set, construct_only, type = Manager)]
manager: RefCell<Option<Manager>>,
#[property(get, set, nullable)]
pub(super) quote: RefCell<Option<super::Message>>,
pub(super) attachments: RefCell<Vec<Attachment>>,
#[property(get = Self::textual_description)]
pub(super) textual_description: PhantomData<Option<String>>,
#[property(name = "body", get)]
pub(super) formatted_body: RefCell<Option<String>>,
#[property(get)]
pub(super) message_attributes: RefCell<AttrList>,
#[property(get, set)]
pub(super) requires_attention: Cell<bool>,
#[property(get, set)]
show_header: Cell<bool>,
#[property(get, set)]
show_timestamp: Cell<bool>,
#[property(get = Self::datetime)]
datetime: PhantomData<Option<glib::DateTime>>,
}
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<String> {
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> {
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 {}
}
-121
View File
@@ -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<Self, Self::Error> {
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<imp::CallMessage>) @extends Message, DisplayMessage, TimelineItem;
}
impl CallMessage {
pub fn from_call(
sender: &Contact,
channel: &Channel,
timestamp: u64,
manager: &Manager,
call: PreCallMessage,
) -> Option<Self> {
let call_type = CallMessageType::try_from(&call).ok()?;
let s: Self = Object::builder::<Self>()
.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<Option<PreCallMessage>>,
#[property(get, set, construct_only, default = CallMessageType::default(), builder(CallMessageType::default()))]
pub(super) call_type: RefCell<CallMessageType>,
}
#[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<String> {
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::<Message>();
upcast.imp().update_show_header(upcast, previous);
}
fn update_show_timestamp(&self, obj: &Self::Type, previous: Option<&TimelineItem>) {
let upcast = obj.upcast_ref::<Message>();
upcast.imp().update_show_timestamp(upcast, previous);
}
}
impl MessageImpl for CallMessage {}
#[glib::derived_properties]
impl ObjectImpl for CallMessage {}
}
-65
View File
@@ -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<imp::DeletionMessage>) @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::<Self>()
.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<Option<Delete>>,
}
#[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 {}
}
-196
View File
@@ -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<imp::DisplayMessage>) @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(&notification_title);
notification.set_body(Some(&notification_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, &notification);
}
}
pub trait DisplayMessageExt: 'static {
fn textual_description(&self) -> Option<String>;
fn flash_requires_attention(&self);
}
impl<O: IsA<DisplayMessage>> DisplayMessageExt for O {
fn textual_description(&self) -> Option<String> {
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<String> {
None
}
}
unsafe impl<T> IsSubclassable<T> for DisplayMessage
where
T: DisplayMessageImpl + MessageImpl + TimelineItemImpl,
T::Type: IsA<DisplayMessage> + IsA<Message> + IsA<TimelineItem>,
{
fn class_init(class: &mut glib::Class<Self>) {
Self::parent_class_init::<T>(class.upcast_ref_mut());
let klass = class.as_mut();
klass.textual_description = textual_description_trampoline::<T>;
}
}
fn textual_description_trampoline<T>(this: &DisplayMessage) -> Option<String>
where
T: ObjectSubclass + DisplayMessageImpl,
T::Type: IsA<DisplayMessage>,
{
let this = this.downcast_ref::<T::Type>().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<Message>,
pub textual_description: fn(&super::DisplayMessage) -> Option<String>,
}
unsafe impl ClassStruct for DisplayMessageClass {
type Type = DisplayMessage;
}
pub(super) fn display_message_textual_description(
this: &super::DisplayMessage,
) -> Option<String> {
let klass = this.class();
(klass.as_ref().textual_description)(this)
}
#[derive(Debug, Default)]
pub struct DisplayMessage {
pub(super) requires_attention: Cell<bool>,
}
#[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<Vec<ParamSpec>> = 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::<Message>();
upcast.imp().update_show_header(upcast, previous);
}
fn update_show_timestamp(&self, obj: &Self::Type, previous: Option<&TimelineItem>) {
let upcast = obj.upcast_ref::<Message>();
upcast.imp().update_show_timestamp(upcast, previous);
}
}
impl MessageImpl for DisplayMessage {}
}
-495
View File
@@ -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<imp::Message>)
@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<Self> {
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::<TextMessage>()
.property("manager", manager)
.property("sender", &contact)
.property("channel", &channel)
.property("timestamp", timestamp)
.property("read", channel.property::<bool>("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::<TextMessage>()
.property("manager", manager)
.property("sender", &contact)
.property("channel", &channel)
.property("timestamp", timestamp)
.property("read", channel.property::<bool>("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(&notification_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<String> {
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<DataMessage>) {
self.imp().data.swap(&RefCell::new(data));
}
fn internal_data(&self) -> Option<DataMessage> {
self.imp().data.borrow().clone()
}
fn internal_data_mut(&self) -> RefMut<'_, Option<DataMessage>> {
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<String> {
self.dynamic_cast_ref::<Message>()
.expect("`MessageExt` to dynamic cast to `Message`")
.uid()
}
fn set_internal_data(&self, data: Option<DataMessage>) {
self.dynamic_cast_ref::<Message>()
.expect("`MessageExt` to dynamic cast to `Message`")
.set_internal_data(data)
}
fn internal_data(&self) -> Option<DataMessage> {
self.dynamic_cast_ref::<Message>()
.expect("`MessageExt` to dynamic cast to `Message`")
.internal_data()
}
fn internal_data_mut(&self) -> RefMut<'_, Option<DataMessage>> {
self.dynamic_cast_ref::<Message>()
.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::<Message>()
.expect("`MessageExt` to dynamic cast to `Message`")
.mark_as_read()
}
}
impl<O: IsA<Message>> MessageExt for O {}
pub trait MessageImpl: ObjectImpl {}
unsafe impl<T> IsSubclassable<T> for Message
where
T: MessageImpl + TimelineItemImpl,
T::Type: IsA<Message> + IsA<TimelineItem>,
{
}
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<Option<Contact>>,
#[property(get, set, construct_only, type = Channel)]
channel: RefCell<Option<Channel>>,
#[property(get, set, construct_only)]
pub(super) read: RefCell<bool>,
#[property(get, set)]
pub(super) pending: RefCell<bool>,
#[property(get, set)]
pub(super) error: RefCell<bool>,
pub(super) data: RefCell<Option<DataMessage>>,
#[property(get, set, construct_only, type = Manager)]
manager: RefCell<Option<Manager>>,
}
#[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::<super::Message>()) {
// 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::<super::Message>()) {
// 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);
}
}
}
}
-131
View File
@@ -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<imp::ReactionMessage>) @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::<Self>()
.property("sender", sender)
.property("channel", channel)
.property("timestamp", timestamp)
.property("manager", manager)
.property("read", channel.property::<bool>("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(&notification_title);
notification.set_body(Some(&notification_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, &notification);
}
}
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<Option<Reaction>>,
}
#[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 {}
}
-449
View File
@@ -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<imp::TextMessage>) @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<S: AsRef<str>>(
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<S: AsRef<str>>(
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<S: AsRef<str>>(
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::<Self>()
.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<u64> {
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<S: AsRef<str>>(
&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<Attachment> {
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::<Result<Vec<_>, _>>()?;
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<String>, 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<u16> = 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<Option<super::TextMessage>>,
#[property(get = Self::reactions, type = String, default = Some(""))]
pub(super) reactions: RefCell<HashMap<Uuid, Reaction>>,
pub(super) attachments: RefCell<Vec<Attachment>>,
#[property(name = "body", get)]
pub(super) formatted_body: RefCell<Option<String>>,
#[property(get)]
pub(super) message_attributes: RefCell<AttrList>,
#[property(get, set)]
pub(super) is_deleted: RefCell<bool>,
}
impl TextMessage {
fn reactions(&self) -> String {
let mut reaction_emojis: Vec<String> = Vec::new();
let mut reaction_counts: Vec<i32> = 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::<Vec<String>>()
.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::<Message>();
upcast.imp().update_show_header(upcast, previous);
}
fn update_show_timestamp(&self, obj: &Self::Type, previous: Option<&TimelineItem>) {
let upcast = obj.upcast_ref::<Message>();
upcast.imp().update_show_timestamp(upcast, previous);
}
}
impl DisplayMessageImpl for TextMessage {
fn textual_description(&self, obj: &super::TextMessage) -> Option<String> {
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 {}
}
+1 -3
View File
@@ -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;
+2 -2
View File
@@ -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)]
@@ -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<imp::Timeline>)
@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<Utc>) {
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<TimelineItem> {
pub fn get_by_timestamp(&self, timestamp: DateTime<Utc>) -> Option<Message> {
let timestamp = timestamp.timestamp_millis() as u64;
let current_items = self.imp().list.borrow();
let index = current_items.binary_search_by_key(&timestamp, |i| i.timestamp());
match index {
@@ -53,12 +48,12 @@ impl Timeline {
}
}
pub fn iter_forwards(&self) -> impl Iterator<Item = TimelineItem> + 'static {
pub fn iter_forwards(&self) -> impl Iterator<Item = Message> + 'static {
let current_items = self.imp().list.borrow();
current_items.clone().into_iter()
}
pub fn iter_backwards(&self) -> impl Iterator<Item = TimelineItem> + 'static {
pub fn iter_backwards(&self) -> impl Iterator<Item = Message> + '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<TimelineItem>) {
pub fn prepend(&self, items: Vec<Message>) {
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<VecDeque<TimelineItem>>,
pub list: RefCell<VecDeque<Message>>,
}
#[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 {
-158
View File
@@ -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<imp::TimelineItem>);
}
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<O: IsA<TimelineItem>> 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<T> IsSubclassable<T> for TimelineItem
where
T: TimelineItemImpl,
T::Type: IsA<TimelineItem>,
{
fn class_init(class: &mut glib::Class<Self>) {
Self::parent_class_init::<T>(class.upcast_ref_mut());
let klass = class.as_mut();
klass.update_show_header = update_show_header_trampoline::<T>;
klass.update_show_timestamp = update_show_timestamp_trampoline::<T>;
}
}
fn update_show_header_trampoline<T>(this: &TimelineItem, previous: Option<&TimelineItem>)
where
T: ObjectSubclass + TimelineItemImpl,
T::Type: IsA<TimelineItem>,
{
let this = this.downcast_ref::<T::Type>().unwrap();
this.imp().update_show_header(this, previous)
}
fn update_show_timestamp_trampoline<T>(this: &TimelineItem, next: Option<&TimelineItem>)
where
T: ObjectSubclass + TimelineItemImpl,
T::Type: IsA<TimelineItem>,
{
let this = this.downcast_ref::<T::Type>().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<u64>,
#[property(get, set)]
show_header: Cell<bool>,
#[property(get, set)]
show_timestamp: Cell<bool>,
#[property(get = Self::datetime)]
datetime: PhantomData<Option<glib::DateTime>>,
}
impl TimelineItem {
// Time as [glib::DateTime]. Should not return `None`, but just in case.
fn datetime(&self) -> Option<glib::DateTime> {
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 {}
}
+11 -125
View File
@@ -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<presage_store_sqlite::SqliteStoreError>;
#[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<libsignal_service::sender::MessageSenderError>),
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<PresageError>),
ConfigurationError(ConfigurationError),
ManagerThreadPanic,
Backend(ftypes::Error),
}
impl From<PresageError> 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<ftypes::Error> for ApplicationError {
fn from(value: ftypes::Error) -> Self {
Self::Backend(value)
}
}
@@ -111,72 +62,25 @@ impl From<glib::Error> for ApplicationError {
}
}
impl From<presage_store_sqlite::SqliteStoreError> for ApplicationError {
fn from(e: presage_store_sqlite::SqliteStoreError) -> Self {
ApplicationError::Db(e)
}
}
impl From<libsignal_service::sender::AttachmentUploadError> 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,
}
}
}
-80
View File
@@ -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<imp::CallMessageItem>)
@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::<Self>()
.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<gtk::Image>,
#[property(get, set)]
message: RefCell<Option<CallMessage>>,
#[property(get, set, construct_only, type = Manager)]
manager: RefCell<Option<Manager>>,
}
#[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<Self>) {
obj.init_template();
}
}
#[glib::derived_properties]
impl ObjectImpl for CallMessageItem {}
impl WidgetImpl for CallMessageItem {}
impl BoxImpl for CallMessageItem {}
}
+3 -4
View File
@@ -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<DisplayMessage>,
message: Option<Message>,
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!(
"<span font-weight='500'>{}:</span> {}",
+1 -20
View File
@@ -36,29 +36,10 @@ pub mod imp {
#[template_child]
label_name: TemplateChild<gtk::Label>,
#[property(get, set = Self::set_channel)]
#[property(get, set)]
channel: RefCell<Option<Channel>>,
}
impl ChannelItemCompact {
fn set_channel(&self, chan: Option<Channel>) {
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} <b>{part2}</b>")
} else {
format!("<b>{part2}</b>")
};
self.label_name.set_markup(&format);
self.channel.replace(chan);
}
}
#[glib::object_subclass]
impl ObjectSubclass for ChannelItemCompact {
const NAME: &'static str = "FlChannelItemCompact";
+1 -1
View File
@@ -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},
};
+64 -66
View File
@@ -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<Option<Channel>>,
#[property(get, set, nullable)]
reply_message: RefCell<Option<TextMessage>>,
reply_message: RefCell<Option<Message>>,
#[property(get, set, default = true)]
sticky: Cell<bool>,
@@ -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::<TextMessage>);
self.obj().set_reply_message(None::<Message>);
}
#[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::<Message>);
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(&quote);
obj.set_reply_message(None::<TextMessage>);
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::<crate::gui::Window>()
.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::<MessageItem>()
{
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::<Option<TextMessage>>().expect(
"Type of signal `reply` of `ItemRow` to be `TextMessage`.",
);
let msg = args[1]
.get::<Option<Message>>()
.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::<TextMessage>);
s.obj().set_reply_message(None::<Message>);
if let Some(channel) = s.active_channel.borrow().as_ref() {
let draft = channel.property("draft");
s.text_entry.set_text(draft);
+27 -43
View File
@@ -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<gtk::Widget> {
if let Some(message) = item.dynamic_cast_ref::<TextMessage>() {
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::<TextMessage>()
.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::<CallMessage>() {
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<gtk::Widget> {
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::<Message>()
.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<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecObject::builder::<TimelineItem>("item")
glib::ParamSpecObject::builder::<Message>("item")
.write_only()
.build(),
]
@@ -113,8 +97,8 @@ mod imp {
match pspec.name() {
"item" => {
let v = value
.get::<Option<TimelineItem>>()
.expect("ItemRow to only get TimelineItem");
.get::<Option<Message>>()
.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<Vec<Signal>> = Lazy::new(|| -> Vec<Signal> {
vec![
Signal::builder("reply")
.param_types([TextMessage::static_type()])
.param_types([Message::static_type()])
.build(),
]
});
+3 -3
View File
@@ -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<gtk::Label>,
#[property(set = Self::set_item)]
item: PhantomData<TimelineItem>,
item: PhantomData<Message>,
}
impl TimeDivider {
fn set_item(&self, item: Option<TimelineItem>) {
fn set_item(&self, item: Option<Message>) {
let formatted = item
.and_then(|v| v.datetime())
.and_then(|d| Utility::format_date_human(&d));
+1 -1
View File
@@ -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
+35 -67
View File
@@ -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::<Self>()
.property("message", message)
.build();
let s = Object::builder::<Self>().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<TextMessage>| msg.map(|m| m.sender().is_self()))
.transform_to(|_, msg: Option<Message>| 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<MessageIndicators>,
#[property(get, set = Self::set_message, type = TextMessage)]
message: RefCell<Option<TextMessage>>,
#[property(get, set = Self::set_message, type = Message)]
message: RefCell<Option<Message>>,
#[property(get, set, nullable)]
pressed_attachment: RefCell<Option<Attachment>>,
#[property(get, set, default = true)]
@@ -381,8 +376,6 @@ pub mod imp {
#[property(get = Self::shows_media_loading)]
shows_media_loading: PhantomData<bool>,
#[property(get = Self::has_reaction)]
has_reaction: PhantomData<bool>,
}
#[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<TextMessage>) {
fn set_message(&self, msg: Option<Message>) {
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::<crate::gui::Window>()
.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<Vec<Signal>> = Lazy::new(|| -> Vec<Signal> {
vec![
Signal::builder("reply")
.param_types([TextMessage::static_type()])
.param_types([Message::static_type()])
.build(),
]
});
-1
View File
@@ -1,5 +1,4 @@
mod attachment;
mod call_message_item;
mod channel_info_dialog;
mod channel_item;
mod channel_item_compact;
+16 -11
View File
@@ -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::<Server>()
.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::<Server>()
.map(|s| s.server())
.unwrap_or(SignalServers::Production),
phone,
phone_number: phone,
captcha,
))
}))
.expect("Failed to send setup decision");
// XXX: Maybe display spinner afterwards?
}
+1 -1
View File
@@ -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 {}
+18 -14
View File
@@ -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<u8>)>::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<u8>) = 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>("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()
}
}
-1
View File
@@ -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};
-10
View File
@@ -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>) -> 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<chrono::Utc>) -> Option<glib::DateTime> {
glib::DateTime::from_unix_utc(chrono.timestamp())
.ok()