From 1e984b07655dcac11f29bffc8e78977ad66b3cc0 Mon Sep 17 00:00:00 2001 From: Bnyro Date: Tue, 20 Jun 2023 17:13:14 +0200 Subject: [PATCH] (Down-)voting posts and comments --- src/components/comment_row.rs | 16 ++-- src/components/community_page.rs | 2 +- src/components/mod.rs | 1 + src/components/post_page.rs | 16 +++- src/components/post_row.rs | 20 ++++- src/components/voting_row.rs | 142 +++++++++++++++++++++++++++++++ 6 files changed, 182 insertions(+), 15 deletions(-) create mode 100644 src/components/voting_row.rs diff --git a/src/components/comment_row.rs b/src/components/comment_row.rs index b8ae8f8..b6933b7 100644 --- a/src/components/comment_row.rs +++ b/src/components/comment_row.rs @@ -6,10 +6,14 @@ 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 CommentRow { comment: CommentView, - avatar: Controller + avatar: Controller, + voting_row: Controller } #[derive(Debug)] @@ -62,10 +66,8 @@ impl FactoryComponent for CommentRow { set_use_markup: true, }, - gtk::Label { - set_label: &format!("{} score", self.comment.counts.score), - set_halign: gtk::Align::Start, - }, + #[local_ref] + voting_row -> gtk::Box {}, gtk::Separator {} } @@ -77,8 +79,9 @@ impl FactoryComponent for CommentRow { fn init_model(value: Self::Init, _index: &DynamicIndex, _sender: FactorySender) -> Self { let avatar = WebImage::builder().launch(get_web_image_url(value.community.clone().icon)).detach(); + let voting_row = VotingRowModel::builder().launch(VotingStats::from_comment(value.clone().counts, value.my_vote)).detach(); - Self { comment: value, avatar } + Self { comment: value, avatar, voting_row } } fn init_widgets( @@ -89,6 +92,7 @@ impl FactoryComponent for CommentRow { sender: FactorySender, ) -> Self::Widgets { let community_image = self.avatar.widget(); + let voting_row = self.voting_row.widget(); let widgets = view_output!(); widgets } diff --git a/src/components/community_page.rs b/src/components/community_page.rs index 13b5003..816dcdf 100644 --- a/src/components/community_page.rs +++ b/src/components/community_page.rs @@ -167,7 +167,7 @@ impl SimpleComponent for CommunityPage { Ok(post) => Some(CommunityInput::CreatedPost(post.post_view)), Err(err) => { println!("{}", err.to_string()); None } }; - if message.is_some() { sender.input(message.unwrap()) }; + if let Some(message) = message { sender.input(message) }; }); } CommunityInput::ToggleSubscription => { diff --git a/src/components/mod.rs b/src/components/mod.rs index df591af..d8e61a0 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -4,3 +4,4 @@ pub mod profile_page; pub mod community_page; pub mod post_page; pub mod comment_row; +pub mod voting_row; diff --git a/src/components/post_page.rs b/src/components/post_page.rs index 0e81889..009f947 100644 --- a/src/components/post_page.rs +++ b/src/components/post_page.rs @@ -5,7 +5,7 @@ use relm4_components::web_image::WebImage; use crate::{api, util::{get_web_image_msg, get_web_image_url, markdown_to_pango_markup}, dialogs::create_post::{CreatePostDialog, CreatePostDialogOutput, DialogMsg, CREATE_COMMENT_DIALOG_BROKER, DialogType}}; -use super::comment_row::CommentRow; +use super::{comment_row::CommentRow, voting_row::{VotingRowModel, VotingStats, VotingRowInput}}; pub struct PostPage { info: GetPostResponse, @@ -14,7 +14,8 @@ pub struct PostPage { community_avatar: Controller, comments: FactoryVecDeque, #[allow(dead_code)] - create_comment_dialog: Controller + create_comment_dialog: Controller, + voting_row: Controller } #[derive(Debug)] @@ -122,9 +123,13 @@ impl SimpleComponent for PostPage { set_margin_bottom: 10, set_halign: gtk::Align::Center, + #[local_ref] + voting_row -> gtk::Box { + set_margin_end: 10, + }, gtk::Label { #[watch] - set_text: &format!("{} comments, {} score", model.info.post_view.counts.comments, model.info.post_view.counts.score), + set_text: &format!("{} comments", model.info.post_view.counts.comments), }, gtk::Button { set_label: "Comment", @@ -158,12 +163,14 @@ impl SimpleComponent for PostPage { .forward(sender.input_sender(), |msg| match msg { CreatePostDialogOutput::CreateRequest(_name, body) => PostInput::CreateCommentRequest(body) }); - let model = PostPage { info: init, image, comments, creator_avatar, community_avatar, create_comment_dialog: dialog, }; + let voting_row = VotingRowModel::builder().launch(VotingStats::default()).detach(); + let model = PostPage { info: init, image, comments, creator_avatar, community_avatar, create_comment_dialog: dialog, voting_row }; let image = model.image.widget(); let comments = model.comments.widget(); let creator_avatar = model.creator_avatar.widget(); let community_avatar = model.community_avatar.widget(); + let voting_row = model.voting_row.widget(); let widgets = view_output!(); ComponentParts { model, widgets } @@ -178,6 +185,7 @@ impl SimpleComponent for PostPage { self.community_avatar.emit(get_web_image_msg(post.community_view.community.icon)); self.creator_avatar.emit(get_web_image_msg(post.post_view.creator.avatar)); + self.voting_row.emit(VotingRowInput::UpdateStats(VotingStats::from_post(post.post_view.counts.clone(), post.post_view.my_vote))); self.comments.guard().clear(); std::thread::spawn(move || { diff --git a/src/components/post_row.rs b/src/components/post_row.rs index 495b1ab..be07922 100644 --- a/src/components/post_row.rs +++ b/src/components/post_row.rs @@ -5,11 +5,14 @@ use relm4_components::web_image::WebImage; use crate::util::get_web_image_url; +use super::voting_row::{VotingRowModel, VotingStats}; + #[derive(Debug)] pub struct PostRow { post: PostView, author_image: Controller, community_image: Controller, + voting_row: Controller } #[derive(Debug)] @@ -91,9 +94,16 @@ impl FactoryComponent for PostRow { add_css_class: "font-bold", }, - gtk::Label { - set_halign: gtk::Align::Start, - set_text: &format!("{} score, {} comments", self.post.counts.score, self.post.clone().counts.comments), + gtk::Box { + set_orientation: gtk::Orientation::Horizontal, + #[local_ref] + voting_row -> gtk::Box { + set_margin_end: 10, + }, + gtk::Label { + set_halign: gtk::Align::Start, + set_text: &format!("{} comments", self.post.clone().counts.comments), + }, }, gtk::Separator { @@ -107,8 +117,9 @@ impl FactoryComponent for PostRow { fn init_model(value: Self::Init, _index: &DynamicIndex, _sender: FactorySender) -> Self { let author_image= WebImage::builder().launch(get_web_image_url(value.creator.clone().avatar)).detach(); let community_image= WebImage::builder().launch(get_web_image_url(value.creator.clone().avatar)).detach(); + let voting_row = VotingRowModel::builder().launch(VotingStats::from_post(value.clone().counts, value.my_vote)).detach(); - Self { post: value, author_image, community_image } + Self { post: value, author_image, community_image, voting_row } } fn init_widgets( @@ -120,6 +131,7 @@ impl FactoryComponent for PostRow { ) -> Self::Widgets { let author_image = self.author_image.widget(); let community_image = self.community_image.widget(); + let voting_row = self.voting_row.widget(); let widgets = view_output!(); widgets } diff --git a/src/components/voting_row.rs b/src/components/voting_row.rs new file mode 100644 index 0000000..13ae500 --- /dev/null +++ b/src/components/voting_row.rs @@ -0,0 +1,142 @@ +use lemmy_api_common::{lemmy_db_schema::{aggregates::structs::{PostAggregates, CommentAggregates}, newtypes::{PostId, CommentId}}}; +use relm4::{SimpleComponent, ComponentParts, gtk}; +use gtk::prelude::*; + +use crate::api; + +#[derive(Default, Debug, Clone)] +pub struct VotingStats { + #[allow(dead_code)] + upvotes: i64, + #[allow(dead_code)] + downvotes: i64, + score: i64, + own_vote: Option, + #[allow(dead_code)] + id: i32, + post_id: Option, + comment_id: Option +} + +impl VotingStats { + pub fn from_post(counts: PostAggregates, my_vote: Option) -> Self { + Self { + upvotes: counts.upvotes, + downvotes: counts.downvotes, + own_vote: my_vote, + post_id: Some(counts.post_id.0), + id: counts.id, + score: counts.score, + comment_id: None, + } + } + + pub fn from_comment(counts: CommentAggregates, my_vote: Option) -> Self { + Self { + upvotes: counts.upvotes, + downvotes: counts.downvotes, + own_vote: my_vote, + post_id: None, + id: counts.id, + score: counts.score, + comment_id: Some(counts.comment_id.0), + } + } +} + +#[derive(Debug)] +pub struct VotingRowModel { + stats: VotingStats +} + +#[derive(Debug)] +pub enum VotingRowInput { + UpdateStats(VotingStats), + Vote(i16), +} + +#[derive(Debug)] +pub enum VotingRowOutput { + +} + +#[relm4::component(pub)] +impl SimpleComponent for VotingRowModel { + type Input = VotingRowInput; + type Output = VotingRowOutput; + type Init = VotingStats; + + view! { + gtk::Box { + set_orientation: gtk::Orientation::Horizontal, + + if model.stats.own_vote == Some(1) { + gtk::Button { + set_icon_name: "go-up", + connect_clicked => VotingRowInput::Vote(0), + add_css_class: "suggested-action", + } + } else { + gtk::Button { + set_icon_name: "go-up", + connect_clicked => VotingRowInput::Vote(1) + } + }, + gtk::Label { + #[watch] + set_label: &format!("{}", model.stats.score), + set_margin_start: 10, + set_margin_end: 10, + }, + if model.stats.own_vote == Some(-1) { + gtk::Button { + set_icon_name: "go-down", + connect_clicked => VotingRowInput::Vote(0), + add_css_class: "suggested-action", + } + } else { + gtk::Button { + set_icon_name: "go-down", + connect_clicked => VotingRowInput::Vote(-1) + } + }, + } + } + + fn init( + init: Self::Init, + root: &Self::Root, + _sender: relm4::ComponentSender, + ) -> relm4::ComponentParts { + let model = VotingRowModel { stats: init }; + let widgets = view_output!(); + ComponentParts { widgets, model } + } + + fn update(&mut self, message: Self::Input, sender: relm4::ComponentSender) { + match message { + VotingRowInput::Vote(score) => { + let stats = self.stats.clone(); + std::thread::spawn(move || { + let info = if stats.post_id.is_some() { + let response = api::post::like_post(PostId { 0: stats.post_id.unwrap() }, score); + match response { + Ok(post) => Some(VotingStats::from_post(post.post_view.counts, post.post_view.my_vote)), + Err(err) => { println!("{}", err.to_string()); None } + } + } else { + let response = api::comment::like_comment(CommentId { 0: stats.comment_id.unwrap() }, score); + match response { + Ok(comment) => Some(VotingStats::from_comment(comment.comment_view.counts, comment.comment_view.my_vote)), + Err(err) => { println!("{}", err.to_string()); None } + } + }; + if let Some(info) = info { sender.input(VotingRowInput::UpdateStats(info)) }; + }); + } + VotingRowInput::UpdateStats(stats) => { + self.stats = stats; + } + } + } +}