lemoa/src/components/post_page.rs

375 lines
14 KiB
Rust

use gtk::prelude::*;
use lemmy_api_common::{
lemmy_db_views::structs::{CommentView, PostView},
post::GetPostResponse,
};
use relm4::{factory::FactoryVecDeque, prelude::*};
use relm4_components::web_image::WebImage;
use crate::{
api,
dialogs::editor::{DialogMsg, EditorData, EditorDialog, EditorOutput, EditorType},
settings,
util::{self, get_web_image_msg, get_web_image_url, markdown_to_pango_markup},
};
use super::{
comment_row::CommentRow,
voting_row::{VotingRowInput, VotingRowModel, VotingStats},
};
pub struct PostPage {
info: GetPostResponse,
image: Controller<WebImage>,
creator_avatar: Controller<WebImage>,
community_avatar: Controller<WebImage>,
comments: FactoryVecDeque<CommentRow>,
#[allow(dead_code)]
create_comment_dialog: Controller<EditorDialog>,
voting_row: Controller<VotingRowModel>,
}
#[derive(Debug)]
pub enum PostInput {
UpdatePost(GetPostResponse),
DoneFetchComments(Vec<CommentView>),
OpenPerson,
OpenCommunity,
OpenLink,
OpenCreateCommentDialog,
CreateCommentRequest(EditorData),
EditPostRequest(EditorData),
CreatedComment(CommentView),
OpenEditPostDialog,
DeletePost,
DoneEditPost(PostView),
PassAppMessage(crate::AppMsg),
}
#[relm4::component(pub)]
impl SimpleComponent for PostPage {
type Init = GetPostResponse;
type Input = PostInput;
type Output = crate::AppMsg;
view! {
gtk::ScrolledWindow {
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_vexpand: false,
set_margin_all: 10,
#[local_ref]
image -> gtk::Box {
set_height_request: 400,
set_margin_bottom: 20,
set_margin_top: 20,
},
gtk::Label {
#[watch]
set_text: &model.info.post_view.post.name,
add_css_class: "font-very-bold",
},
gtk::Label {
#[watch]
set_markup: &markdown_to_pango_markup(model.info.post_view.post.body.clone().unwrap_or("".to_string())),
set_margin_top: 10,
set_use_markup: true,
},
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_margin_top: 10,
set_spacing: 10,
set_vexpand: false,
set_halign: gtk::Align::Center,
gtk::Label {
set_text: "posted by"
},
if model.info.post_view.creator.avatar.is_some() {
gtk::Box {
set_hexpand: false,
set_margin_start: 10,
#[local_ref]
creator_avatar -> gtk::Box {}
}
} else {
gtk::Box {}
},
gtk::Button {
#[watch]
set_label: &model.info.post_view.creator.name,
connect_clicked => PostInput::OpenPerson,
},
gtk::Label {
set_text: " in "
},
if model.info.community_view.community.icon.is_some() {
gtk::Box {
set_hexpand: false,
#[local_ref]
community_avatar -> gtk::Box {}
}
} else {
gtk::Box {}
},
gtk::Button {
#[watch]
set_label: &model.info.community_view.community.title,
connect_clicked => PostInput::OpenCommunity,
},
gtk::Label {
set_margin_start: 10,
set_label: &util::format_elapsed_time(model.info.post_view.post.published),
},
gtk::Box {
set_hexpand: true,
},
gtk::Button {
set_label: "View",
connect_clicked => PostInput::OpenLink,
},
if model.info.post_view.creator.id.0 == settings::get_current_account().id {
gtk::Button {
set_icon_name: "document-edit",
connect_clicked => PostInput::OpenEditPostDialog,
set_margin_start: 5,
}
} else {
gtk::Box {}
},
if model.info.post_view.creator.id.0 == settings::get_current_account().id {
gtk::Button {
set_icon_name: "edit-delete",
connect_clicked => PostInput::DeletePost,
set_margin_start: 5,
}
} else {
gtk::Box {}
}
},
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_margin_top: 10,
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", model.info.post_view.counts.comments),
},
if settings::get_current_account().jwt.is_some() {
gtk::Button {
set_label: "Comment",
set_margin_start: 10,
connect_clicked => PostInput::OpenCreateCommentDialog,
}
} else {
gtk::Box {}
}
},
gtk::Separator {},
#[local_ref]
comments -> gtk::Box {
set_orientation: gtk::Orientation::Vertical,
}
}
}
}
fn init(
init: Self::Init,
root: &Self::Root,
sender: relm4::ComponentSender<Self>,
) -> relm4::ComponentParts<Self> {
let image = WebImage::builder().launch("".to_string()).detach();
let comments = FactoryVecDeque::new(gtk::Box::default(), sender.input_sender());
let creator_avatar = WebImage::builder().launch("".to_string()).detach();
let community_avatar = WebImage::builder().launch("".to_string()).detach();
let dialog = EditorDialog::builder()
.transient_for(root)
.launch(EditorType::Comment)
.forward(sender.input_sender(), |msg| match msg {
EditorOutput::CreateRequest(comment, _) => PostInput::CreateCommentRequest(comment),
EditorOutput::EditRequest(post, _) => PostInput::EditPostRequest(post),
});
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 }
}
fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>) {
match message {
PostInput::UpdatePost(post) => {
self.info = post.clone();
self.image
.emit(get_web_image_msg(post.post_view.post.thumbnail_url));
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 || {
if post.post_view.counts.comments == 0 {
return;
}
let comments = api::post::get_comments(post.post_view.post.id);
if let Ok(comments) = comments {
sender.input(PostInput::DoneFetchComments(comments));
}
});
}
PostInput::DoneFetchComments(comments) => {
for comment in comments {
self.comments.guard().push_back(comment);
}
}
PostInput::OpenPerson => {
let person_id = self.info.post_view.creator.id.clone();
sender
.output_sender()
.emit(crate::AppMsg::OpenPerson(person_id));
}
PostInput::OpenCommunity => {
let community_id = self.info.community_view.community.id.clone();
sender
.output_sender()
.emit(crate::AppMsg::OpenCommunity(community_id));
}
PostInput::OpenLink => {
let post = self.info.post_view.post.clone();
let mut link = get_web_image_url(post.url);
if link.is_empty() {
link = get_web_image_url(post.thumbnail_url);
}
if link.is_empty() {
link = get_web_image_url(post.embed_video_url);
}
if link.is_empty() {
return;
}
gtk::show_uri(None::<&relm4::gtk::Window>, &link, 0);
}
PostInput::OpenCreateCommentDialog => {
let sender = self.create_comment_dialog.sender();
sender.emit(DialogMsg::UpdateType(EditorType::Comment, true));
sender.emit(DialogMsg::Show);
}
PostInput::CreatedComment(comment) => {
self.comments.guard().push_front(comment);
}
PostInput::CreateCommentRequest(post) => {
let id = self.info.post_view.post.id.0;
std::thread::spawn(move || {
let message = match api::comment::create_comment(id, post.body, None) {
Ok(comment) => Some(PostInput::CreatedComment(comment.comment_view)),
Err(err) => {
println!("{}", err.to_string());
None
}
};
if let Some(message) = message {
sender.input(message)
};
});
}
PostInput::DeletePost => {
let post_id = self.info.post_view.post.id;
std::thread::spawn(move || {
let _ = api::post::delete_post(post_id);
sender
.output_sender()
.emit(crate::AppMsg::StartFetchPosts(None, true));
});
}
PostInput::OpenEditPostDialog => {
let url = match self.info.post_view.post.url.clone() {
Some(url) => url.to_string(),
None => String::from(""),
};
let data = EditorData {
name: self.info.post_view.post.name.clone(),
body: self
.info
.post_view
.post
.body
.clone()
.unwrap_or(String::from("")),
url: reqwest::Url::parse(&url).ok(),
id: None,
};
let sender = self.create_comment_dialog.sender();
sender.emit(DialogMsg::UpdateData(data));
sender.emit(DialogMsg::UpdateType(EditorType::Post, false));
sender.emit(DialogMsg::Show);
}
PostInput::EditPostRequest(post) => {
let id = self.info.post_view.post.id.0;
std::thread::spawn(move || {
let message = match api::post::edit_post(post.name, post.url, post.body, id) {
Ok(post) => Some(PostInput::DoneEditPost(post.post_view)),
Err(err) => {
println!("{}", err.to_string());
None
}
};
if let Some(message) = message {
sender.input(message)
};
});
}
PostInput::DoneEditPost(post) => {
self.info.post_view = post;
}
PostInput::PassAppMessage(message) => {
sender.output_sender().emit(message);
}
}
}
}