use crate::{ dialogs::editor::{DialogMsg, EditorData, EditorDialog, EditorOutput, EditorType}, util::markdown_to_pango_markup, }; use gtk::prelude::*; use lemmy_api_common::{ lemmy_db_schema::{SortType, SubscribedType}, lemmy_db_views::structs::PostView, lemmy_db_views_actor::structs::CommunityView, }; use relm4::{factory::FactoryVecDeque, prelude::*}; use relm4_components::web_image::WebImage; use crate::{api, settings, util::get_web_image_msg}; use super::{ post_row::PostRow, sort_dropdown::{SortDropdown, SortDropdownOutput}, }; pub struct CommunityPage { info: CommunityView, avatar: Controller, sort_dropdown: Controller, posts: FactoryVecDeque, #[allow(dead_code)] create_post_dialog: Controller, current_sort_type: SortType, current_posts_page: i64, } #[derive(Debug)] pub enum CommunityInput { UpdateCommunity(CommunityView), FetchPosts, DoneFetchPosts(Vec), OpenCreatePostDialog, CreatePostRequest(EditorData), CreatedPost(PostView), ToggleSubscription, UpdateSubscriptionState(SubscribedType), UpdateOrder(SortType), ToggleBlocked, UpdateBlocked(bool), None, } #[relm4::component(pub)] impl SimpleComponent for CommunityPage { type Init = CommunityView; type Input = CommunityInput; type Output = crate::AppMsg; view! { gtk::ScrolledWindow { set_vexpand: false, gtk::Box { set_orientation: gtk::Orientation::Vertical, set_vexpand: false, set_margin_all: 10, #[local_ref] avatar -> gtk::Box { set_size_request: (100, 100), set_margin_bottom: 20, set_margin_top: 20, }, gtk::Label { #[watch] set_text: &model.info.community.name, add_css_class: "font-very-bold", }, gtk::Label { #[watch] set_markup: &markdown_to_pango_markup(model.info.community.description.clone().unwrap_or("".to_string())), set_use_markup: true, set_wrap: true, }, gtk::Box { set_orientation: gtk::Orientation::Horizontal, set_halign: gtk::Align::Center, gtk::Label { #[watch] set_text: &format!("{} subscribers", model.info.counts.subscribers), set_margin_end: 10, }, gtk::Label { #[watch] set_text: &format!("{} posts, {} comments", model.info.counts.posts, model.info.counts.comments), set_margin_start: 10, }, }, gtk::Box { set_orientation: gtk::Orientation::Horizontal, set_halign: gtk::Align::Center, set_margin_top: 10, set_spacing: 10, #[watch] set_visible: settings::get_current_account().jwt.is_some(), match model.info.subscribed { SubscribedType::Subscribed => { gtk::Button { set_label: "Unsubscribe", connect_clicked => CommunityInput::ToggleSubscription, } } SubscribedType::NotSubscribed => { gtk::Button { set_label: "Subscribe", connect_clicked => CommunityInput::ToggleSubscription, } } SubscribedType::Pending => { gtk::Label { set_label: "Subscription pending", } } }, gtk::Button { set_label: "Block", #[watch] set_visible: !model.info.blocked, connect_clicked => CommunityInput::ToggleBlocked, }, gtk::Button { set_label: "Unblock", #[watch] set_visible: model.info.blocked, connect_clicked => CommunityInput::ToggleBlocked, }, gtk::Button { set_label: "Create post", connect_clicked => CommunityInput::OpenCreatePostDialog, } }, #[local_ref] sort_dropdown -> gtk::DropDown { set_margin_top: 10, set_halign: gtk::Align::Start, }, gtk::Separator { set_margin_top: 10, }, #[local_ref] posts -> gtk::Box { set_orientation: gtk::Orientation::Vertical, }, gtk::Button { set_label: "More", set_margin_all: 10, connect_clicked => CommunityInput::FetchPosts, } } } } fn init( init: Self::Init, root: &Self::Root, sender: relm4::ComponentSender, ) -> relm4::ComponentParts { let avatar = WebImage::builder().launch("".to_string()).detach(); let posts = FactoryVecDeque::new(gtk::Box::default(), sender.output_sender()); let sort_dropdown = SortDropdown::builder() .launch(()) .forward(sender.input_sender(), |msg| match msg { SortDropdownOutput::New(sort_order) => CommunityInput::UpdateOrder(sort_order), }); let dialog = EditorDialog::builder() .transient_for(root) .launch(EditorType::Post) .forward(sender.input_sender(), |msg| match msg { EditorOutput::CreateRequest(post, _) => CommunityInput::CreatePostRequest(post), _ => CommunityInput::None, }); let model = CommunityPage { info: init, avatar, sort_dropdown, posts, create_post_dialog: dialog, current_sort_type: SortType::Hot, current_posts_page: 0, }; let avatar = model.avatar.widget(); let sort_dropdown = model.sort_dropdown.widget(); let posts = model.posts.widget(); let widgets = view_output!(); ComponentParts { model, widgets } } fn update(&mut self, message: Self::Input, sender: ComponentSender) { match message { CommunityInput::UpdateCommunity(community) => { self.info = community.clone(); self.avatar .emit(get_web_image_msg(community.community.icon)); self.posts.guard().clear(); self.current_posts_page = 0; if community.counts.posts == 0 { return; } sender.input(CommunityInput::FetchPosts); } CommunityInput::FetchPosts => { let name = self.info.community.name.clone(); let sort_type = self.current_sort_type; self.current_posts_page += 1; let page = self.current_posts_page; std::thread::spawn(move || { let community_posts = api::posts::list_posts(page, Some(name), None, Some(sort_type)); if let Ok(community_posts) = community_posts { sender.input(CommunityInput::DoneFetchPosts(community_posts)); } }); } CommunityInput::DoneFetchPosts(posts) => { for post in posts { self.posts.guard().push_back(post); } } CommunityInput::OpenCreatePostDialog => { let sender = self.create_post_dialog.sender(); sender.emit(DialogMsg::Show); } CommunityInput::CreatedPost(post) => { self.posts.guard().push_front(post); } CommunityInput::CreatePostRequest(post) => { let id = self.info.community.id.0; std::thread::spawn(move || { let message = match api::post::create_post(post.name, post.body, post.url, id) { Ok(post) => Some(CommunityInput::CreatedPost(post.post_view)), Err(err) => { println!("{}", err); None } }; if let Some(message) = message { sender.input(message) }; }); } CommunityInput::ToggleSubscription => { let community_id = self.info.community.id; let new_state = matches!(self.info.subscribed, SubscribedType::NotSubscribed); std::thread::spawn(move || { match api::community::follow_community(community_id, new_state) { Ok(community) => { sender.input(CommunityInput::UpdateSubscriptionState( community.community_view.subscribed, )); } Err(err) => { println!("{}", err); } }; }); } CommunityInput::UpdateSubscriptionState(state) => { self.info.subscribed = state; } CommunityInput::UpdateOrder(sort_order) => { self.current_sort_type = sort_order; self.current_posts_page = 0; self.posts.guard().clear(); sender.input_sender().emit(CommunityInput::FetchPosts); } CommunityInput::ToggleBlocked => { let community_id = self.info.community.id; let blocked = self.info.blocked; std::thread::spawn(move || { match api::community::block_community(community_id, !blocked) { Ok(resp) => { sender.input(CommunityInput::UpdateBlocked(resp.blocked)); } Err(err) => { println!("{}", err); } } }); } CommunityInput::UpdateBlocked(blocked) => self.info.blocked = blocked, CommunityInput::None => {} } } }