From 1ebfa7776f78b46cea153c91af9ecd57b3d4655f Mon Sep 17 00:00:00 2001 From: Bnyro Date: Thu, 22 Jun 2023 09:04:15 +0200 Subject: [PATCH] Add support for viewing the inbox (mentions, replies) --- src/api/user.rs | 33 +++++++- src/components/inbox_page.rs | 120 +++++++++++++++++++++++++++ src/components/mention_row.rs | 150 ++++++++++++++++++++++++++++++++++ src/components/mod.rs | 2 + src/main.rs | 28 +++++-- 5 files changed, 326 insertions(+), 7 deletions(-) create mode 100644 src/components/inbox_page.rs create mode 100644 src/components/mention_row.rs diff --git a/src/api/user.rs b/src/api/user.rs index 59f86f3..f21fc9a 100644 --- a/src/api/user.rs +++ b/src/api/user.rs @@ -1,4 +1,4 @@ -use lemmy_api_common::{person::{GetPersonDetailsResponse, GetPersonDetails}}; +use lemmy_api_common::{person::{GetPersonDetailsResponse, GetPersonDetails, GetPersonMentionsResponse, GetRepliesResponse, MarkAllAsRead, GetReplies, GetPersonMentions}, lemmy_db_schema::CommentSortType}; use crate::settings; @@ -15,4 +15,33 @@ pub fn get_user(username: String, page: i64) -> std::result::Result GetPersonDetailsResponse { serde_json::from_str(include_str!("../examples/person.json")).unwrap() -} \ No newline at end of file +} + +pub fn get_mentions(page: i64, unread_only: bool) -> std::result::Result { + let params = GetPersonMentions { + auth: settings::get_current_account().jwt.unwrap(), + unread_only: Some(unread_only), + page: Some(page), + sort: Some(CommentSortType::New), + ..Default::default() + }; + super::get("/user/mentions", ¶ms) +} + +pub fn get_replies(page: i64, unread_only: bool) -> std::result::Result { + let params = GetReplies { + auth: settings::get_current_account().jwt.unwrap(), + page: Some(page), + unread_only: Some(unread_only), + sort: Some(CommentSortType::New), + ..Default::default() + }; + super::get("/user/replies", ¶ms) +} + +pub fn mark_all_as_read() -> std::result::Result { + let params = MarkAllAsRead { + auth: settings::get_current_account().jwt.unwrap(), + }; + super::post("/user/mark_all_as_read", ¶ms) +} diff --git a/src/components/inbox_page.rs b/src/components/inbox_page.rs new file mode 100644 index 0000000..4c129e8 --- /dev/null +++ b/src/components/inbox_page.rs @@ -0,0 +1,120 @@ +use lemmy_api_common::lemmy_db_views_actor::structs::CommentReplyView; +use relm4::{prelude::*, factory::FactoryVecDeque}; +use gtk::prelude::*; + +use crate::api; + +use super::mention_row::MentionRow; + +#[derive(Debug, Clone)] +pub enum InboxType { + Mentions, + Replies, +} + +pub struct InboxPage { + mentions: FactoryVecDeque, + page: i64, + unread_only: bool, + type_: InboxType +} + +#[derive(Debug)] +pub enum InboxInput { + UpdateType(InboxType), + ToggleUnreadState, + FetchInbox, + UpdateInbox(Vec) +} + +#[relm4::component(pub)] +impl SimpleComponent for InboxPage { + type Init = (); + type Input = InboxInput; + type Output = crate::AppMsg; + + view! { + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + gtk::Box { + set_orientation: gtk::Orientation::Horizontal, + set_margin_all: 15, + set_spacing: 10, + gtk::Button { + set_label: "Mentions", + connect_clicked => InboxInput::UpdateType(InboxType::Mentions), + }, + gtk::Button { + set_label: "Replies", + connect_clicked => InboxInput::UpdateType(InboxType::Replies), + }, + gtk::ToggleButton { + set_active: false, + set_label: "Show unread only", + connect_clicked => InboxInput::ToggleUnreadState, + }, + }, + gtk::ScrolledWindow { + #[local_ref] + mentions -> gtk::Box { + set_vexpand: true, + set_orientation: gtk::Orientation::Vertical, + } + } + } + } + + fn init( + _init: Self::Init, + root: &Self::Root, + sender: ComponentSender, + ) -> ComponentParts { + let mentions = FactoryVecDeque::new(gtk::Box::default(), sender.output_sender()); + let model = Self { mentions, page: 1, unread_only: false, type_: InboxType::Mentions }; + let mentions = model.mentions.widget(); + let widgets = view_output!(); + sender.input(InboxInput::FetchInbox); + ComponentParts { model, widgets } + } + + fn update(&mut self, message: Self::Input, sender: ComponentSender) { + match message { + InboxInput::FetchInbox => { + let type_ = self.type_.clone(); + let page = self.page.clone(); + let unread_only = self.unread_only.clone(); + std::thread::spawn(move || { + let comments = match type_ { + InboxType::Mentions => { + if let Ok(response) = api::user::get_mentions(page, unread_only) { + let serialised = serde_json::to_string(&response.mentions).unwrap(); + serde_json::from_str(&serialised).ok() + } else { None } + } + InboxType::Replies => { + if let Ok(response) = api::user::get_replies(page, unread_only) { + Some(response.replies) + } else { None } + } + }; + if let Some(comments) = comments { + sender.input(InboxInput::UpdateInbox(comments)) + }; + }); + } + InboxInput::UpdateType(type_) => { + self.type_ = type_; + sender.input(InboxInput::FetchInbox); + } + InboxInput::ToggleUnreadState => { + self.unread_only = !self.unread_only; + sender.input(InboxInput::FetchInbox); + } + InboxInput::UpdateInbox(comments) => { + for comment in comments { + self.mentions.guard().push_back(comment); + } + } + } + } +} diff --git a/src/components/mention_row.rs b/src/components/mention_row.rs new file mode 100644 index 0000000..03d582c --- /dev/null +++ b/src/components/mention_row.rs @@ -0,0 +1,150 @@ +use lemmy_api_common::lemmy_db_views_actor::structs::CommentReplyView; +use relm4::prelude::*; +use gtk::prelude::*; +use relm4_components::web_image::WebImage; + +use crate::util::get_web_image_url; +use crate::util::markdown_to_pango_markup; + +use super::voting_row::VotingRowModel; +use super::voting_row::VotingStats; + +#[derive(Debug)] +pub struct MentionRow { + comment: CommentReplyView, + creator_image: Controller, + community_image: Controller, + voting_row: Controller +} + +#[derive(Debug)] +pub enum MentionRowMsg { + OpenPerson, + OpenPost, + OpenCommunity, +} + +#[relm4::factory(pub)] +impl FactoryComponent for MentionRow { + type Init = CommentReplyView; + type Input = MentionRowMsg; + type Output = crate::AppMsg; + type CommandOutput = (); + type Widgets = PostViewWidgets; + 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, + + gtk::Label { + set_label: &self.comment.post.name, + add_css_class: "font-bold", + set_halign: gtk::Align::Start, + }, + + gtk::Box { + set_orientation: gtk::Orientation::Horizontal, + gtk::Label { + set_label: "in", + }, + if self.comment.community.icon.clone().is_some() { + gtk::Box { + set_hexpand: false, + set_margin_start: 10, + set_margin_end: 10, + #[local_ref] + community_image -> gtk::Box {} + } + } else { + gtk::Box {} + }, + + gtk::Button { + set_label: &self.comment.community.title, + connect_clicked => MentionRowMsg::OpenCommunity, + }, + }, + + gtk::Box { + set_orientation: gtk::Orientation::Horizontal, + set_spacing: 10, + set_margin_top: 10, + set_vexpand: false, + + if self.comment.creator.avatar.is_some() { + gtk::Box { + set_hexpand: false, + #[local_ref] + creator_image -> gtk::Box {} + } + } else { + gtk::Box {} + }, + + gtk::Button { + set_label: &self.comment.creator.name, + connect_clicked => MentionRowMsg::OpenPerson, + }, + }, + + gtk::Label { + #[watch] + 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] + voting_row -> gtk::Box {}, + + gtk::Separator {} + } + } + + fn forward_to_parent(output: Self::Output) -> Option { + Some(output) + } + + fn init_model(value: Self::Init, _index: &DynamicIndex, _sender: FactorySender) -> Self { + let creator_image = WebImage::builder().launch(get_web_image_url(value.creator.avatar.clone())).detach(); + let community_image = WebImage::builder().launch(get_web_image_url(value.community.icon.clone())).detach(); + let voting_row = VotingRowModel::builder().launch(VotingStats::from_comment(value.counts.clone(), value.my_vote)).detach(); + + Self { comment: value, creator_image, community_image, voting_row } + } + + 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 community_image = self.community_image.widget(); + let voting_row = self.voting_row.widget(); + let widgets = view_output!(); + widgets + } + + fn update(&mut self, message: Self::Input, sender: FactorySender) { + match message { + MentionRowMsg::OpenPerson => { + sender.output(crate::AppMsg::OpenPerson(self.comment.creator.name.clone())); + } + MentionRowMsg::OpenPost => { + sender.output(crate::AppMsg::OpenPost(self.comment.post.id.clone())); + } + MentionRowMsg::OpenCommunity => { + sender.output(crate::AppMsg::OpenCommunity(self.comment.community.name.clone())); + } + } + } +} \ No newline at end of file diff --git a/src/components/mod.rs b/src/components/mod.rs index d8e61a0..5e2446b 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -5,3 +5,5 @@ pub mod community_page; pub mod post_page; pub mod comment_row; pub mod voting_row; +pub mod inbox_page; +pub mod mention_row; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 4de5897..7ad3b27 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,7 @@ pub mod util; pub mod dialogs; use api::{user::default_person, community::default_community, post::default_post}; -use components::{post_row::PostRow, community_row::CommunityRow, profile_page::{ProfilePage, self}, community_page::{CommunityPage, self}, post_page::{PostPage, self}}; +use components::{post_row::PostRow, community_row::CommunityRow, profile_page::{ProfilePage, self}, community_page::{CommunityPage, self}, post_page::{PostPage, self}, inbox_page::InboxPage}; use gtk::prelude::*; use lemmy_api_common::{lemmy_db_views_actor::structs::CommunityView, lemmy_db_views::structs::PostView, person::GetPersonDetailsResponse, lemmy_db_schema::{newtypes::PostId, ListingType}, post::GetPostResponse, community::GetCommunityResponse}; use relm4::{prelude::*, factory::FactoryVecDeque, set_global_css, actions::{RelmAction, RelmActionGroup}}; @@ -22,7 +22,8 @@ enum AppState { Person, Post, Login, - Message + Message, + Inbox } struct App { @@ -33,7 +34,8 @@ struct App { communities: FactoryVecDeque, profile_page: Controller, community_page: Controller, - post_page: Controller + post_page: Controller, + inbox_page: Controller } #[derive(Debug, Clone)] @@ -54,7 +56,8 @@ pub enum AppMsg { OpenPerson(String), DoneFetchPerson(GetPersonDetailsResponse), OpenPost(PostId), - DoneFetchPost(GetPostResponse) + DoneFetchPost(GetPostResponse), + OpenInbox } #[relm4::component] @@ -83,6 +86,10 @@ impl SimpleComponent for App { set_label: "Subscribed", connect_clicked => AppMsg::StartFetchPosts(Some(ListingType::Subscribed)), }, + pack_start = >k::Button { + set_label: "Inbox", + connect_clicked => AppMsg::OpenInbox, + }, pack_start = >k::Button { set_label: "Communities", connect_clicked => AppMsg::ViewCommunities(None), @@ -247,6 +254,12 @@ impl SimpleComponent for App { } } } + AppState::Inbox => { + gtk::ScrolledWindow { + #[local_ref] + inbox_page -> gtk::Box {} + } + } } } } @@ -275,8 +288,9 @@ impl SimpleComponent for App { let profile_page = ProfilePage::builder().launch(default_person()).forward(sender.input_sender(), |msg| msg); let community_page = CommunityPage::builder().launch(default_community().community_view).forward(sender.input_sender(), |msg| msg); let post_page = PostPage::builder().launch(default_post()).forward(sender.input_sender(), |msg| msg); + let inbox_page = InboxPage::builder().launch(()).forward(sender.input_sender(), |msg| msg); - let model = App { state, posts, communities, profile_page, community_page, post_page, message: None, latest_action: None }; + let model = App { state, posts, communities, profile_page, community_page, post_page, inbox_page, message: None, latest_action: None }; // fetch posts if that's the initial page if !instance_url.is_empty() { sender.input(AppMsg::StartFetchPosts(None)) }; @@ -287,6 +301,7 @@ impl SimpleComponent for App { let profile_page = model.profile_page.widget(); let community_page = model.community_page.widget(); let post_page = model.post_page.widget(); + let inbox_page = model.inbox_page.widget(); let widgets = view_output!(); @@ -447,6 +462,9 @@ impl SimpleComponent for App { AppMsg::Retry => { sender.input(self.latest_action.clone().unwrap_or(AppMsg::StartFetchPosts(None))); } + AppMsg::OpenInbox => { + self.state = AppState::Inbox; + } } } }