From e289867cb8eceadf220980eee4b7de22a8b82d07 Mon Sep 17 00:00:00 2001 From: Bnyro Date: Wed, 28 Jun 2023 11:42:20 +0200 Subject: [PATCH] Inbox: support for private messages --- Cargo.lock | 120 ++++++++++++++++++++++++-- Cargo.toml | 2 + README.md | 4 +- src/components/comment_row.rs | 1 - src/components/inbox_page.rs | 87 +++++++++++++++---- src/components/mention_row.rs | 3 +- src/components/mod.rs | 1 + src/components/private_message_row.rs | 108 +++++++++++++++++++++++ src/util.rs | 13 ++- 9 files changed, 304 insertions(+), 35 deletions(-) create mode 100644 src/components/private_message_row.rs diff --git a/Cargo.lock b/Cargo.lock index 6087ce3..621f47d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -183,7 +183,7 @@ dependencies = [ "serde_urlencoded", "smallvec", "socket2", - "time", + "time 0.3.22", "url", ] @@ -250,6 +250,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" version = "1.0.71" @@ -369,8 +378,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" dependencies = [ "android-tzdata", + "iana-time-zone", + "js-sys", "num-traits", "serde", + "time 0.1.45", + "wasm-bindgen", + "winapi", ] [[package]] @@ -748,7 +762,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -1102,6 +1116,29 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "iana-time-zone" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -1154,6 +1191,15 @@ version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f" +[[package]] +name = "isolang" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f80f221db1bc708b71128757b9396727c04de86968081e18e89b0575e03be071" +dependencies = [ + "phf 0.11.2", +] + [[package]] name = "itoa" version = "1.0.6" @@ -1255,6 +1301,7 @@ dependencies = [ name = "lemoa" version = "0.1.0" dependencies = [ + "chrono", "html2pango", "lemmy_api_common", "markdown", @@ -1265,6 +1312,7 @@ dependencies = [ "reqwest", "serde", "serde_json", + "timeago", ] [[package]] @@ -1385,7 +1433,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" dependencies = [ "log", - "phf", + "phf 0.10.1", "phf_codegen", "string_cache", "string_cache_codegen", @@ -1443,7 +1491,7 @@ checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.48.0", ] @@ -1616,7 +1664,16 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" dependencies = [ - "phf_shared", + "phf_shared 0.10.0", +] + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_shared 0.11.2", ] [[package]] @@ -1626,7 +1683,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" dependencies = [ "phf_generator", - "phf_shared", + "phf_shared 0.10.0", ] [[package]] @@ -1635,7 +1692,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" dependencies = [ - "phf_shared", + "phf_shared 0.10.0", "rand", ] @@ -1648,6 +1705,15 @@ dependencies = [ "siphasher", ] +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "1.1.0" @@ -2156,7 +2222,7 @@ dependencies = [ "new_debug_unreachable", "once_cell", "parking_lot", - "phf_shared", + "phf_shared 0.10.0", "precomputed-hash", "serde", ] @@ -2168,7 +2234,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" dependencies = [ "phf_generator", - "phf_shared", + "phf_shared 0.10.0", "proc-macro2", "quote", ] @@ -2284,6 +2350,17 @@ dependencies = [ "syn 2.0.18", ] +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + [[package]] name = "time" version = "0.3.22" @@ -2311,6 +2388,16 @@ dependencies = [ "time-core", ] +[[package]] +name = "timeago" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5082dc942361cdfb74eab98bf995762d6015e5bb3a20bf7c5c71213778b4fcb4" +dependencies = [ + "chrono", + "isolang", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -2586,6 +2673,12 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -2709,6 +2802,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.42.0" diff --git a/Cargo.toml b/Cargo.toml index f14d4de..ea6b997 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,3 +14,5 @@ markdown = "0.3.0" html2pango = "0.5.0" rand = "0.8.5" mime_guess = "2.0.4" +chrono = "0.4.26" +timeago = "0.4.1" diff --git a/README.md b/README.md index 638d055..7484b19 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,6 @@ Working: - Voting for or against posts or comments - Editing and deleting posts or comments - Viewing the personal inbox (mentions, replies) - -Not yet supported: - - Private messages # Build dependencies @@ -61,6 +58,7 @@ sudo docker cp $(CONTAINER_ID):/root/lemoa/target/release/lemoa . Once the build is done, there will be an executable `lemoa` binary file in your current directory, executing it starts Lemoa :tada:. # Building with meson + ``` meson _build ninja -C _build diff --git a/src/components/comment_row.rs b/src/components/comment_row.rs index 386a4b5..73d93cc 100644 --- a/src/components/comment_row.rs +++ b/src/components/comment_row.rs @@ -77,7 +77,6 @@ impl FactoryComponent for CommentRow { set_markup: &markdown_to_pango_markup(self.comment.comment.content.clone()), set_halign: gtk::Align::Start, set_use_markup: true, - set_selectable: true, }, gtk::Box { diff --git a/src/components/inbox_page.rs b/src/components/inbox_page.rs index 8392d3f..ecd2924 100644 --- a/src/components/inbox_page.rs +++ b/src/components/inbox_page.rs @@ -1,19 +1,23 @@ use gtk::prelude::*; -use lemmy_api_common::lemmy_db_views_actor::structs::CommentReplyView; +use lemmy_api_common::{ + lemmy_db_views::structs::PrivateMessageView, lemmy_db_views_actor::structs::CommentReplyView, +}; use relm4::{factory::FactoryVecDeque, prelude::*}; use crate::api; -use super::mention_row::MentionRow; +use super::{mention_row::MentionRow, private_message_row::PrivateMessageRow}; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum InboxType { Replies, Mentions, + PrivateMessages, } pub struct InboxPage { mentions: FactoryVecDeque, + private_messages: FactoryVecDeque, page: i64, unread_only: bool, type_: InboxType, @@ -25,6 +29,7 @@ pub enum InboxInput { ToggleUnreadState, FetchInbox, UpdateInbox(Vec), + UpdatePrivateMessages(Vec), MarkAllAsRead, } @@ -41,32 +46,57 @@ impl SimpleComponent for InboxPage { set_orientation: gtk::Orientation::Horizontal, set_margin_all: 15, set_spacing: 10, - gtk::Button { + gtk::ToggleButton { set_label: "Replies", connect_clicked => InboxInput::UpdateType(InboxType::Replies), + #[watch] + set_active: model.type_ == InboxType::Replies, }, - gtk::Button { + gtk::ToggleButton { set_label: "Mentions", connect_clicked => InboxInput::UpdateType(InboxType::Mentions), + #[watch] + set_active: model.type_ == InboxType::Mentions, + }, + gtk::ToggleButton { + set_label: "Private messages", + connect_clicked => InboxInput::UpdateType(InboxType::PrivateMessages), + #[watch] + set_active: model.type_ == InboxType::PrivateMessages, + }, + gtk::Box { + set_hexpand: true, }, gtk::ToggleButton { set_active: false, set_label: "Show unread only", connect_clicked => InboxInput::ToggleUnreadState, }, - gtk::Box { - set_hexpand: true, - }, gtk::Button { set_label: "Mark all as read", connect_clicked => InboxInput::MarkAllAsRead, } }, gtk::ScrolledWindow { - #[local_ref] - mentions -> gtk::Box { - set_vexpand: true, - set_orientation: gtk::Orientation::Vertical, + match model.type_ { + InboxType::PrivateMessages => { + gtk::Box { + #[local_ref] + private_messages -> gtk::Box { + set_vexpand: true, + set_orientation: gtk::Orientation::Vertical, + } + } + } + _ => { + gtk::Box { + #[local_ref] + mentions -> gtk::Box { + set_vexpand: true, + set_orientation: gtk::Orientation::Vertical, + } + } + } } } } @@ -78,13 +108,16 @@ impl SimpleComponent for InboxPage { sender: ComponentSender, ) -> ComponentParts { let mentions = FactoryVecDeque::new(gtk::Box::default(), sender.output_sender()); + let private_messages = FactoryVecDeque::new(gtk::Box::default(), sender.output_sender()); let model = Self { mentions, + private_messages, page: 1, unread_only: false, type_: InboxType::Replies, }; let mentions = model.mentions.widget(); + let private_messages = model.private_messages.widget(); let widgets = view_output!(); ComponentParts { model, widgets } } @@ -96,26 +129,40 @@ impl SimpleComponent for InboxPage { let page = self.page.clone(); let unread_only = self.unread_only.clone(); std::thread::spawn(move || { - let comments = match type_ { + let message = match type_ { InboxType::Mentions => { if let Ok(response) = api::user::get_mentions(page, unread_only) { // It's just a different object, but its contents are exactly the same let serialised = serde_json::to_string(&response.mentions).unwrap(); - serde_json::from_str(&serialised).ok() + let mentions = serde_json::from_str(&serialised).ok(); + if let Some(mentions) = mentions { + Some(InboxInput::UpdateInbox(mentions)) + } else { + None + } } else { None } } InboxType::Replies => { if let Ok(response) = api::user::get_replies(page, unread_only) { - Some(response.replies) + Some(InboxInput::UpdateInbox(response.replies)) + } else { + None + } + } + InboxType::PrivateMessages => { + if let Ok(response) = + api::private_message::list_private_messages(unread_only, page) + { + Some(InboxInput::UpdatePrivateMessages(response.private_messages)) } else { None } } }; - if let Some(comments) = comments { - sender.input(InboxInput::UpdateInbox(comments)) + if let Some(message) = message { + sender.input(message) }; }); } @@ -133,6 +180,12 @@ impl SimpleComponent for InboxPage { self.mentions.guard().push_back(comment); } } + InboxInput::UpdatePrivateMessages(messages) => { + self.private_messages.guard().clear(); + for message in messages { + self.private_messages.guard().push_back(message); + } + } InboxInput::MarkAllAsRead => { let show_unread_only = self.unread_only.clone(); std::thread::spawn(move || { diff --git a/src/components/mention_row.rs b/src/components/mention_row.rs index e6e2e62..0839ba6 100644 --- a/src/components/mention_row.rs +++ b/src/components/mention_row.rs @@ -30,7 +30,7 @@ impl FactoryComponent for MentionRow { type Input = MentionRowMsg; type Output = crate::AppMsg; type CommandOutput = (); - type Widgets = PostViewWidgets; + type Widgets = MentionRowWidgets; type ParentInput = crate::AppMsg; type ParentWidget = gtk::Box; @@ -104,7 +104,6 @@ impl FactoryComponent for MentionRow { set_markup: &markdown_to_pango_markup(self.comment.comment.content.clone()), set_halign: gtk::Align::Start, set_use_markup: true, - set_selectable: true, }, #[local_ref] diff --git a/src/components/mod.rs b/src/components/mod.rs index 395e425..3db4823 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -5,5 +5,6 @@ pub mod inbox_page; pub mod mention_row; pub mod post_page; pub mod post_row; +pub mod private_message_row; pub mod profile_page; pub mod voting_row; diff --git a/src/components/private_message_row.rs b/src/components/private_message_row.rs new file mode 100644 index 0000000..f2897f0 --- /dev/null +++ b/src/components/private_message_row.rs @@ -0,0 +1,108 @@ +use gtk::prelude::*; +use lemmy_api_common::lemmy_db_views::structs::PrivateMessageView; +use relm4::prelude::FactoryComponent; +use relm4::prelude::*; +use relm4_components::web_image::WebImage; + +use crate::util::{self, get_web_image_url, markdown_to_pango_markup}; + +pub struct PrivateMessageRow { + message: PrivateMessageView, + creator_image: Controller, +} + +#[derive(Debug)] +pub enum PrivateMessageRowInput { + OpenPerson, +} + +#[relm4::factory(pub)] +impl FactoryComponent for PrivateMessageRow { + type Init = PrivateMessageView; + type Input = PrivateMessageRowInput; + type Output = crate::AppMsg; + type CommandOutput = (); + type Widgets = PrivateMessageRowWidgets; + type ParentInput = crate::AppMsg; + type ParentWidget = gtk::Box; + + view! { + root = gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_spacing: 10, + set_margin_end: 10, + set_margin_start: 10, + set_margin_top: 10, + set_vexpand: false, + + gtk::Box { + set_orientation: gtk::Orientation::Horizontal, + set_spacing: 10, + set_vexpand: false, + set_hexpand: true, + + if self.message.creator.avatar.is_some() { + gtk::Box { + set_hexpand: false, + #[local_ref] + creator_image -> gtk::Box {} + } + } else { + gtk::Box {} + }, + + gtk::Button { + set_label: &self.message.creator.name, + connect_clicked => PrivateMessageRowInput::OpenPerson, + }, + + gtk::Label { + set_margin_start: 10, + set_label: &util::format_elapsed_time(self.message.private_message.published) + } + }, + + gtk::Label { + #[watch] + set_markup: &markdown_to_pango_markup(self.message.private_message.content.clone()), + set_halign: gtk::Align::Start, + set_use_markup: true, + }, + + gtk::Separator {} + } + } + + fn init_model(init: Self::Init, _index: &Self::Index, _sender: FactorySender) -> Self { + let creator_image = WebImage::builder() + .launch(get_web_image_url(init.creator.avatar.clone())) + .detach(); + Self { + message: init, + creator_image, + } + } + fn init_widgets( + &mut self, + _index: &Self::Index, + root: &Self::Root, + _returned_widget: &::ReturnedWidget, + sender: FactorySender, + ) -> Self::Widgets { + let creator_image = self.creator_image.widget(); + let widgets = view_output!(); + widgets + } + + fn forward_to_parent(output: Self::Output) -> Option { + Some(output) + } + + fn update(&mut self, message: Self::Input, sender: FactorySender) { + match message { + PrivateMessageRowInput::OpenPerson => { + sender.output(crate::AppMsg::OpenPerson(self.message.creator.id)) + } + } + } +} diff --git a/src/util.rs b/src/util.rs index c24f653..74d4376 100644 --- a/src/util.rs +++ b/src/util.rs @@ -10,13 +10,20 @@ pub fn get_web_image_msg(url: Option) -> WebImageMsg { } pub fn get_web_image_url(url: Option) -> String { - return if let Some(url) = url { + if let Some(url) = url { url.to_string() } else { - String::from("") - }; + "".to_string() + } } pub fn markdown_to_pango_markup(text: String) -> String { return html2pango::markup_html(&markdown::to_html(&text)).unwrap_or(text); } + +pub fn format_elapsed_time(time: chrono::NaiveDateTime) -> String { + let formatter = timeago::Formatter::new(); + let current_time = chrono::Utc::now(); + let published = time.and_utc(); + formatter.convert_chrono(published, current_time) +}