Reformat project and change package id

This commit is contained in:
Bnyro 2023-06-26 10:58:21 +02:00
parent d71bbcd3f3
commit fbf4b8dfa5
31 changed files with 644 additions and 284 deletions

4
.gitignore vendored
View File

@ -1,3 +1,7 @@
/target /target
/.cargo /.cargo
/_build /_build
/builddir
/.flatpak
/.flatpak-builder
/.vscode

15
.vscode/settings.json vendored
View File

@ -1,3 +1,16 @@
{ {
"C_Cpp.default.compileCommands": "builddir/vscode_compile_commands.json" "C_Cpp.default.compileCommands": "builddir/vscode_compile_commands.json",
"files.watcherExclude": {
"**/.dart_tool": true,
".flatpak/**": true,
"_build/**": true
},
"mesonbuild.configureOnOpen": false,
"mesonbuild.buildFolder": "_build",
"mesonbuild.mesonPath": "${workspaceFolder}/.flatpak/meson.sh",
"rust-analyzer.server.path": "${workspaceFolder}/.flatpak/rust-analyzer.sh",
"rust-analyzer.runnables.command": "${workspaceFolder}/.flatpak/cargo.sh",
"rust-analyzer.files.excludeDirs": [
".flatpak"
]
} }

View File

@ -1,5 +1,5 @@
{ {
"id": "com.lemmy-gtk.lemoa", "id": "com.lemmygtk.lemoa",
"runtime": "org.gnome.Platform", "runtime": "org.gnome.Platform",
"runtime-version": "44", "runtime-version": "44",
"sdk": "org.gnome.Sdk", "sdk": "org.gnome.Sdk",

View File

@ -8,7 +8,7 @@ project(
gnome = import('gnome') gnome = import('gnome')
application_id = 'com.lemmy-gtk.lemoa' application_id = 'com.lemmygtk.lemoa'
dependency('glib-2.0', version: '>= 2.70') dependency('glib-2.0', version: '>= 2.70')
dependency('gio-2.0', version: '>= 2.70') dependency('gio-2.0', version: '>= 2.70')

View File

@ -1,6 +1,10 @@
use lemmy_api_common::{person::Login, sensitive::Sensitive}; use lemmy_api_common::{person::Login, sensitive::Sensitive};
pub fn login(username_or_email: String, password: String, totp_token: Option<String>) -> std::result::Result<lemmy_api_common::person::LoginResponse, reqwest::Error> { pub fn login(
username_or_email: String,
password: String,
totp_token: Option<String>,
) -> std::result::Result<lemmy_api_common::person::LoginResponse, reqwest::Error> {
let params = Login { let params = Login {
username_or_email: Sensitive::new(username_or_email), username_or_email: Sensitive::new(username_or_email),
password: Sensitive::new(password), password: Sensitive::new(password),

View File

@ -1,4 +1,7 @@
use lemmy_api_common::{comment::{CommentResponse, CreateComment, CreateCommentLike, DeleteComment, EditComment}, lemmy_db_schema::newtypes::{PostId, CommentId}}; use lemmy_api_common::{
comment::{CommentResponse, CreateComment, CreateCommentLike, DeleteComment, EditComment},
lemmy_db_schema::newtypes::{CommentId, PostId},
};
use crate::settings; use crate::settings;
@ -27,10 +30,7 @@ pub fn like_comment(comment_id: CommentId, score: i16) -> Result<CommentResponse
super::post("/comment/like", &params) super::post("/comment/like", &params)
} }
pub fn edit_comment( pub fn edit_comment(body: String, comment_id: i32) -> Result<CommentResponse, reqwest::Error> {
body: String,
comment_id: i32
) -> Result<CommentResponse, reqwest::Error> {
let params = EditComment { let params = EditComment {
content: Some(body), content: Some(body),
comment_id: CommentId(comment_id), comment_id: CommentId(comment_id),

View File

@ -1,9 +1,17 @@
use lemmy_api_common::{community::{ListCommunities, ListCommunitiesResponse}, lemmy_db_schema::{SortType, SearchType, ListingType}, lemmy_db_views_actor::structs::CommunityView}; use lemmy_api_common::{
community::{ListCommunities, ListCommunitiesResponse},
lemmy_db_schema::{ListingType, SearchType, SortType},
lemmy_db_views_actor::structs::CommunityView,
};
use crate::settings;
use super::search; use super::search;
use crate::settings;
pub fn fetch_communities(page: i64, query: Option<String>,listing_type: Option<ListingType>) -> std::result::Result<Vec<CommunityView>, reqwest::Error> { pub fn fetch_communities(
page: i64,
query: Option<String>,
listing_type: Option<ListingType>,
) -> std::result::Result<Vec<CommunityView>, reqwest::Error> {
if query.is_none() || query.clone().unwrap().trim().is_empty() { if query.is_none() || query.clone().unwrap().trim().is_empty() {
let params = ListCommunities { let params = ListCommunities {
type_: listing_type, type_: listing_type,

View File

@ -1,4 +1,7 @@
use lemmy_api_common::{community::{GetCommunity, GetCommunityResponse, CommunityResponse, FollowCommunity}, lemmy_db_schema::newtypes::CommunityId}; use lemmy_api_common::{
community::{CommunityResponse, FollowCommunity, GetCommunity, GetCommunityResponse},
lemmy_db_schema::newtypes::CommunityId,
};
use crate::settings; use crate::settings;

View File

@ -1,9 +1,9 @@
use reqwest::blocking::multipart::Part;
use std::io::Read;
use std::fs::File;
use crate::settings; use crate::settings;
use serde::Deserialize;
use rand::distributions::{Alphanumeric, DistString}; use rand::distributions::{Alphanumeric, DistString};
use reqwest::blocking::multipart::Part;
use serde::Deserialize;
use std::fs::File;
use std::io::Read;
use super::CLIENT; use super::CLIENT;
@ -21,9 +21,7 @@ struct UploadImageFile {
pub delete_token: String, pub delete_token: String,
} }
pub fn upload_image( pub fn upload_image(image: std::path::PathBuf) -> Result<String, reqwest::Error> {
image: std::path::PathBuf,
) -> Result<String, reqwest::Error> {
let mime_type = mime_guess::from_path(image.clone()).first(); let mime_type = mime_guess::from_path(image.clone()).first();
let file_name = Alphanumeric.sample_string(&mut rand::thread_rng(), 16); let file_name = Alphanumeric.sample_string(&mut rand::thread_rng(), 16);
@ -31,14 +29,19 @@ pub fn upload_image(
let mut data = Vec::new(); let mut data = Vec::new();
file.read_to_end(&mut data).unwrap(); file.read_to_end(&mut data).unwrap();
let part = Part::bytes(data).file_name(file_name).mime_str(&mime_type.unwrap().essence_str())?; let part = Part::bytes(data)
.file_name(file_name)
.mime_str(&mime_type.unwrap().essence_str())?;
let form = reqwest::blocking::multipart::Form::new().part("images[]", part); let form = reqwest::blocking::multipart::Form::new().part("images[]", part);
let account = settings::get_current_account(); let account = settings::get_current_account();
let base_url = account.instance_url; let base_url = account.instance_url;
let path = format!("{}/pictrs/image", base_url); let path = format!("{}/pictrs/image", base_url);
let res: UploadImageResponse = CLIENT let res: UploadImageResponse = CLIENT
.post(&path) .post(&path)
.header("cookie", format!("jwt={}", account.jwt.unwrap().to_string())) .header(
"cookie",
format!("jwt={}", account.jwt.unwrap().to_string()),
)
.multipart(form) .multipart(form)
.send()? .send()?
.json()?; .json()?;

View File

@ -2,26 +2,24 @@ use serde::{de::DeserializeOwned, Serialize};
use crate::settings::get_current_account; use crate::settings::get_current_account;
pub mod auth;
pub mod comment;
pub mod communities; pub mod communities;
pub mod community; pub mod community;
pub mod image;
pub mod moderation;
pub mod post; pub mod post;
pub mod posts; pub mod posts;
pub mod search; pub mod search;
pub mod user;
pub mod auth;
pub mod moderation;
pub mod comment;
pub mod site; pub mod site;
pub mod image; pub mod user;
static API_VERSION: &str = "v3"; static API_VERSION: &str = "v3";
use reqwest::blocking::Client;
use relm4::once_cell::sync::Lazy; use relm4::once_cell::sync::Lazy;
use reqwest::blocking::Client;
pub static CLIENT: Lazy<Client> = Lazy::new(|| { pub static CLIENT: Lazy<Client> = Lazy::new(|| Client::new());
Client::new()
});
fn get_api_url() -> String { fn get_api_url() -> String {
format!("{}/api/{}", get_current_account().instance_url, API_VERSION).to_string() format!("{}/api/{}", get_current_account().instance_url, API_VERSION).to_string()
@ -36,11 +34,7 @@ where
T: DeserializeOwned, T: DeserializeOwned,
Params: Serialize + std::fmt::Debug, Params: Serialize + std::fmt::Debug,
{ {
CLIENT CLIENT.get(&get_url(path)).query(&params).send()?.json()
.get(&get_url(path))
.query(&params)
.send()?
.json()
} }
fn post<T, Params>(path: &str, params: &Params) -> Result<T, reqwest::Error> fn post<T, Params>(path: &str, params: &Params) -> Result<T, reqwest::Error>
@ -48,11 +42,7 @@ where
T: DeserializeOwned, T: DeserializeOwned,
Params: Serialize + std::fmt::Debug, Params: Serialize + std::fmt::Debug,
{ {
CLIENT CLIENT.post(&get_url(path)).json(&params).send()?.json()
.post(&get_url(path))
.json(&params)
.send()?
.json()
} }
fn put<T, Params>(path: &str, params: &Params) -> Result<T, reqwest::Error> fn put<T, Params>(path: &str, params: &Params) -> Result<T, reqwest::Error>
@ -60,9 +50,5 @@ where
T: DeserializeOwned, T: DeserializeOwned,
Params: Serialize + std::fmt::Debug, Params: Serialize + std::fmt::Debug,
{ {
CLIENT CLIENT.put(&get_url(path)).json(&params).send()?.json()
.put(&get_url(path))
.json(&params)
.send()?
.json()
} }

View File

@ -1,4 +1,9 @@
use lemmy_api_common::{lemmy_db_schema::newtypes::{CommentId, PostId}, comment::{RemoveComment, CommentResponse}, sensitive::Sensitive, post::{PostResponse, RemovePost}}; use lemmy_api_common::{
comment::{CommentResponse, RemoveComment},
lemmy_db_schema::newtypes::{CommentId, PostId},
post::{PostResponse, RemovePost},
sensitive::Sensitive,
};
pub fn remove_post( pub fn remove_post(
post_id: i32, post_id: i32,

View File

@ -1,4 +1,14 @@
use lemmy_api_common::{post::{GetPost, GetPostResponse, PostResponse, CreatePost, CreatePostLike, DeletePost, EditPost}, lemmy_db_schema::{newtypes::{PostId, CommunityId}, CommentSortType, ListingType}, comment::{GetComments, GetCommentsResponse}, lemmy_db_views::structs::CommentView}; use lemmy_api_common::{
comment::{GetComments, GetCommentsResponse},
lemmy_db_schema::{
newtypes::{CommunityId, PostId},
CommentSortType, ListingType,
},
lemmy_db_views::structs::CommentView,
post::{
CreatePost, CreatePostLike, DeletePost, EditPost, GetPost, GetPostResponse, PostResponse,
},
};
use crate::settings; use crate::settings;
@ -6,7 +16,7 @@ pub fn get_post(id: PostId) -> Result<GetPostResponse, reqwest::Error> {
let params = GetPost { let params = GetPost {
id: Some(id), id: Some(id),
comment_id: None, comment_id: None,
auth: settings::get_current_account().jwt auth: settings::get_current_account().jwt,
}; };
super::get("/post", &params) super::get("/post", &params)
@ -54,7 +64,7 @@ pub fn edit_post(
name: String, name: String,
url: Option<reqwest::Url>, url: Option<reqwest::Url>,
body: String, body: String,
post_id: i32 post_id: i32,
) -> Result<PostResponse, reqwest::Error> { ) -> Result<PostResponse, reqwest::Error> {
let params = EditPost { let params = EditPost {
name: Some(name), name: Some(name),

View File

@ -1,8 +1,16 @@
use lemmy_api_common::{post::{GetPostsResponse, GetPosts}, lemmy_db_views::structs::PostView, lemmy_db_schema::ListingType}; use lemmy_api_common::{
lemmy_db_schema::ListingType,
lemmy_db_views::structs::PostView,
post::{GetPosts, GetPostsResponse},
};
use crate::settings; use crate::settings;
pub fn list_posts(page: i64, community_name: Option<String>, listing_type: Option<ListingType>) -> std::result::Result<Vec<PostView>, reqwest::Error> { pub fn list_posts(
page: i64,
community_name: Option<String>,
listing_type: Option<ListingType>,
) -> std::result::Result<Vec<PostView>, reqwest::Error> {
let params = GetPosts { let params = GetPosts {
page: Some(page), page: Some(page),
type_: listing_type, type_: listing_type,

View File

@ -1,8 +1,15 @@
use lemmy_api_common::{site::{SearchResponse, Search}, lemmy_db_schema::{SortType, SearchType}}; use lemmy_api_common::{
lemmy_db_schema::{SearchType, SortType},
site::{Search, SearchResponse},
};
use crate::settings; use crate::settings;
pub fn fetch_search(page: i64, query: String, search_type: Option<SearchType>) -> std::result::Result<SearchResponse, reqwest::Error> { pub fn fetch_search(
page: i64,
query: String,
search_type: Option<SearchType>,
) -> std::result::Result<SearchResponse, reqwest::Error> {
let params = Search { let params = Search {
q: query, q: query,
sort: Some(SortType::TopMonth), sort: Some(SortType::TopMonth),

View File

@ -1,4 +1,4 @@
use lemmy_api_common::site::{GetSiteResponse, GetSite}; use lemmy_api_common::site::{GetSite, GetSiteResponse};
use crate::settings; use crate::settings;

View File

@ -1,8 +1,17 @@
use lemmy_api_common::{person::{GetPersonDetailsResponse, GetPersonDetails, GetPersonMentionsResponse, GetRepliesResponse, MarkAllAsRead, GetReplies, GetPersonMentions}, lemmy_db_schema::{CommentSortType, newtypes::PersonId}}; use lemmy_api_common::{
lemmy_db_schema::{newtypes::PersonId, CommentSortType},
person::{
GetPersonDetails, GetPersonDetailsResponse, GetPersonMentions, GetPersonMentionsResponse,
GetReplies, GetRepliesResponse, MarkAllAsRead,
},
};
use crate::settings; use crate::settings;
pub fn get_user(id: PersonId, page: i64) -> std::result::Result<GetPersonDetailsResponse, reqwest::Error> { pub fn get_user(
id: PersonId,
page: i64,
) -> std::result::Result<GetPersonDetailsResponse, reqwest::Error> {
let params = GetPersonDetails { let params = GetPersonDetails {
page: Some(page), page: Some(page),
person_id: Some(id), person_id: Some(id),
@ -17,7 +26,10 @@ 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> { pub fn get_mentions(
page: i64,
unread_only: bool,
) -> std::result::Result<GetPersonMentionsResponse, reqwest::Error> {
let params = GetPersonMentions { let params = GetPersonMentions {
auth: settings::get_current_account().jwt.unwrap(), auth: settings::get_current_account().jwt.unwrap(),
unread_only: Some(unread_only), unread_only: Some(unread_only),
@ -28,7 +40,10 @@ pub fn get_mentions(page: i64, unread_only: bool) -> std::result::Result<GetPers
super::get("/user/mention", &params) super::get("/user/mention", &params)
} }
pub fn get_replies(page: i64, unread_only: bool) -> std::result::Result<GetRepliesResponse, reqwest::Error> { pub fn get_replies(
page: i64,
unread_only: bool,
) -> std::result::Result<GetRepliesResponse, reqwest::Error> {
let params = GetReplies { let params = GetReplies {
auth: settings::get_current_account().jwt.unwrap(), auth: settings::get_current_account().jwt.unwrap(),
page: Some(page), page: Some(page),

View File

@ -1,13 +1,13 @@
use gtk::prelude::*;
use lemmy_api_common::lemmy_db_views::structs::CommentView; use lemmy_api_common::lemmy_db_views::structs::CommentView;
use relm4::prelude::*; use relm4::prelude::*;
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::dialogs::editor::EditorData;
use crate::settings;
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 super::post_page::PostInput; use super::post_page::PostInput;
use super::voting_row::VotingRowModel; use super::voting_row::VotingRowModel;
@ -17,14 +17,14 @@ use super::voting_row::VotingStats;
pub struct CommentRow { pub struct CommentRow {
pub comment: CommentView, pub comment: CommentView,
avatar: Controller<WebImage>, avatar: Controller<WebImage>,
voting_row: Controller<VotingRowModel> voting_row: Controller<VotingRowModel>,
} }
#[derive(Debug)] #[derive(Debug)]
pub enum CommentRowMsg { pub enum CommentRowMsg {
OpenPerson, OpenPerson,
DeleteComment, DeleteComment,
OpenEditCommentDialog OpenEditCommentDialog,
} }
#[relm4::factory(pub)] #[relm4::factory(pub)]
@ -109,10 +109,21 @@ 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.creator.avatar.clone())).detach(); let avatar = WebImage::builder()
let voting_row = VotingRowModel::builder().launch(VotingStats::from_comment(value.counts.clone(), value.my_vote)).detach(); .launch(get_web_image_url(value.creator.avatar.clone()))
.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,
}
} }
fn init_widgets( fn init_widgets(
@ -131,13 +142,17 @@ 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(PostInput::PassAppMessage(crate::AppMsg::OpenPerson(self.comment.creator.id.clone()))); sender.output(PostInput::PassAppMessage(crate::AppMsg::OpenPerson(
self.comment.creator.id.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(PostInput::PassAppMessage(crate::AppMsg::StartFetchPosts(None, true))); let _ = sender.output(PostInput::PassAppMessage(
crate::AppMsg::StartFetchPosts(None, true),
));
}); });
} }
CommentRowMsg::OpenEditCommentDialog => { CommentRowMsg::OpenEditCommentDialog => {

View File

@ -1,10 +1,16 @@
use crate::{util::markdown_to_pango_markup, dialogs::editor::{EditorDialog, DialogMsg, EditorOutput, EditorType, EditorData}}; use crate::{
use lemmy_api_common::{lemmy_db_views::structs::PostView, lemmy_db_views_actor::structs::CommunityView, lemmy_db_schema::SubscribedType}; dialogs::editor::{DialogMsg, EditorData, EditorDialog, EditorOutput, EditorType},
use relm4::{prelude::*, factory::FactoryVecDeque, MessageBroker}; util::markdown_to_pango_markup,
};
use gtk::prelude::*; use gtk::prelude::*;
use lemmy_api_common::{
lemmy_db_schema::SubscribedType, lemmy_db_views::structs::PostView,
lemmy_db_views_actor::structs::CommunityView,
};
use relm4::{factory::FactoryVecDeque, prelude::*, MessageBroker};
use relm4_components::web_image::WebImage; use relm4_components::web_image::WebImage;
use crate::{api, util::get_web_image_msg, settings}; use crate::{api, settings, util::get_web_image_msg};
use super::post_row::PostRow; use super::post_row::PostRow;
@ -16,7 +22,7 @@ pub struct CommunityPage {
posts: FactoryVecDeque<PostRow>, posts: FactoryVecDeque<PostRow>,
#[allow(dead_code)] #[allow(dead_code)]
create_post_dialog: Controller<EditorDialog>, create_post_dialog: Controller<EditorDialog>,
current_posts_page: i64 current_posts_page: i64,
} }
#[derive(Debug)] #[derive(Debug)]
@ -29,7 +35,7 @@ pub enum CommunityInput {
CreatedPost(PostView), CreatedPost(PostView),
ToggleSubscription, ToggleSubscription,
UpdateSubscriptionState(SubscribedType), UpdateSubscriptionState(SubscribedType),
None None,
} }
#[relm4::component(pub)] #[relm4::component(pub)]
@ -139,10 +145,16 @@ impl SimpleComponent for CommunityPage {
.launch_with_broker(EditorType::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,
}); });
let model = CommunityPage { info: init, avatar, posts, create_post_dialog: dialog, current_posts_page: 0 }; let model = CommunityPage {
info: init,
avatar,
posts,
create_post_dialog: dialog,
current_posts_page: 0,
};
let avatar = model.avatar.widget(); let avatar = model.avatar.widget();
let posts = model.posts.widget(); let posts = model.posts.widget();
let widgets = view_output!(); let widgets = view_output!();
@ -154,10 +166,13 @@ impl SimpleComponent for CommunityPage {
match message { match message {
CommunityInput::UpdateCommunity(community) => { CommunityInput::UpdateCommunity(community) => {
self.info = community.clone(); self.info = community.clone();
self.avatar.emit(get_web_image_msg(community.community.icon)); self.avatar
.emit(get_web_image_msg(community.community.icon));
self.posts.guard().clear(); self.posts.guard().clear();
self.current_posts_page = 0; self.current_posts_page = 0;
if community.counts.posts == 0 { return; } if community.counts.posts == 0 {
return;
}
sender.input(CommunityInput::FetchPosts); sender.input(CommunityInput::FetchPosts);
} }
CommunityInput::FetchPosts => { CommunityInput::FetchPosts => {
@ -176,9 +191,7 @@ impl SimpleComponent for CommunityPage {
self.posts.guard().push_back(post); self.posts.guard().push_back(post);
} }
} }
CommunityInput::OpenCreatePostDialog => { CommunityInput::OpenCreatePostDialog => COMMUNITY_PAGE_BROKER.send(DialogMsg::Show),
COMMUNITY_PAGE_BROKER.send(DialogMsg::Show)
}
CommunityInput::CreatedPost(post) => { CommunityInput::CreatedPost(post) => {
self.posts.guard().push_front(post); self.posts.guard().push_front(post);
} }
@ -187,23 +200,35 @@ impl SimpleComponent for CommunityPage {
std::thread::spawn(move || { std::thread::spawn(move || {
let message = match api::post::create_post(post.name, post.body, post.url, id) { let message = match api::post::create_post(post.name, post.body, post.url, id) {
Ok(post) => Some(CommunityInput::CreatedPost(post.post_view)), Ok(post) => Some(CommunityInput::CreatedPost(post.post_view)),
Err(err) => { println!("{}", err.to_string()); None } Err(err) => {
println!("{}", err.to_string());
None
}
};
if let Some(message) = message {
sender.input(message)
}; };
if let Some(message) = message { sender.input(message) };
}); });
} }
CommunityInput::ToggleSubscription => { CommunityInput::ToggleSubscription => {
let community_id = self.info.community.id.0; let community_id = self.info.community.id.0;
let new_state = match self.info.subscribed { let new_state = match self.info.subscribed {
SubscribedType::NotSubscribed => true, SubscribedType::NotSubscribed => true,
_ => false _ => false,
}; };
std::thread::spawn(move || { std::thread::spawn(move || {
let message = match api::community::follow_community(community_id, new_state) { let message = match api::community::follow_community(community_id, new_state) {
Ok(community) => Some(CommunityInput::UpdateSubscriptionState(community.community_view.subscribed)), Ok(community) => Some(CommunityInput::UpdateSubscriptionState(
Err(err) => { println!("{}", err.to_string()); None } community.community_view.subscribed,
)),
Err(err) => {
println!("{}", err.to_string());
None
}
};
if message.is_some() {
sender.input(message.unwrap())
}; };
if message.is_some() { sender.input(message.unwrap()) };
}); });
} }
CommunityInput::UpdateSubscriptionState(state) => { CommunityInput::UpdateSubscriptionState(state) => {

View File

@ -1,6 +1,6 @@
use gtk::prelude::*;
use lemmy_api_common::lemmy_db_views_actor::structs::CommunityView; use lemmy_api_common::lemmy_db_views_actor::structs::CommunityView;
use relm4::prelude::*; use relm4::prelude::*;
use gtk::prelude::*;
use relm4_components::web_image::WebImage; use relm4_components::web_image::WebImage;
use crate::util::get_web_image_url; use crate::util::get_web_image_url;
@ -75,9 +75,14 @@ impl FactoryComponent for CommunityRow {
} }
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 community_image= WebImage::builder().launch(get_web_image_url(value.community.clone().icon)).detach(); let community_image = WebImage::builder()
.launch(get_web_image_url(value.community.clone().icon))
.detach();
Self { community: value, community_image } Self {
community: value,
community_image,
}
} }
fn init_widgets( fn init_widgets(
@ -94,9 +99,9 @@ impl FactoryComponent for CommunityRow {
fn update(&mut self, message: Self::Input, sender: FactorySender<Self>) { fn update(&mut self, message: Self::Input, sender: FactorySender<Self>) {
match message { match message {
CommunityRowMsg::OpenCommunity => { CommunityRowMsg::OpenCommunity => sender.output(crate::AppMsg::OpenCommunity(
sender.output(crate::AppMsg::OpenCommunity(self.community.community.id.clone())) self.community.community.id.clone(),
} )),
} }
} }
} }

View File

@ -1,6 +1,6 @@
use lemmy_api_common::lemmy_db_views_actor::structs::CommentReplyView;
use relm4::{prelude::*, factory::FactoryVecDeque};
use gtk::prelude::*; use gtk::prelude::*;
use lemmy_api_common::lemmy_db_views_actor::structs::CommentReplyView;
use relm4::{factory::FactoryVecDeque, prelude::*};
use crate::api; use crate::api;
@ -16,7 +16,7 @@ pub struct InboxPage {
mentions: FactoryVecDeque<MentionRow>, mentions: FactoryVecDeque<MentionRow>,
page: i64, page: i64,
unread_only: bool, unread_only: bool,
type_: InboxType type_: InboxType,
} }
#[derive(Debug)] #[derive(Debug)]
@ -75,7 +75,12 @@ impl SimpleComponent for InboxPage {
sender: ComponentSender<Self>, sender: ComponentSender<Self>,
) -> ComponentParts<Self> { ) -> ComponentParts<Self> {
let mentions = FactoryVecDeque::new(gtk::Box::default(), sender.output_sender()); let mentions = FactoryVecDeque::new(gtk::Box::default(), sender.output_sender());
let model = Self { mentions, page: 1, unread_only: false, type_: InboxType::Mentions }; let model = Self {
mentions,
page: 1,
unread_only: false,
type_: InboxType::Mentions,
};
let mentions = model.mentions.widget(); let mentions = model.mentions.widget();
let widgets = view_output!(); let widgets = view_output!();
ComponentParts { model, widgets } ComponentParts { model, widgets }
@ -94,12 +99,16 @@ impl SimpleComponent for InboxPage {
// It's just a different object, but its contents are exactly the same // It's just a different object, but its contents are exactly the same
let serialised = serde_json::to_string(&response.mentions).unwrap(); let serialised = serde_json::to_string(&response.mentions).unwrap();
serde_json::from_str(&serialised).ok() serde_json::from_str(&serialised).ok()
} else { None } } else {
None
}
} }
InboxType::Replies => { InboxType::Replies => {
if let Ok(response) = api::user::get_replies(page, unread_only) { if let Ok(response) = api::user::get_replies(page, unread_only) {
Some(response.replies) Some(response.replies)
} else { None } } else {
None
}
} }
}; };
if let Some(comments) = comments { if let Some(comments) = comments {

View File

@ -1,6 +1,6 @@
use gtk::prelude::*;
use lemmy_api_common::lemmy_db_views_actor::structs::CommentReplyView; use lemmy_api_common::lemmy_db_views_actor::structs::CommentReplyView;
use relm4::prelude::*; use relm4::prelude::*;
use gtk::prelude::*;
use relm4_components::web_image::WebImage; use relm4_components::web_image::WebImage;
use crate::util::get_web_image_url; use crate::util::get_web_image_url;
@ -14,7 +14,7 @@ pub struct MentionRow {
comment: CommentReplyView, comment: CommentReplyView,
creator_image: Controller<WebImage>, creator_image: Controller<WebImage>,
community_image: Controller<WebImage>, community_image: Controller<WebImage>,
voting_row: Controller<VotingRowModel> voting_row: Controller<VotingRowModel>,
} }
#[derive(Debug)] #[derive(Debug)]
@ -114,11 +114,25 @@ impl FactoryComponent for MentionRow {
} }
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 creator_image = WebImage::builder().launch(get_web_image_url(value.creator.avatar.clone())).detach(); let creator_image = WebImage::builder()
let community_image = WebImage::builder().launch(get_web_image_url(value.community.icon.clone())).detach(); .launch(get_web_image_url(value.creator.avatar.clone()))
let voting_row = VotingRowModel::builder().launch(VotingStats::from_comment(value.counts.clone(), value.my_vote)).detach(); .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 } Self {
comment: value,
creator_image,
community_image,
voting_row,
}
} }
fn init_widgets( fn init_widgets(
@ -144,7 +158,9 @@ impl FactoryComponent for MentionRow {
sender.output(crate::AppMsg::OpenPost(self.comment.post.id.clone())); sender.output(crate::AppMsg::OpenPost(self.comment.post.id.clone()));
} }
MentionRowMsg::OpenCommunity => { MentionRowMsg::OpenCommunity => {
sender.output(crate::AppMsg::OpenCommunity(self.comment.community.id.clone())); sender.output(crate::AppMsg::OpenCommunity(
self.comment.community.id.clone(),
));
} }
} }
} }

View File

@ -1,9 +1,9 @@
pub mod post_row;
pub mod community_row;
pub mod profile_page;
pub mod community_page;
pub mod post_page;
pub mod comment_row; pub mod comment_row;
pub mod voting_row; pub mod community_page;
pub mod community_row;
pub mod inbox_page; pub mod inbox_page;
pub mod mention_row; pub mod mention_row;
pub mod post_page;
pub mod post_row;
pub mod profile_page;
pub mod voting_row;

View File

@ -1,11 +1,22 @@
use lemmy_api_common::{lemmy_db_views::structs::{CommentView, PostView}, post::GetPostResponse};
use relm4::{prelude::*, factory::FactoryVecDeque, MessageBroker};
use gtk::prelude::*; use gtk::prelude::*;
use lemmy_api_common::{
lemmy_db_views::structs::{CommentView, PostView},
post::GetPostResponse,
};
use relm4::{factory::FactoryVecDeque, prelude::*, MessageBroker};
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, EditorType, EditorData}, settings}; use crate::{
api,
dialogs::editor::{DialogMsg, EditorData, EditorDialog, EditorOutput, EditorType},
settings,
util::{get_web_image_msg, get_web_image_url, markdown_to_pango_markup},
};
use super::{comment_row::CommentRow, voting_row::{VotingRowModel, VotingStats, VotingRowInput}}; use super::{
comment_row::CommentRow,
voting_row::{VotingRowInput, VotingRowModel, VotingStats},
};
pub static POST_PAGE_BROKER: MessageBroker<DialogMsg> = MessageBroker::new(); pub static POST_PAGE_BROKER: MessageBroker<DialogMsg> = MessageBroker::new();
@ -17,7 +28,7 @@ pub struct PostPage {
comments: FactoryVecDeque<CommentRow>, comments: FactoryVecDeque<CommentRow>,
#[allow(dead_code)] #[allow(dead_code)]
create_comment_dialog: Controller<EditorDialog>, create_comment_dialog: Controller<EditorDialog>,
voting_row: Controller<VotingRowModel> voting_row: Controller<VotingRowModel>,
} }
#[derive(Debug)] #[derive(Debug)]
@ -200,11 +211,21 @@ impl SimpleComponent for PostPage {
EditorOutput::CreateRequest(comment, _) => PostInput::CreateCommentRequest(comment), EditorOutput::CreateRequest(comment, _) => PostInput::CreateCommentRequest(comment),
EditorOutput::EditRequest(post, type_) => match type_ { EditorOutput::EditRequest(post, type_) => match type_ {
EditorType::Post => PostInput::EditPostRequest(post), EditorType::Post => PostInput::EditPostRequest(post),
EditorType::Comment => PostInput::EditCommentRequest(post) EditorType::Comment => PostInput::EditCommentRequest(post),
} },
}); });
let voting_row = VotingRowModel::builder().launch(VotingStats::default()).detach(); let voting_row = VotingRowModel::builder()
let model = PostPage { info: init, image, comments, creator_avatar, community_avatar, create_comment_dialog: dialog, voting_row }; .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 image = model.image.widget();
let comments = model.comments.widget(); let comments = model.comments.widget();
@ -221,15 +242,24 @@ impl SimpleComponent for PostPage {
PostInput::UpdatePost(post) => { PostInput::UpdatePost(post) => {
self.info = post.clone(); self.info = post.clone();
self.image.emit(get_web_image_msg(post.post_view.post.thumbnail_url)); self.image
self.community_avatar.emit(get_web_image_msg(post.community_view.community.icon)); .emit(get_web_image_msg(post.post_view.post.thumbnail_url));
self.creator_avatar.emit(get_web_image_msg(post.post_view.creator.avatar)); 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.voting_row
.emit(VotingRowInput::UpdateStats(VotingStats::from_post(
post.post_view.counts.clone(),
post.post_view.my_vote,
)));
self.comments.guard().clear(); self.comments.guard().clear();
std::thread::spawn(move || { std::thread::spawn(move || {
if post.post_view.counts.comments == 0 { return; } if post.post_view.counts.comments == 0 {
return;
}
let comments = api::post::get_comments(post.post_view.post.id); let comments = api::post::get_comments(post.post_view.post.id);
if let Ok(comments) = comments { if let Ok(comments) = comments {
sender.input(PostInput::DoneFetchComments(comments)); sender.input(PostInput::DoneFetchComments(comments));
@ -258,7 +288,9 @@ impl SimpleComponent for PostPage {
if link.is_empty() { if link.is_empty() {
link = get_web_image_url(post.embed_video_url); link = get_web_image_url(post.embed_video_url);
} }
if link.is_empty() { return; } if link.is_empty() {
return;
}
gtk::show_uri(None::<&relm4::gtk::Window>, &link, 0); gtk::show_uri(None::<&relm4::gtk::Window>, &link, 0);
} }
PostInput::OpenCreateCommentDialog => { PostInput::OpenCreateCommentDialog => {
@ -273,9 +305,14 @@ impl SimpleComponent for PostPage {
std::thread::spawn(move || { std::thread::spawn(move || {
let message = match api::comment::create_comment(id, post.body, None) { let message = match api::comment::create_comment(id, post.body, None) {
Ok(comment) => Some(PostInput::CreatedComment(comment.comment_view)), Ok(comment) => Some(PostInput::CreatedComment(comment.comment_view)),
Err(err) => { println!("{}", err.to_string()); None } Err(err) => {
println!("{}", err.to_string());
None
}
};
if let Some(message) = message {
sender.input(message)
}; };
if let Some(message) = message { sender.input(message) };
}); });
} }
PostInput::DeletePost => { PostInput::DeletePost => {
@ -288,11 +325,17 @@ impl SimpleComponent for PostPage {
PostInput::OpenEditPostDialog => { 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(""),
}; };
let data = EditorData { let data = EditorData {
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, id: None,
}; };
@ -305,9 +348,14 @@ impl SimpleComponent for PostPage {
std::thread::spawn(move || { std::thread::spawn(move || {
let message = match api::post::edit_post(post.name, post.url, post.body, id) { let message = match api::post::edit_post(post.name, post.url, post.body, id) {
Ok(post) => Some(PostInput::DoneEditPost(post.post_view)), Ok(post) => Some(PostInput::DoneEditPost(post.post_view)),
Err(err) => { println!("{}", err.to_string()); None } Err(err) => {
println!("{}", err.to_string());
None
}
};
if let Some(message) = message {
sender.input(message)
}; };
if let Some(message) = message { sender.input(message) };
}); });
} }
PostInput::DoneEditPost(post) => { PostInput::DoneEditPost(post) => {
@ -322,9 +370,14 @@ impl SimpleComponent for PostPage {
std::thread::spawn(move || { std::thread::spawn(move || {
let message = match api::comment::edit_comment(data.body, data.id.unwrap()) { let message = match api::comment::edit_comment(data.body, data.id.unwrap()) {
Ok(comment) => Some(PostInput::UpdateComment(comment.comment_view)), Ok(comment) => Some(PostInput::UpdateComment(comment.comment_view)),
Err(err) => { println!("{}", err.to_string()); None } Err(err) => {
println!("{}", err.to_string());
None
}
};
if let Some(message) = message {
sender.input(message)
}; };
if let Some(message) = message { sender.input(message) };
}); });
} }
PostInput::UpdateComment(comment) => { PostInput::UpdateComment(comment) => {

View File

@ -1,10 +1,10 @@
use gtk::prelude::*;
use lemmy_api_common::lemmy_db_views::structs::PostView; use lemmy_api_common::lemmy_db_views::structs::PostView;
use relm4::prelude::*; use relm4::prelude::*;
use gtk::prelude::*;
use relm4_components::web_image::WebImage; use relm4_components::web_image::WebImage;
use crate::{util::get_web_image_url, api};
use crate::settings; use crate::settings;
use crate::{api, util::get_web_image_url};
use super::voting_row::{VotingRowModel, VotingStats}; use super::voting_row::{VotingRowModel, VotingStats};
@ -13,7 +13,7 @@ pub struct PostRow {
post: PostView, post: PostView,
author_image: Controller<WebImage>, author_image: Controller<WebImage>,
community_image: Controller<WebImage>, community_image: Controller<WebImage>,
voting_row: Controller<VotingRowModel> voting_row: Controller<VotingRowModel>,
} }
#[derive(Debug)] #[derive(Debug)]
@ -21,7 +21,7 @@ pub enum PostViewMsg {
OpenPost, OpenPost,
OpenCommunity, OpenCommunity,
OpenPerson, OpenPerson,
DeletePost DeletePost,
} }
#[relm4::factory(pub)] #[relm4::factory(pub)]
@ -123,14 +123,27 @@ 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.avatar.clone())).detach(); let author_image = WebImage::builder()
let community_image= WebImage::builder().launch(get_web_image_url(value.community.icon.clone())).detach(); .launch(get_web_image_url(value.creator.avatar.clone()))
let voting_row = VotingRowModel::builder().launch(VotingStats::from_post(value.counts.clone(), value.my_vote)).detach(); .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.counts.clone(), value.my_vote))
.detach();
Self { post: value, author_image, community_image, voting_row } Self {
post: value,
author_image,
community_image,
voting_row,
}
} }
fn init_widgets( fn init_widgets(

View File

@ -1,6 +1,6 @@
use lemmy_api_common::person::GetPersonDetailsResponse;
use relm4::{prelude::*, factory::FactoryVecDeque};
use gtk::prelude::*; use gtk::prelude::*;
use lemmy_api_common::person::GetPersonDetailsResponse;
use relm4::{factory::FactoryVecDeque, prelude::*};
use relm4_components::web_image::WebImage; use relm4_components::web_image::WebImage;
use crate::util::get_web_image_msg; use crate::util::get_web_image_msg;
@ -11,7 +11,7 @@ use super::post_row::PostRow;
pub struct ProfilePage { pub struct ProfilePage {
info: GetPersonDetailsResponse, info: GetPersonDetailsResponse,
avatar: Controller<WebImage>, avatar: Controller<WebImage>,
posts: FactoryVecDeque<PostRow> posts: FactoryVecDeque<PostRow>,
} }
#[derive(Debug)] #[derive(Debug)]
@ -84,7 +84,11 @@ impl SimpleComponent for ProfilePage {
) -> relm4::ComponentParts<Self> { ) -> relm4::ComponentParts<Self> {
let avatar = WebImage::builder().launch("".to_string()).detach(); let avatar = WebImage::builder().launch("".to_string()).detach();
let posts = FactoryVecDeque::new(gtk::Box::default(), sender.output_sender()); let posts = FactoryVecDeque::new(gtk::Box::default(), sender.output_sender());
let model = ProfilePage { info: init, avatar, posts }; let model = ProfilePage {
info: init,
avatar,
posts,
};
let avatar = model.avatar.widget(); let avatar = model.avatar.widget();
let posts = model.posts.widget(); let posts = model.posts.widget();
let widgets = view_output!(); let widgets = view_output!();
@ -96,7 +100,8 @@ impl SimpleComponent for ProfilePage {
match message { match message {
ProfileInput::UpdatePerson(person) => { ProfileInput::UpdatePerson(person) => {
self.info = person.clone(); self.info = person.clone();
self.avatar.emit(get_web_image_msg(person.person_view.person.avatar)); self.avatar
.emit(get_web_image_msg(person.person_view.person.avatar));
self.posts.guard().clear(); self.posts.guard().clear();
for post in person.posts { for post in person.posts {
self.posts.guard().push_back(post); self.posts.guard().push_back(post);

View File

@ -1,6 +1,9 @@
use lemmy_api_common::{lemmy_db_schema::{aggregates::structs::{PostAggregates, CommentAggregates}, newtypes::{PostId, CommentId}}};
use relm4::{SimpleComponent, ComponentParts, gtk};
use gtk::prelude::*; use gtk::prelude::*;
use lemmy_api_common::lemmy_db_schema::{
aggregates::structs::{CommentAggregates, PostAggregates},
newtypes::{CommentId, PostId},
};
use relm4::{gtk, ComponentParts, SimpleComponent};
use crate::{api, settings}; use crate::{api, settings};
@ -15,7 +18,7 @@ pub struct VotingStats {
#[allow(dead_code)] #[allow(dead_code)]
id: i32, id: i32,
post_id: Option<i32>, post_id: Option<i32>,
comment_id: Option<i32> comment_id: Option<i32>,
} }
impl VotingStats { impl VotingStats {
@ -46,7 +49,7 @@ impl VotingStats {
#[derive(Debug)] #[derive(Debug)]
pub struct VotingRowModel { pub struct VotingRowModel {
stats: VotingStats stats: VotingStats,
} }
#[derive(Debug)] #[derive(Debug)]
@ -56,9 +59,7 @@ pub enum VotingRowInput {
} }
#[derive(Debug)] #[derive(Debug)]
pub enum VotingRowOutput { pub enum VotingRowOutput {}
}
#[relm4::component(pub)] #[relm4::component(pub)]
impl SimpleComponent for VotingRowModel { impl SimpleComponent for VotingRowModel {
@ -105,24 +106,52 @@ impl SimpleComponent for VotingRowModel {
match message { match message {
VotingRowInput::Vote(vote) => { VotingRowInput::Vote(vote) => {
let mut score = self.stats.own_vote.unwrap_or(0) + vote; let mut score = self.stats.own_vote.unwrap_or(0) + vote;
if score < -1 || score > 1 { score = 0 }; if score < -1 || score > 1 {
if settings::get_current_account().jwt.is_none() { return; } score = 0
};
if settings::get_current_account().jwt.is_none() {
return;
}
let stats = self.stats.clone(); let stats = self.stats.clone();
std::thread::spawn(move || { std::thread::spawn(move || {
let info = if stats.post_id.is_some() { let info = if stats.post_id.is_some() {
let response = api::post::like_post(PostId { 0: stats.post_id.unwrap() }, score); let response = api::post::like_post(
PostId {
0: stats.post_id.unwrap(),
},
score,
);
match response { match response {
Ok(post) => Some(VotingStats::from_post(post.post_view.counts, post.post_view.my_vote)), Ok(post) => Some(VotingStats::from_post(
Err(err) => { println!("{}", err.to_string()); None } post.post_view.counts,
post.post_view.my_vote,
)),
Err(err) => {
println!("{}", err.to_string());
None
}
} }
} else { } else {
let response = api::comment::like_comment(CommentId { 0: stats.comment_id.unwrap() }, score); let response = api::comment::like_comment(
CommentId {
0: stats.comment_id.unwrap(),
},
score,
);
match response { match response {
Ok(comment) => Some(VotingStats::from_comment(comment.comment_view.counts, comment.comment_view.my_vote)), Ok(comment) => Some(VotingStats::from_comment(
Err(err) => { println!("{}", err.to_string()); None } 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)) }; if let Some(info) = info {
sender.input(VotingRowInput::UpdateStats(info))
};
}); });
} }
VotingRowInput::UpdateStats(stats) => { VotingRowInput::UpdateStats(stats) => {

View File

@ -1,5 +1,8 @@
use relm4::{prelude::*, gtk::{ResponseType, FileFilter}};
use gtk::prelude::*; use gtk::prelude::*;
use relm4::{
gtk::{FileFilter, ResponseType},
prelude::*,
};
use crate::api; use crate::api;
@ -8,7 +11,7 @@ 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 id: Option<i32>,
} }
pub struct EditorDialog { pub struct EditorDialog {
@ -20,13 +23,13 @@ pub struct EditorDialog {
body_buffer: gtk::TextBuffer, body_buffer: gtk::TextBuffer,
// Optional field to temporarily store the post or comment id // Optional field to temporarily store the post or comment id
id: Option<i32>, id: Option<i32>,
window: gtk::Window window: gtk::Window,
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum EditorType { pub enum EditorType {
Post, Post,
Comment Comment,
} }
#[derive(Debug)] #[derive(Debug)]
@ -38,13 +41,13 @@ pub enum DialogMsg {
Okay, Okay,
ChooseImage, ChooseImage,
UploadImage(std::path::PathBuf), UploadImage(std::path::PathBuf),
AppendBody(String) AppendBody(String),
} }
#[derive(Debug)] #[derive(Debug)]
pub enum EditorOutput { pub enum EditorOutput {
CreateRequest(EditorData, EditorType), CreateRequest(EditorData, EditorType),
EditRequest(EditorData, EditorType) EditRequest(EditorData, EditorType),
} }
#[relm4::component(pub)] #[relm4::component(pub)]
@ -151,7 +154,16 @@ impl SimpleComponent for EditorDialog {
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 window = root.toplevel_window().unwrap(); let window = root.toplevel_window().unwrap();
let model = EditorDialog { type_: init, visible: false, is_new: true, name_buffer, url_buffer, body_buffer, id: None, window }; let model = EditorDialog {
type_: init,
visible: false,
is_new: true,
name_buffer,
url_buffer,
body_buffer,
id: None,
window,
};
let widgets = view_output!(); let widgets = view_output!();
ComponentParts { model, widgets } ComponentParts { model, widgets }
} }
@ -164,17 +176,22 @@ impl SimpleComponent for EditorDialog {
self.url_buffer.set_text(""); self.url_buffer.set_text("");
self.body_buffer.set_text(""); self.body_buffer.set_text("");
self.visible = false; self.visible = false;
}, }
DialogMsg::Okay => { DialogMsg::Okay => {
let name = self.name_buffer.text().to_string(); let name = self.name_buffer.text().to_string();
let url = self.url_buffer.text().to_string(); let url = self.url_buffer.text().to_string();
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, id: self.id }; 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, self.type_), true => EditorOutput::CreateRequest(post, self.type_),
false => EditorOutput::EditRequest(post, self.type_) false => EditorOutput::EditRequest(post, self.type_),
}; };
let _ = sender.output(message); let _ = sender.output(message);
self.visible = false; self.visible = false;
@ -185,12 +202,22 @@ impl SimpleComponent for EditorDialog {
} }
DialogMsg::UpdateData(data) => { DialogMsg::UpdateData(data) => {
self.name_buffer.set_text(data.name); self.name_buffer.set_text(data.name);
if let Some(url) = data.url { self.url_buffer.set_text(url.to_string()); } if let Some(url) = data.url {
self.url_buffer.set_text(url.to_string());
}
self.body_buffer.set_text(&data.body.clone()); self.body_buffer.set_text(&data.body.clone());
} }
DialogMsg::ChooseImage => { DialogMsg::ChooseImage => {
let buttons = [("_Cancel", ResponseType::Cancel), ("_Okay", ResponseType::Accept)]; let buttons = [
let dialog = gtk::FileChooserDialog::new(Some("Upload image"), None::<&gtk::ApplicationWindow>, gtk::FileChooserAction::Open, &buttons); ("_Cancel", ResponseType::Cancel),
("_Okay", ResponseType::Accept),
];
let dialog = gtk::FileChooserDialog::new(
Some("Upload image"),
None::<&gtk::ApplicationWindow>,
gtk::FileChooserAction::Open,
&buttons,
);
dialog.set_transient_for(Some(&self.window)); dialog.set_transient_for(Some(&self.window));
let image_filter = FileFilter::new(); let image_filter = FileFilter::new();
image_filter.add_pattern("image/*"); image_filter.add_pattern("image/*");
@ -200,7 +227,7 @@ impl SimpleComponent for EditorDialog {
ResponseType::Accept => { ResponseType::Accept => {
let path = dialog.file().unwrap().path(); let path = dialog.file().unwrap().path();
sender.input(DialogMsg::UploadImage(path.unwrap())) sender.input(DialogMsg::UploadImage(path.unwrap()))
}, }
_ => dialog.hide(), _ => dialog.hide(),
} }
dialog.destroy(); dialog.destroy();
@ -217,7 +244,8 @@ impl SimpleComponent for EditorDialog {
DialogMsg::AppendBody(new_text) => { DialogMsg::AppendBody(new_text) => {
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();
self.body_buffer.set_text(&format!("{}\n{}", body, new_text)); self.body_buffer
.set_text(&format!("{}\n{}", body, new_text));
} }
} }
} }

View File

@ -1,18 +1,39 @@
pub mod settings;
pub mod api; pub mod api;
pub mod components; pub mod components;
pub mod util; pub mod config;
pub mod dialogs; pub mod dialogs;
pub mod settings;
pub mod util;
use api::{user::default_person, community::default_community, post::default_post}; use api::{community::default_community, post::default_post, user::default_person};
use components::{post_row::PostRow, community_row::CommunityRow, profile_page::{ProfilePage, self}, community_page::{CommunityPage, self}, post_page::{PostPage, self}, inbox_page::{InboxPage, InboxInput}}; use components::{
community_page::{self, CommunityPage},
community_row::CommunityRow,
inbox_page::{InboxInput, InboxPage},
post_page::{self, PostPage},
post_row::PostRow,
profile_page::{self, ProfilePage},
};
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, CommunityId, PersonId}, ListingType}, post::GetPostResponse, community::GetCommunityResponse}; use lemmy_api_common::{
use relm4::{prelude::*, factory::FactoryVecDeque, set_global_css, actions::{RelmAction, RelmActionGroup}}; community::GetCommunityResponse,
lemmy_db_schema::{
newtypes::{CommunityId, PersonId, PostId},
ListingType,
},
lemmy_db_views::structs::PostView,
lemmy_db_views_actor::structs::CommunityView,
person::GetPersonDetailsResponse,
post::GetPostResponse,
};
use relm4::{
actions::{RelmAction, RelmActionGroup},
factory::FactoryVecDeque,
prelude::*,
set_global_css,
};
use settings::get_current_account; use settings::get_current_account;
static APP_ID: &str = "com.lemmy-gtk.lemoa";
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
enum AppState { enum AppState {
Loading, Loading,
@ -24,7 +45,7 @@ enum AppState {
Post, Post,
Login, Login,
Message, Message,
Inbox Inbox,
} }
struct App { struct App {
@ -65,7 +86,7 @@ pub enum AppMsg {
OpenPost(PostId), OpenPost(PostId),
DoneFetchPost(GetPostResponse), DoneFetchPost(GetPostResponse),
OpenInbox, OpenInbox,
PopBackStack PopBackStack,
} }
#[relm4::component] #[relm4::component]
@ -322,22 +343,52 @@ impl SimpleComponent for App {
sender: ComponentSender<Self>, sender: ComponentSender<Self>,
) -> ComponentParts<Self> { ) -> ComponentParts<Self> {
let current_account = settings::get_current_account(); let current_account = settings::get_current_account();
let state = if current_account.instance_url.is_empty() { AppState::ChooseInstance } else { AppState::Loading }; let state = if current_account.instance_url.is_empty() {
AppState::ChooseInstance
} else {
AppState::Loading
};
let logged_in = current_account.jwt.is_some(); let logged_in = current_account.jwt.is_some();
// initialize all controllers and factories // initialize all controllers and factories
let posts = FactoryVecDeque::new(gtk::Box::default(), sender.input_sender()); let posts = FactoryVecDeque::new(gtk::Box::default(), sender.input_sender());
let communities = FactoryVecDeque::new(gtk::Box::default(), sender.input_sender()); let communities = FactoryVecDeque::new(gtk::Box::default(), sender.input_sender());
let profile_page = ProfilePage::builder().launch(default_person()).forward(sender.input_sender(), |msg| msg); let profile_page = ProfilePage::builder()
let community_page = CommunityPage::builder().launch(default_community().community_view).forward(sender.input_sender(), |msg| msg); .launch(default_person())
let post_page = PostPage::builder().launch(default_post()).forward(sender.input_sender(), |msg| msg); .forward(sender.input_sender(), |msg| msg);
let inbox_page = InboxPage::builder().launch(()).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 inbox_page = InboxPage::builder()
.launch(())
.forward(sender.input_sender(), |msg| msg);
let community_search_buffer = gtk::EntryBuffer::builder().build(); let community_search_buffer = gtk::EntryBuffer::builder().build();
let model = App { state, back_queue: vec![], logged_in, posts, communities, profile_page, community_page, post_page, inbox_page, message: None, current_communities_type: None, current_posts_type: None, current_communities_page: 1, current_posts_page: 1, community_search_buffer }; let model = App {
state,
back_queue: vec![],
logged_in,
posts,
communities,
profile_page,
community_page,
post_page,
inbox_page,
message: None,
current_communities_type: None,
current_posts_type: None,
current_communities_page: 1,
current_posts_page: 1,
community_search_buffer,
};
// fetch posts if that's the initial page // fetch posts if that's the initial page
if !current_account.instance_url.is_empty() { sender.input(AppMsg::StartFetchPosts(None, true)) }; if !current_account.instance_url.is_empty() {
sender.input(AppMsg::StartFetchPosts(None, true))
};
// setup all widgets and different stack pages // setup all widgets and different stack pages
let posts_box = model.posts.widget(); let posts_box = model.posts.widget();
@ -351,13 +402,16 @@ impl SimpleComponent for App {
// create the header bar menu and its actions // create the header bar menu and its actions
let instance_sender = sender.clone(); let instance_sender = sender.clone();
let instance_action: RelmAction<ChangeInstanceAction> = RelmAction::new_stateless(move |_| { let instance_action: RelmAction<ChangeInstanceAction> =
RelmAction::new_stateless(move |_| {
instance_sender.input(AppMsg::ChooseInstance); instance_sender.input(AppMsg::ChooseInstance);
}); });
let profile_sender = sender.clone(); let profile_sender = sender.clone();
let profile_action: RelmAction<ProfileAction> = RelmAction::new_stateless(move |_| { let profile_action: RelmAction<ProfileAction> = RelmAction::new_stateless(move |_| {
let person = settings::get_current_account(); let person = settings::get_current_account();
if !person.name.is_empty() { profile_sender.input(AppMsg::OpenPerson(PersonId(person.id))); } if !person.name.is_empty() {
profile_sender.input(AppMsg::OpenPerson(PersonId(person.id)));
}
}); });
let login_sender = sender.clone(); let login_sender = sender.clone();
let login_action: RelmAction<LoginAction> = RelmAction::new_stateless(move |_| { let login_action: RelmAction<LoginAction> = RelmAction::new_stateless(move |_| {
@ -380,15 +434,20 @@ impl SimpleComponent for App {
fn update(&mut self, msg: Self::Input, sender: ComponentSender<Self>) { fn update(&mut self, msg: Self::Input, sender: ComponentSender<Self>) {
// save the back queue // save the back queue
match msg { match msg {
AppMsg::DoneFetchCommunities(_) | AppMsg::DoneFetchCommunity(_) | AppMsg::DoneFetchPerson(_) | AppMsg::DoneFetchPost(_) | AppMsg::DoneFetchPosts(_) | AppMsg::ShowMessage(_) => { AppMsg::DoneFetchCommunities(_)
self.back_queue.push(msg.clone()) | AppMsg::DoneFetchCommunity(_)
} | AppMsg::DoneFetchPerson(_)
| AppMsg::DoneFetchPost(_)
| AppMsg::DoneFetchPosts(_)
| AppMsg::ShowMessage(_) => self.back_queue.push(msg.clone()),
_ => {} _ => {}
} }
match msg { match msg {
AppMsg::DoneChoosingInstance(instance_url) => { AppMsg::DoneChoosingInstance(instance_url) => {
if instance_url.trim().is_empty() { return; } if instance_url.trim().is_empty() {
return;
}
let mut current_account = settings::get_current_account(); let mut current_account = settings::get_current_account();
current_account.instance_url = instance_url; current_account.instance_url = instance_url;
settings::update_current_account(current_account); settings::update_current_account(current_account);
@ -400,41 +459,58 @@ impl SimpleComponent for App {
} }
AppMsg::StartFetchPosts(type_, remove_previous) => { AppMsg::StartFetchPosts(type_, remove_previous) => {
self.current_posts_type = type_; self.current_posts_type = type_;
let page = if remove_previous { 1 } else { self.current_posts_page + 1 }; let page = if remove_previous {
1
} else {
self.current_posts_page + 1
};
self.current_posts_page = page; self.current_posts_page = page;
std::thread::spawn(move || { std::thread::spawn(move || {
let message = match api::posts::list_posts(page, None, type_) { let message = match api::posts::list_posts(page, None, type_) {
Ok(posts) => AppMsg::DoneFetchPosts(posts), Ok(posts) => AppMsg::DoneFetchPosts(posts),
Err(err) => AppMsg::ShowMessage(err.to_string()) Err(err) => AppMsg::ShowMessage(err.to_string()),
}; };
sender.input(message); sender.input(message);
}); });
} }
AppMsg::DoneFetchPosts(posts) => { AppMsg::DoneFetchPosts(posts) => {
self.state = AppState::Posts; self.state = AppState::Posts;
if self.current_posts_page == 1 { self.posts.guard().clear(); } if self.current_posts_page == 1 {
self.posts.guard().clear();
}
for post in posts { for post in posts {
self.posts.guard().push_back(post); self.posts.guard().push_back(post);
} }
} }
AppMsg::FetchCommunities(listing_type, remove_previous) => { AppMsg::FetchCommunities(listing_type, remove_previous) => {
let query_text = self.community_search_buffer.text().as_str().to_owned(); let query_text = self.community_search_buffer.text().as_str().to_owned();
let query = if query_text.is_empty() { None } else { Some(query_text) }; let query = if query_text.is_empty() {
None
} else {
Some(query_text)
};
self.state = AppState::Communities; self.state = AppState::Communities;
let page = if remove_previous { 1 } else { self.current_communities_page + 1 }; let page = if remove_previous {
1
} else {
self.current_communities_page + 1
};
self.current_communities_page = page; self.current_communities_page = page;
self.current_communities_type = listing_type; self.current_communities_type = listing_type;
std::thread::spawn(move || { std::thread::spawn(move || {
let message = match api::communities::fetch_communities(page, query, listing_type) { let message =
match api::communities::fetch_communities(page, query, listing_type) {
Ok(communities) => AppMsg::DoneFetchCommunities(communities), Ok(communities) => AppMsg::DoneFetchCommunities(communities),
Err(err) => AppMsg::ShowMessage(err.to_string()) Err(err) => AppMsg::ShowMessage(err.to_string()),
}; };
sender.input(message); sender.input(message);
}); });
} }
AppMsg::DoneFetchCommunities(communities) => { AppMsg::DoneFetchCommunities(communities) => {
self.state = AppState::Communities; self.state = AppState::Communities;
if self.current_communities_page == 1 { self.communities.guard().clear(); } if self.current_communities_page == 1 {
self.communities.guard().clear();
}
for community in communities { for community in communities {
self.communities.guard().push_back(community); self.communities.guard().push_back(community);
} }
@ -444,13 +520,15 @@ impl SimpleComponent for App {
std::thread::spawn(move || { std::thread::spawn(move || {
let message = match api::user::get_user(person_id, 1) { let message = match api::user::get_user(person_id, 1) {
Ok(person) => AppMsg::DoneFetchPerson(person), Ok(person) => AppMsg::DoneFetchPerson(person),
Err(err) => AppMsg::ShowMessage(err.to_string()) Err(err) => AppMsg::ShowMessage(err.to_string()),
}; };
sender.input(message); sender.input(message);
}); });
} }
AppMsg::DoneFetchPerson(person) => { AppMsg::DoneFetchPerson(person) => {
self.profile_page.sender().emit(profile_page::ProfileInput::UpdatePerson(person)); self.profile_page
.sender()
.emit(profile_page::ProfileInput::UpdatePerson(person));
self.state = AppState::Person; self.state = AppState::Person;
} }
AppMsg::OpenCommunity(community_id) => { AppMsg::OpenCommunity(community_id) => {
@ -458,13 +536,17 @@ impl SimpleComponent for App {
std::thread::spawn(move || { std::thread::spawn(move || {
let message = match api::community::get_community(community_id) { let message = match api::community::get_community(community_id) {
Ok(community) => AppMsg::DoneFetchCommunity(community), Ok(community) => AppMsg::DoneFetchCommunity(community),
Err(err) => AppMsg::ShowMessage(err.to_string()) Err(err) => AppMsg::ShowMessage(err.to_string()),
}; };
sender.input(message); sender.input(message);
}); });
} }
AppMsg::DoneFetchCommunity(community) => { AppMsg::DoneFetchCommunity(community) => {
self.community_page.sender().emit(community_page::CommunityInput::UpdateCommunity(community.community_view)); self.community_page
.sender()
.emit(community_page::CommunityInput::UpdateCommunity(
community.community_view,
));
self.state = AppState::Community; self.state = AppState::Community;
} }
AppMsg::OpenPost(post_id) => { AppMsg::OpenPost(post_id) => {
@ -472,21 +554,29 @@ impl SimpleComponent for App {
std::thread::spawn(move || { std::thread::spawn(move || {
let message = match api::post::get_post(post_id) { let message = match api::post::get_post(post_id) {
Ok(post) => AppMsg::DoneFetchPost(post), Ok(post) => AppMsg::DoneFetchPost(post),
Err(err) => AppMsg::ShowMessage(err.to_string()) Err(err) => AppMsg::ShowMessage(err.to_string()),
}; };
sender.input(message); sender.input(message);
}); });
} }
AppMsg::DoneFetchPost(post) => { AppMsg::DoneFetchPost(post) => {
self.post_page.sender().emit(post_page::PostInput::UpdatePost(post)); self.post_page
.sender()
.emit(post_page::PostInput::UpdatePost(post));
self.state = AppState::Post; self.state = AppState::Post;
} }
AppMsg::ShowLogin => { AppMsg::ShowLogin => {
self.state = AppState::Login; self.state = AppState::Login;
} }
AppMsg::Login(username, password, totp_token) => { AppMsg::Login(username, password, totp_token) => {
if get_current_account().instance_url.is_empty() { return; } if get_current_account().instance_url.is_empty() {
let token = if totp_token.is_empty() { None } else { Some(totp_token) }; return;
}
let token = if totp_token.is_empty() {
None
} else {
Some(totp_token)
};
self.state = AppState::Loading; self.state = AppState::Loading;
std::thread::spawn(move || { std::thread::spawn(move || {
let message = match api::auth::login(username, password, token) { let message = match api::auth::login(username, password, token) {
@ -506,7 +596,7 @@ impl SimpleComponent for App {
AppMsg::ShowMessage("Wrong credentials!".to_string()) AppMsg::ShowMessage("Wrong credentials!".to_string())
} }
} }
Err(err) => AppMsg::ShowMessage(err.to_string()) Err(err) => AppMsg::ShowMessage(err.to_string()),
}; };
sender.input(message); sender.input(message);
}); });
@ -531,7 +621,9 @@ impl SimpleComponent for App {
} }
AppMsg::PopBackStack => { AppMsg::PopBackStack => {
let action = self.back_queue.get(self.back_queue.len() - 2); let action = self.back_queue.get(self.back_queue.len() - 2);
if let Some(action) = action { sender.input(action.clone()); } if let Some(action) = action {
sender.input(action.clone());
}
for _ in 0..2 { for _ in 0..2 {
self.back_queue.remove(self.back_queue.len() - 1); self.back_queue.remove(self.back_queue.len() - 1);
} }
@ -547,7 +639,7 @@ relm4::new_stateless_action!(LoginAction, WindowActionGroup, "login");
relm4::new_stateless_action!(LogoutAction, WindowActionGroup, "logout"); relm4::new_stateless_action!(LogoutAction, WindowActionGroup, "logout");
fn main() { fn main() {
let app = RelmApp::new(APP_ID); let app = RelmApp::new(config::APP_ID);
set_global_css(include_str!("style.css")); set_global_css(include_str!("style.css"));
app.run::<App>(()); app.run::<App>(());
} }

View File

@ -1,8 +1,8 @@
use std::{fs::File, path::PathBuf}; use crate::config::APP_ID;
use crate::gtk::glib; use crate::gtk::glib;
use lemmy_api_common::sensitive::Sensitive; use lemmy_api_common::sensitive::Sensitive;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::APP_ID; use std::{fs::File, path::PathBuf};
#[derive(Deserialize, Serialize, Default, Clone)] #[derive(Deserialize, Serialize, Default, Clone)]
pub struct Account { pub struct Account {
@ -15,7 +15,7 @@ pub struct Account {
#[derive(Deserialize, Serialize, Default)] #[derive(Deserialize, Serialize, Default)]
pub struct Preferences { pub struct Preferences {
pub accounts: Vec<Account>, pub accounts: Vec<Account>,
pub current_account_index: u32 pub current_account_index: u32,
} }
pub fn data_path() -> PathBuf { pub fn data_path() -> PathBuf {

View File

@ -4,15 +4,19 @@ use relm4_components::web_image::WebImageMsg;
pub fn get_web_image_msg(url: Option<DbUrl>) -> WebImageMsg { pub fn get_web_image_msg(url: Option<DbUrl>) -> WebImageMsg {
return if let Some(url) = url { return if let Some(url) = url {
WebImageMsg::LoadImage(url.to_string()) WebImageMsg::LoadImage(url.to_string())
} else { WebImageMsg::Unload }; } else {
WebImageMsg::Unload
};
} }
pub fn get_web_image_url(url: Option<DbUrl>) -> String { pub fn get_web_image_url(url: Option<DbUrl>) -> String {
return if let Some(url) = url { return if let Some(url) = url {
url.to_string() url.to_string()
} else { String::from("") } } else {
String::from("")
};
} }
pub fn markdown_to_pango_markup(text: String) -> String { pub fn markdown_to_pango_markup(text: String) -> String {
return html2pango::markup_html(&markdown::to_html(&text)).unwrap_or(text) return html2pango::markup_html(&markdown::to_html(&text)).unwrap_or(text);
} }