Add support for viewing the inbox (mentions, replies)

This commit is contained in:
Bnyro 2023-06-22 09:04:15 +02:00
parent b56a8247e1
commit 1ebfa7776f
5 changed files with 326 additions and 7 deletions

View File

@ -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; use crate::settings;
@ -16,3 +16,32 @@ pub fn get_user(username: String, page: i64) -> std::result::Result<GetPersonDet
pub fn default_person() -> GetPersonDetailsResponse { pub fn default_person() -> GetPersonDetailsResponse {
serde_json::from_str(include_str!("../examples/person.json")).unwrap() serde_json::from_str(include_str!("../examples/person.json")).unwrap()
} }
pub fn get_mentions(page: i64, unread_only: bool) -> std::result::Result<GetPersonMentionsResponse, reqwest::Error> {
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", &params)
}
pub fn get_replies(page: i64, unread_only: bool) -> std::result::Result<GetRepliesResponse, reqwest::Error> {
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", &params)
}
pub fn mark_all_as_read() -> std::result::Result<GetRepliesResponse, reqwest::Error> {
let params = MarkAllAsRead {
auth: settings::get_current_account().jwt.unwrap(),
};
super::post("/user/mark_all_as_read", &params)
}

View File

@ -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<MentionRow>,
page: i64,
unread_only: bool,
type_: InboxType
}
#[derive(Debug)]
pub enum InboxInput {
UpdateType(InboxType),
ToggleUnreadState,
FetchInbox,
UpdateInbox(Vec<CommentReplyView>)
}
#[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<Self>,
) -> ComponentParts<Self> {
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<Self>) {
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);
}
}
}
}
}

View File

@ -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<WebImage>,
community_image: Controller<WebImage>,
voting_row: Controller<VotingRowModel>
}
#[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<Self::ParentInput> {
Some(output)
}
fn init_model(value: Self::Init, _index: &DynamicIndex, _sender: FactorySender<Self>) -> 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: &<Self::ParentWidget as relm4::factory::FactoryView>::ReturnedWidget,
sender: FactorySender<Self>,
) -> 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<Self>) {
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()));
}
}
}
}

View File

@ -5,3 +5,5 @@ pub mod community_page;
pub mod post_page; pub mod post_page;
pub mod comment_row; pub mod comment_row;
pub mod voting_row; pub mod voting_row;
pub mod inbox_page;
pub mod mention_row;

View File

@ -5,7 +5,7 @@ pub mod util;
pub mod dialogs; pub mod dialogs;
use api::{user::default_person, community::default_community, post::default_post}; 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 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 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}}; use relm4::{prelude::*, factory::FactoryVecDeque, set_global_css, actions::{RelmAction, RelmActionGroup}};
@ -22,7 +22,8 @@ enum AppState {
Person, Person,
Post, Post,
Login, Login,
Message Message,
Inbox
} }
struct App { struct App {
@ -33,7 +34,8 @@ struct App {
communities: FactoryVecDeque<CommunityRow>, communities: FactoryVecDeque<CommunityRow>,
profile_page: Controller<ProfilePage>, profile_page: Controller<ProfilePage>,
community_page: Controller<CommunityPage>, community_page: Controller<CommunityPage>,
post_page: Controller<PostPage> post_page: Controller<PostPage>,
inbox_page: Controller<InboxPage>
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -54,7 +56,8 @@ pub enum AppMsg {
OpenPerson(String), OpenPerson(String),
DoneFetchPerson(GetPersonDetailsResponse), DoneFetchPerson(GetPersonDetailsResponse),
OpenPost(PostId), OpenPost(PostId),
DoneFetchPost(GetPostResponse) DoneFetchPost(GetPostResponse),
OpenInbox
} }
#[relm4::component] #[relm4::component]
@ -83,6 +86,10 @@ impl SimpleComponent for App {
set_label: "Subscribed", set_label: "Subscribed",
connect_clicked => AppMsg::StartFetchPosts(Some(ListingType::Subscribed)), connect_clicked => AppMsg::StartFetchPosts(Some(ListingType::Subscribed)),
}, },
pack_start = &gtk::Button {
set_label: "Inbox",
connect_clicked => AppMsg::OpenInbox,
},
pack_start = &gtk::Button { pack_start = &gtk::Button {
set_label: "Communities", set_label: "Communities",
connect_clicked => AppMsg::ViewCommunities(None), 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 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 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 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 // fetch posts if that's the initial page
if !instance_url.is_empty() { sender.input(AppMsg::StartFetchPosts(None)) }; 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 profile_page = model.profile_page.widget();
let community_page = model.community_page.widget(); let community_page = model.community_page.widget();
let post_page = model.post_page.widget(); let post_page = model.post_page.widget();
let inbox_page = model.inbox_page.widget();
let widgets = view_output!(); let widgets = view_output!();
@ -447,6 +462,9 @@ impl SimpleComponent for App {
AppMsg::Retry => { AppMsg::Retry => {
sender.input(self.latest_action.clone().unwrap_or(AppMsg::StartFetchPosts(None))); sender.input(self.latest_action.clone().unwrap_or(AppMsg::StartFetchPosts(None)));
} }
AppMsg::OpenInbox => {
self.state = AppState::Inbox;
}
} }
} }
} }