Support for editing comments

This commit is contained in:
Bnyro 2023-06-21 16:36:24 +02:00
parent b56776b671
commit 3ac17ddd9f
5 changed files with 106 additions and 42 deletions

View File

@ -4,16 +4,18 @@ use gtk::prelude::*;
use relm4_components::web_image::WebImage; use relm4_components::web_image::WebImage;
use crate::api; use crate::api;
use crate::dialogs::editor::EditorData;
use crate::util::get_web_image_url; use crate::util::get_web_image_url;
use crate::util::markdown_to_pango_markup; use crate::util::markdown_to_pango_markup;
use crate::settings; use crate::settings;
use super::post_page::PostInput;
use super::voting_row::VotingRowModel; use super::voting_row::VotingRowModel;
use super::voting_row::VotingStats; use super::voting_row::VotingStats;
#[derive(Debug)] #[derive(Debug)]
pub struct CommentRow { pub struct CommentRow {
comment: CommentView, pub comment: CommentView,
avatar: Controller<WebImage>, avatar: Controller<WebImage>,
voting_row: Controller<VotingRowModel> voting_row: Controller<VotingRowModel>
} }
@ -22,16 +24,17 @@ pub struct CommentRow {
pub enum CommentRowMsg { pub enum CommentRowMsg {
OpenPerson, OpenPerson,
DeleteComment, DeleteComment,
OpenEditCommentDialog
} }
#[relm4::factory(pub)] #[relm4::factory(pub)]
impl FactoryComponent for CommentRow { impl FactoryComponent for CommentRow {
type Init = CommentView; type Init = CommentView;
type Input = CommentRowMsg; type Input = CommentRowMsg;
type Output = crate::AppMsg; type Output = PostInput;
type CommandOutput = (); type CommandOutput = ();
type Widgets = PostViewWidgets; type Widgets = PostViewWidgets;
type ParentInput = crate::AppMsg; type ParentInput = PostInput;
type ParentWidget = gtk::Box; type ParentWidget = gtk::Box;
view! { view! {
@ -64,9 +67,11 @@ impl FactoryComponent for CommentRow {
}, },
gtk::Label { gtk::Label {
#[watch]
set_markup: &markdown_to_pango_markup(self.comment.comment.content.clone()), set_markup: &markdown_to_pango_markup(self.comment.comment.content.clone()),
set_halign: gtk::Align::Start, set_halign: gtk::Align::Start,
set_use_markup: true, set_use_markup: true,
set_selectable: true,
}, },
gtk::Box { gtk::Box {
@ -74,6 +79,16 @@ impl FactoryComponent for CommentRow {
#[local_ref] #[local_ref]
voting_row -> gtk::Box {}, voting_row -> gtk::Box {},
if self.comment.creator.id.0 == settings::get_current_account().id {
gtk::Button {
set_icon_name: "document-edit",
connect_clicked => CommentRowMsg::OpenEditCommentDialog,
set_margin_start: 5,
}
} else {
gtk::Box {}
},
if self.comment.creator.id.0 == settings::get_current_account().id { if self.comment.creator.id.0 == settings::get_current_account().id {
gtk::Button { gtk::Button {
set_icon_name: "edit-delete", set_icon_name: "edit-delete",
@ -94,8 +109,8 @@ impl FactoryComponent for CommentRow {
} }
fn init_model(value: Self::Init, _index: &DynamicIndex, _sender: FactorySender<Self>) -> Self { fn init_model(value: Self::Init, _index: &DynamicIndex, _sender: FactorySender<Self>) -> Self {
let avatar = WebImage::builder().launch(get_web_image_url(value.community.clone().icon)).detach(); let avatar = WebImage::builder().launch(get_web_image_url(value.creator.avatar.clone())).detach();
let voting_row = VotingRowModel::builder().launch(VotingStats::from_comment(value.clone().counts, value.my_vote)).detach(); let voting_row = VotingRowModel::builder().launch(VotingStats::from_comment(value.counts.clone(), value.my_vote)).detach();
Self { comment: value, avatar, voting_row } Self { comment: value, avatar, voting_row }
} }
@ -116,15 +131,24 @@ impl FactoryComponent for CommentRow {
fn update(&mut self, message: Self::Input, sender: FactorySender<Self>) { fn update(&mut self, message: Self::Input, sender: FactorySender<Self>) {
match message { match message {
CommentRowMsg::OpenPerson => { CommentRowMsg::OpenPerson => {
sender.output(crate::AppMsg::OpenPerson(self.comment.creator.name.clone())) sender.output(PostInput::PassAppMessage(crate::AppMsg::OpenPerson(self.comment.creator.name.clone())));
} }
CommentRowMsg::DeleteComment => { CommentRowMsg::DeleteComment => {
let comment_id = self.comment.comment.id; let comment_id = self.comment.comment.id;
std::thread::spawn(move || { std::thread::spawn(move || {
let _ = api::comment::delete_comment(comment_id); let _ = api::comment::delete_comment(comment_id);
let _ = sender.output(crate::AppMsg::StartFetchPosts(None)); let _ = sender.output(PostInput::PassAppMessage(crate::AppMsg::StartFetchPosts(None)));
}); });
} }
CommentRowMsg::OpenEditCommentDialog => {
let data = EditorData {
name: String::from(""),
body: self.comment.comment.content.clone(),
url: None,
id: Some(self.comment.comment.id.0),
};
sender.output(PostInput::OpenEditCommentDialog(data));
}
} }
} }
} }

View File

@ -1,4 +1,4 @@
use crate::{util::markdown_to_pango_markup, dialogs::editor::{EditorDialog, DialogMsg, EditorOutput, DialogType, EditorData}}; use crate::{util::markdown_to_pango_markup, dialogs::editor::{EditorDialog, DialogMsg, EditorOutput, EditorType, EditorData}};
use lemmy_api_common::{lemmy_db_views::structs::PostView, lemmy_db_views_actor::structs::CommunityView, lemmy_db_schema::SubscribedType}; use lemmy_api_common::{lemmy_db_views::structs::PostView, lemmy_db_views_actor::structs::CommunityView, lemmy_db_schema::SubscribedType};
use relm4::{prelude::*, factory::FactoryVecDeque, MessageBroker}; use relm4::{prelude::*, factory::FactoryVecDeque, MessageBroker};
use gtk::prelude::*; use gtk::prelude::*;
@ -58,7 +58,7 @@ impl SimpleComponent for CommunityPage {
}, },
gtk::Label { gtk::Label {
#[watch] #[watch]
set_markup: &markdown_to_pango_markup(model.info.clone().community.description.unwrap_or("".to_string())), set_markup: &markdown_to_pango_markup(model.info.community.description.clone().unwrap_or("".to_string())),
set_use_markup: true, set_use_markup: true,
}, },
gtk::Box { gtk::Box {
@ -124,9 +124,9 @@ impl SimpleComponent for CommunityPage {
let dialog = EditorDialog::builder() let dialog = EditorDialog::builder()
.transient_for(root) .transient_for(root)
.launch_with_broker(DialogType::Post, &COMMUNITY_PAGE_BROKER) .launch_with_broker(EditorType::Post, &COMMUNITY_PAGE_BROKER)
.forward(sender.input_sender(), |msg| match msg { .forward(sender.input_sender(), |msg| match msg {
EditorOutput::CreateRequest(post) => CommunityInput::CreatePostRequest(post), EditorOutput::CreateRequest(post, _) => CommunityInput::CreatePostRequest(post),
_ => CommunityInput::None _ => CommunityInput::None
}); });

View File

@ -3,7 +3,7 @@ use relm4::{prelude::*, factory::FactoryVecDeque, MessageBroker};
use gtk::prelude::*; use gtk::prelude::*;
use relm4_components::web_image::WebImage; use relm4_components::web_image::WebImage;
use crate::{api, util::{get_web_image_msg, get_web_image_url, markdown_to_pango_markup}, dialogs::editor::{EditorDialog, EditorOutput, DialogMsg, DialogType, EditorData}, settings}; use crate::{api, util::{get_web_image_msg, get_web_image_url, markdown_to_pango_markup}, dialogs::editor::{EditorDialog, EditorOutput, DialogMsg, EditorType, EditorData}, settings};
use super::{comment_row::CommentRow, voting_row::{VotingRowModel, VotingStats, VotingRowInput}}; use super::{comment_row::CommentRow, voting_row::{VotingRowModel, VotingStats, VotingRowInput}};
@ -31,9 +31,13 @@ pub enum PostInput {
CreateCommentRequest(EditorData), CreateCommentRequest(EditorData),
EditPostRequest(EditorData), EditPostRequest(EditorData),
CreatedComment(CommentView), CreatedComment(CommentView),
EditPost, OpenEditPostDialog,
OpenEditCommentDialog(EditorData),
DeletePost, DeletePost,
DoneEditPost(PostView) DoneEditPost(PostView),
PassAppMessage(crate::AppMsg),
EditCommentRequest(EditorData),
UpdateComment(CommentView),
} }
#[relm4::component(pub)] #[relm4::component(pub)]
@ -125,8 +129,8 @@ impl SimpleComponent for PostPage {
if model.info.post_view.creator.id.0 == settings::get_current_account().id { if model.info.post_view.creator.id.0 == settings::get_current_account().id {
gtk::Button { gtk::Button {
set_icon_name: "document-edit", set_icon_name: "document-edit",
connect_clicked => PostInput::EditPost, connect_clicked => PostInput::OpenEditPostDialog,
set_margin_start: 10, set_margin_start: 5,
} }
} else { } else {
gtk::Box {} gtk::Box {}
@ -136,7 +140,7 @@ impl SimpleComponent for PostPage {
gtk::Button { gtk::Button {
set_icon_name: "edit-delete", set_icon_name: "edit-delete",
connect_clicked => PostInput::DeletePost, connect_clicked => PostInput::DeletePost,
set_margin_start: 10, set_margin_start: 5,
} }
} else { } else {
gtk::Box {} gtk::Box {}
@ -180,15 +184,18 @@ impl SimpleComponent for PostPage {
sender: relm4::ComponentSender<Self>, sender: relm4::ComponentSender<Self>,
) -> relm4::ComponentParts<Self> { ) -> relm4::ComponentParts<Self> {
let image = WebImage::builder().launch("".to_string()).detach(); let image = WebImage::builder().launch("".to_string()).detach();
let comments = FactoryVecDeque::new(gtk::Box::default(), sender.output_sender()); let comments = FactoryVecDeque::new(gtk::Box::default(), sender.input_sender());
let creator_avatar = WebImage::builder().launch("".to_string()).detach(); let creator_avatar = WebImage::builder().launch("".to_string()).detach();
let community_avatar = WebImage::builder().launch("".to_string()).detach(); let community_avatar = WebImage::builder().launch("".to_string()).detach();
let dialog = EditorDialog::builder() let dialog = EditorDialog::builder()
.transient_for(root) .transient_for(root)
.launch_with_broker(DialogType::Comment, &POST_PAGE_BROKER) .launch_with_broker(EditorType::Comment, &POST_PAGE_BROKER)
.forward(sender.input_sender(), |msg| match msg { .forward(sender.input_sender(), |msg| match msg {
EditorOutput::CreateRequest(comment) => PostInput::CreateCommentRequest(comment), EditorOutput::CreateRequest(comment, _) => PostInput::CreateCommentRequest(comment),
EditorOutput::EditRequest(post) => PostInput::EditPostRequest(post) EditorOutput::EditRequest(post, type_) => match type_ {
EditorType::Post => PostInput::EditPostRequest(post),
EditorType::Comment => PostInput::EditCommentRequest(post)
}
}); });
let voting_row = VotingRowModel::builder().launch(VotingStats::default()).detach(); 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 model = PostPage { info: init, image, comments, creator_avatar, community_avatar, create_comment_dialog: dialog, voting_row };
@ -249,7 +256,8 @@ impl SimpleComponent for PostPage {
gtk::show_uri(None::<&relm4::gtk::Window>, &link, 0); gtk::show_uri(None::<&relm4::gtk::Window>, &link, 0);
} }
PostInput::OpenCreateCommentDialog => { PostInput::OpenCreateCommentDialog => {
POST_PAGE_BROKER.send(DialogMsg::Show) POST_PAGE_BROKER.send(DialogMsg::UpdateType(EditorType::Comment, true));
POST_PAGE_BROKER.send(DialogMsg::Show);
} }
PostInput::CreatedComment(comment) => { PostInput::CreatedComment(comment) => {
self.comments.guard().push_front(comment); self.comments.guard().push_front(comment);
@ -271,7 +279,7 @@ impl SimpleComponent for PostPage {
let _ = sender.output(crate::AppMsg::StartFetchPosts(None)); let _ = sender.output(crate::AppMsg::StartFetchPosts(None));
}); });
} }
PostInput::EditPost => { PostInput::OpenEditPostDialog => {
let url = match self.info.post_view.post.url.clone() { let url = match self.info.post_view.post.url.clone() {
Some(url) => url.to_string(), Some(url) => url.to_string(),
None => String::from("") None => String::from("")
@ -280,9 +288,10 @@ impl SimpleComponent for PostPage {
name: self.info.post_view.post.name.clone(), name: self.info.post_view.post.name.clone(),
body: self.info.post_view.post.body.clone().unwrap_or(String::from("")), body: self.info.post_view.post.body.clone().unwrap_or(String::from("")),
url: reqwest::Url::parse(&url).ok(), url: reqwest::Url::parse(&url).ok(),
id: None,
}; };
POST_PAGE_BROKER.send(DialogMsg::UpdateData(data)); POST_PAGE_BROKER.send(DialogMsg::UpdateData(data));
POST_PAGE_BROKER.send(DialogMsg::UpdateType(DialogType::Post, false)); POST_PAGE_BROKER.send(DialogMsg::UpdateType(EditorType::Post, false));
POST_PAGE_BROKER.send(DialogMsg::Show) POST_PAGE_BROKER.send(DialogMsg::Show)
} }
PostInput::EditPostRequest(post) => { PostInput::EditPostRequest(post) => {
@ -298,6 +307,34 @@ impl SimpleComponent for PostPage {
PostInput::DoneEditPost(post) => { PostInput::DoneEditPost(post) => {
self.info.post_view = post; self.info.post_view = post;
} }
PostInput::OpenEditCommentDialog(data) => {
POST_PAGE_BROKER.send(DialogMsg::UpdateData(data));
POST_PAGE_BROKER.send(DialogMsg::UpdateType(EditorType::Comment, false));
POST_PAGE_BROKER.send(DialogMsg::Show);
}
PostInput::EditCommentRequest(data) => {
std::thread::spawn(move || {
let message = match api::comment::edit_comment(data.body, data.id.unwrap()) {
Ok(comment) => Some(PostInput::UpdateComment(comment.comment_view)),
Err(err) => { println!("{}", err.to_string()); None }
};
if let Some(message) = message { sender.input(message) };
});
}
PostInput::UpdateComment(comment) => {
let mut index = 0;
let id = comment.comment.id;
loop {
if self.comments.guard().get(index).unwrap().comment.comment.id == id {
self.comments.guard().get_mut(index).unwrap().comment = comment;
break;
}
index += 1;
}
}
PostInput::PassAppMessage(message) => {
let _ = sender.output(message);
}
} }
} }
} }

View File

@ -104,7 +104,7 @@ impl FactoryComponent for PostRow {
}, },
gtk::Label { gtk::Label {
set_halign: gtk::Align::Start, set_halign: gtk::Align::Start,
set_text: &format!("{} comments", self.post.clone().counts.comments), set_text: &format!("{} comments", self.post.counts.comments.clone()),
}, },
if self.post.creator.id.0 == settings::get_current_account().id { if self.post.creator.id.0 == settings::get_current_account().id {
gtk::Button { gtk::Button {
@ -126,9 +126,9 @@ impl FactoryComponent for PostRow {
fn forward_to_parent(output: Self::Output) -> Option<Self::ParentInput> { Some(output) } fn forward_to_parent(output: Self::Output) -> Option<Self::ParentInput> { Some(output) }
fn init_model(value: Self::Init, _index: &DynamicIndex, _sender: FactorySender<Self>) -> Self { fn init_model(value: Self::Init, _index: &DynamicIndex, _sender: FactorySender<Self>) -> Self {
let author_image= WebImage::builder().launch(get_web_image_url(value.creator.clone().avatar)).detach(); let author_image= WebImage::builder().launch(get_web_image_url(value.creator.avatar.clone())).detach();
let community_image= WebImage::builder().launch(get_web_image_url(value.creator.clone().avatar)).detach(); let community_image= WebImage::builder().launch(get_web_image_url(value.community.icon.clone())).detach();
let voting_row = VotingRowModel::builder().launch(VotingStats::from_post(value.clone().counts, value.my_vote)).detach(); let voting_row = VotingRowModel::builder().launch(VotingStats::from_post(value.counts.clone(), value.my_vote)).detach();
Self { post: value, author_image, community_image, voting_row } Self { post: value, author_image, community_image, voting_row }
} }

View File

@ -6,19 +6,22 @@ pub struct EditorData {
pub name: String, pub name: String,
pub body: String, pub body: String,
pub url: Option<reqwest::Url>, pub url: Option<reqwest::Url>,
pub id: Option<i32>
} }
pub struct EditorDialog { pub struct EditorDialog {
type_: DialogType, type_: EditorType,
is_new: bool, is_new: bool,
visible: bool, visible: bool,
name_buffer: gtk::EntryBuffer, name_buffer: gtk::EntryBuffer,
url_buffer: gtk::EntryBuffer, url_buffer: gtk::EntryBuffer,
body_buffer: gtk::TextBuffer, body_buffer: gtk::TextBuffer,
// Optional field to temporarily store the post or comment id
id: Option<i32>
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum DialogType { pub enum EditorType {
Post, Post,
Comment Comment
} }
@ -27,20 +30,20 @@ pub enum DialogType {
pub enum DialogMsg { pub enum DialogMsg {
Show, Show,
Hide, Hide,
UpdateType(DialogType, bool), UpdateType(EditorType, bool),
UpdateData(EditorData), UpdateData(EditorData),
Okay Okay
} }
#[derive(Debug)] #[derive(Debug)]
pub enum EditorOutput { pub enum EditorOutput {
CreateRequest(EditorData), CreateRequest(EditorData, EditorType),
EditRequest(EditorData) EditRequest(EditorData, EditorType)
} }
#[relm4::component(pub)] #[relm4::component(pub)]
impl SimpleComponent for EditorDialog { impl SimpleComponent for EditorDialog {
type Init = DialogType; type Init = EditorType;
type Input = DialogMsg; type Input = DialogMsg;
type Output = EditorOutput; type Output = EditorOutput;
@ -58,13 +61,13 @@ impl SimpleComponent for EditorDialog {
set_margin_all: 20, set_margin_all: 20,
match model.type_ { match model.type_ {
DialogType::Post => { EditorType::Post => {
gtk::Box { gtk::Box {
set_orientation: gtk::Orientation::Vertical, set_orientation: gtk::Orientation::Vertical,
gtk::Label { gtk::Label {
set_halign: gtk::Align::Center, set_halign: gtk::Align::Center,
set_valign: gtk::Align::Center, set_valign: gtk::Align::Center,
set_label: "Create post", set_label: "Post content",
add_css_class: "font-bold" add_css_class: "font-bold"
}, },
gtk::Entry { gtk::Entry {
@ -81,12 +84,12 @@ impl SimpleComponent for EditorDialog {
}, },
} }
} }
DialogType::Comment => { EditorType::Comment => {
gtk::Box { gtk::Box {
gtk::Label { gtk::Label {
set_halign: gtk::Align::Center, set_halign: gtk::Align::Center,
set_valign: gtk::Align::Center, set_valign: gtk::Align::Center,
set_label: "Create comment", set_label: "Comment content",
add_css_class: "font-bold" add_css_class: "font-bold"
}, },
} }
@ -133,7 +136,7 @@ impl SimpleComponent for EditorDialog {
let name_buffer = gtk::EntryBuffer::builder().build(); let name_buffer = gtk::EntryBuffer::builder().build();
let url_buffer = gtk::EntryBuffer::builder().build(); let url_buffer = gtk::EntryBuffer::builder().build();
let body_buffer = gtk::TextBuffer::builder().build(); let body_buffer = gtk::TextBuffer::builder().build();
let model = EditorDialog { type_: init, visible: false, is_new: true, name_buffer, url_buffer, body_buffer }; let model = EditorDialog { type_: init, visible: false, is_new: true, name_buffer, url_buffer, body_buffer, id: None };
let widgets = view_output!(); let widgets = view_output!();
ComponentParts { model, widgets } ComponentParts { model, widgets }
} }
@ -153,10 +156,10 @@ impl SimpleComponent for EditorDialog {
let (start, end) = &self.body_buffer.bounds(); let (start, end) = &self.body_buffer.bounds();
let body = self.body_buffer.text(start, end, true).to_string(); let body = self.body_buffer.text(start, end, true).to_string();
let url = reqwest::Url::parse(&url).ok(); let url = reqwest::Url::parse(&url).ok();
let post = EditorData { name, body, url }; let post = EditorData { name, body, url, id: self.id };
let message = match self.is_new { let message = match self.is_new {
true => EditorOutput::CreateRequest(post), true => EditorOutput::CreateRequest(post, self.type_),
false => EditorOutput::EditRequest(post) false => EditorOutput::EditRequest(post, self.type_)
}; };
let _ = sender.output(message); let _ = sender.output(message);
self.visible = false; self.visible = false;