Inbox: support for private messages

This commit is contained in:
Bnyro 2023-06-28 11:42:20 +02:00
parent e0bdc593fb
commit e289867cb8
9 changed files with 304 additions and 35 deletions

120
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -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

View File

@ -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 {

View File

@ -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<MentionRow>,
private_messages: FactoryVecDeque<PrivateMessageRow>,
page: i64,
unread_only: bool,
type_: InboxType,
@ -25,6 +29,7 @@ pub enum InboxInput {
ToggleUnreadState,
FetchInbox,
UpdateInbox(Vec<CommentReplyView>),
UpdatePrivateMessages(Vec<PrivateMessageView>),
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<Self>,
) -> ComponentParts<Self> {
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 || {

View File

@ -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]

View File

@ -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;

View File

@ -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<WebImage>,
}
#[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>) -> 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: &<Self::ParentWidget as relm4::factory::FactoryView>::ReturnedWidget,
sender: FactorySender<Self>,
) -> Self::Widgets {
let creator_image = self.creator_image.widget();
let widgets = view_output!();
widgets
}
fn forward_to_parent(output: Self::Output) -> Option<Self::ParentInput> {
Some(output)
}
fn update(&mut self, message: Self::Input, sender: FactorySender<Self>) {
match message {
PrivateMessageRowInput::OpenPerson => {
sender.output(crate::AppMsg::OpenPerson(self.message.creator.id))
}
}
}
}

View File

@ -10,13 +10,20 @@ pub fn get_web_image_msg(url: Option<DbUrl>) -> WebImageMsg {
}
pub fn get_web_image_url(url: Option<DbUrl>) -> 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)
}