Add support for logging in to an account

This commit is contained in:
Bnyro 2023-06-19 10:08:12 +02:00
parent 5bd3491b00
commit 2d845708c4
5 changed files with 182 additions and 42 deletions

View File

@ -1,10 +1,10 @@
use lemmy_api_common::{person::{LoginResponse, Login}, sensitive::Sensitive}; use lemmy_api_common::{person::Login, sensitive::Sensitive};
pub fn login(username_or_email: String, password: String) -> std::result::Result<LoginResponse, reqwest::Error> { pub fn login(username_or_email: String, password: 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)
}; };
super::get("/user/login", &params) super::post("/user/login", &params)
} }

View File

@ -38,3 +38,15 @@ where
.send()? .send()?
.json() .json()
} }
fn post<T, Params>(path: &str, params: &Params) -> Result<T, reqwest::Error>
where
T: DeserializeOwned,
Params: Serialize + std::fmt::Debug,
{
CLIENT
.post(&get_url(path))
.json(&params)
.send()?
.json()
}

View File

@ -19,11 +19,15 @@ enum AppState {
Communities, Communities,
Community, Community,
Person, Person,
Post Post,
Login,
Message
} }
struct App { struct App {
state: AppState, state: AppState,
message: Option<String>,
latest_action: Option<AppMsg>,
posts: FactoryVecDeque<PostRow>, posts: FactoryVecDeque<PostRow>,
communities: FactoryVecDeque<CommunityRow>, communities: FactoryVecDeque<CommunityRow>,
profile_page: Controller<ProfilePage>, profile_page: Controller<ProfilePage>,
@ -31,13 +35,18 @@ struct App {
post_page: Controller<PostPage> post_page: Controller<PostPage>
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub enum AppMsg { pub enum AppMsg {
ChooseInstance, ChooseInstance,
ShowLogin,
Login(String, String),
Logout,
Retry,
ShowMessage(String),
DoneChoosingInstance(String), DoneChoosingInstance(String),
StartFetchPosts, StartFetchPosts,
DoneFetchPosts(Result<Vec<PostView>, reqwest::Error>), DoneFetchPosts(Vec<PostView>),
DoneFetchCommunities(Result<Vec<CommunityView>, reqwest::Error>), DoneFetchCommunities(Vec<CommunityView>),
ViewCommunities(Option<String>), ViewCommunities(Option<String>),
OpenCommunity(String), OpenCommunity(String),
DoneFetchCommunity(GetCommunityResponse), DoneFetchCommunity(GetCommunityResponse),
@ -75,7 +84,6 @@ impl SimpleComponent for App {
}, },
}, },
#[name(stack)]
match model.state { match model.state {
AppState::Posts => gtk::ScrolledWindow { AppState::Posts => gtk::ScrolledWindow {
set_vexpand: true, set_vexpand: true,
@ -118,12 +126,54 @@ impl SimpleComponent for App {
gtk::Button { gtk::Button {
set_label: "Done", set_label: "Done",
connect_clicked[sender, instance_url] => move |_| { connect_clicked[sender, instance_url] => move |_| {
let text = instance_url.buffer().text().as_str().to_string(); let text = instance_url.text().as_str().to_string();
instance_url.buffer().set_text(""); instance_url.set_text("");
sender.input(AppMsg::DoneChoosingInstance(text)); sender.input(AppMsg::DoneChoosingInstance(text));
}, },
} }
}, },
AppState::Login => gtk::Box {
set_hexpand: true,
set_orientation: gtk::Orientation::Vertical,
set_spacing: 12,
set_margin_all: 20,
set_valign: gtk::Align::Center,
set_hexpand: true,
gtk::Label {
set_text: "Login",
add_css_class: "font-bold",
},
#[name(username)]
gtk::Entry {
set_placeholder_text: Some("Username or E-Mail"),
},
#[name(password)]
gtk::PasswordEntry {
set_placeholder_text: Some("Password"),
set_show_peek_icon: true,
},
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_halign: gtk::Align::End,
gtk::Button {
set_label: "Cancel",
connect_clicked => AppMsg::StartFetchPosts,
set_margin_end: 10,
},
gtk::Button {
set_label: "Login",
connect_clicked[sender, username, password] => move |_| {
let username_text = username.text().as_str().to_string();
username.set_text("");
let password_text = password.text().as_str().to_string();
password.set_text("");
sender.input(AppMsg::Login(username_text, password_text));
},
},
}
},
AppState::Communities => gtk::Box { AppState::Communities => gtk::Box {
gtk::ScrolledWindow { gtk::ScrolledWindow {
set_vexpand: true, set_vexpand: true,
@ -145,7 +195,7 @@ impl SimpleComponent for App {
gtk::Button { gtk::Button {
set_label: "Search", set_label: "Search",
connect_clicked[sender, community_search_query] => move |_| { connect_clicked[sender, community_search_query] => move |_| {
let text = community_search_query.buffer().text().as_str().to_string(); let text = community_search_query.text().as_str().to_string();
sender.input(AppMsg::ViewCommunities(Some(text))); sender.input(AppMsg::ViewCommunities(Some(text)));
}, },
} }
@ -177,13 +227,30 @@ impl SimpleComponent for App {
post_page -> gtk::ScrolledWindow {} post_page -> gtk::ScrolledWindow {}
} }
} }
AppState::Message => {
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_margin_all: 40,
gtk::Label {
#[watch]
set_text: &model.message.clone().unwrap_or("".to_string()),
},
gtk::Button {
set_label: "Go Home",
connect_clicked => AppMsg::Retry,
}
}
}
} }
} }
} }
menu! { menu! {
menu_model: { menu_model: {
"Choose Instance" => ChangeInstanceAction "Choose Instance" => ChangeInstanceAction,
"Login" => LoginAction,
"Logout" => LogoutAction
} }
} }
@ -203,7 +270,7 @@ impl SimpleComponent for App {
let community_page = CommunityPage::builder().launch(default_community()).forward(sender.input_sender(), |msg| msg); let community_page = CommunityPage::builder().launch(default_community()).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 model = App { state, posts, communities, profile_page, community_page, post_page }; let model = App { state, posts, communities, profile_page, community_page, post_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) }; if !instance_url.is_empty() { sender.input(AppMsg::StartFetchPosts) };
@ -217,13 +284,23 @@ impl SimpleComponent for App {
let widgets = view_output!(); let widgets = view_output!();
// create the menu and its actions // create the header bar menu and its actions
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);
});
let login_sender = sender.clone();
let login_action: RelmAction<LoginAction> = RelmAction::new_stateless(move |_| {
login_sender.input(AppMsg::ShowLogin);
});
let logout_action: RelmAction<LogoutAction> = RelmAction::new_stateless(move |_| {
sender.input(AppMsg::ChooseInstance); sender.input(AppMsg::ChooseInstance);
}); });
let mut group = RelmActionGroup::<WindowActionGroup>::new(); let mut group = RelmActionGroup::<WindowActionGroup>::new();
group.add_action(instance_action); group.add_action(instance_action);
group.add_action(login_action);
group.add_action(logout_action);
group.register_for_widget(&widgets.main_window); group.register_for_widget(&widgets.main_window);
ComponentParts { model, widgets } ComponentParts { model, widgets }
@ -244,43 +321,46 @@ impl SimpleComponent for App {
} }
AppMsg::StartFetchPosts => { AppMsg::StartFetchPosts => {
std::thread::spawn(move || { std::thread::spawn(move || {
let posts = api::posts::list_posts(1, None); let message = match api::posts::list_posts(1, None) {
sender.input(AppMsg::DoneFetchPosts(posts)); Ok(posts) => AppMsg::DoneFetchPosts(posts),
Err(err) => AppMsg::ShowMessage(err.to_string())
};
sender.input(message);
}); });
} }
AppMsg::DoneFetchPosts(posts) => { AppMsg::DoneFetchPosts(posts) => {
self.state = AppState::Posts; self.state = AppState::Posts;
if let Ok(posts) = posts { self.posts.guard().clear();
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::ViewCommunities(query) => { AppMsg::ViewCommunities(query) => {
self.state = AppState::Communities; self.state = AppState::Communities;
if (query.is_none() || query.clone().unwrap().trim().is_empty()) && !self.communities.is_empty() { return; } if (query.is_none() || query.clone().unwrap().trim().is_empty()) && !self.communities.is_empty() { return; }
std::thread::spawn(move || { std::thread::spawn(move || {
let communities = api::communities::fetch_communities(1, query); let message = match api::communities::fetch_communities(1, query) {
sender.input(AppMsg::DoneFetchCommunities(communities)); Ok(communities) => AppMsg::DoneFetchCommunities(communities),
Err(err) => AppMsg::ShowMessage(err.to_string())
};
sender.input(message);
}); });
} }
AppMsg::DoneFetchCommunities(communities) => { AppMsg::DoneFetchCommunities(communities) => {
self.state = AppState::Communities; self.state = AppState::Communities;
if let Ok(communities) = communities { self.communities.guard().clear();
self.communities.guard().clear(); for community in communities {
for community in communities { self.communities.guard().push_back(community);
self.communities.guard().push_back(community);
}
} }
} }
AppMsg::OpenPerson(person_name) => { AppMsg::OpenPerson(person_name) => {
self.state = AppState::Loading; self.state = AppState::Loading;
std::thread::spawn(move || { std::thread::spawn(move || {
let person = api::user::get_user(person_name, 1); let message = match api::user::get_user(person_name, 1) {
if let Ok(person) = person { Ok(person) => AppMsg::DoneFetchPerson(person),
sender.input(AppMsg::DoneFetchPerson(person)); Err(err) => AppMsg::ShowMessage(err.to_string())
} };
sender.input(message);
}); });
} }
AppMsg::DoneFetchPerson(person) => { AppMsg::DoneFetchPerson(person) => {
@ -290,10 +370,11 @@ impl SimpleComponent for App {
AppMsg::OpenCommunity(community_name) => { AppMsg::OpenCommunity(community_name) => {
self.state = AppState::Loading; self.state = AppState::Loading;
std::thread::spawn(move || { std::thread::spawn(move || {
let community = api::community::get_community(community_name); let message = match api::community::get_community(community_name) {
if let Ok(community) = community { Ok(community) => AppMsg::DoneFetchCommunity(community),
sender.input(AppMsg::DoneFetchCommunity(community)); Err(err) => AppMsg::ShowMessage(err.to_string())
} };
sender.input(message);
}); });
} }
AppMsg::DoneFetchCommunity(community) => { AppMsg::DoneFetchCommunity(community) => {
@ -303,22 +384,55 @@ impl SimpleComponent for App {
AppMsg::OpenPost(post_id) => { AppMsg::OpenPost(post_id) => {
self.state = AppState::Loading; self.state = AppState::Loading;
std::thread::spawn(move || { std::thread::spawn(move || {
let post = api::post::get_post(post_id); let message = match api::post::get_post(post_id) {
if let Ok(post) = post { Ok(post) => AppMsg::DoneFetchPost(post),
sender.input(AppMsg::DoneFetchPost(post)); Err(err) => AppMsg::ShowMessage(err.to_string())
} };
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 => {
self.state = AppState::Login;
}
AppMsg::Login(username, password) => {
self.state = AppState::Loading;
std::thread::spawn(move || {
let message = match api::auth::login(username, password) {
Ok(login) => {
if let Some(token) = login.jwt {
util::set_auth_token(Some(token));
AppMsg::StartFetchPosts
} else {
AppMsg::ShowMessage("Wrong credentials!".to_string())
}
}
Err(err) => AppMsg::ShowMessage(err.to_string())
};
sender.input(message);
});
}
AppMsg::Logout => {
util::set_auth_token(None);
}
AppMsg::ShowMessage(message) => {
self.message = Some(message);
self.state = AppState::Message;
}
AppMsg::Retry => {
sender.input(self.latest_action.clone().unwrap_or(AppMsg::StartFetchPosts));
}
} }
} }
} }
relm4::new_action_group!(WindowActionGroup, "win"); relm4::new_action_group!(WindowActionGroup, "win");
relm4::new_stateless_action!(ChangeInstanceAction, WindowActionGroup, "instance"); relm4::new_stateless_action!(ChangeInstanceAction, WindowActionGroup, "instance");
relm4::new_stateless_action!(LoginAction, WindowActionGroup, "login");
relm4::new_stateless_action!(LogoutAction, WindowActionGroup, "logout");
fn main() { fn main() {
let app = RelmApp::new(APP_ID); let app = RelmApp::new(APP_ID);

View File

@ -1,11 +1,13 @@
use std::{fs::File, path::PathBuf}; use std::{fs::File, path::PathBuf};
use crate::gtk::glib; use crate::gtk::glib;
use lemmy_api_common::sensitive::Sensitive;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::APP_ID; use crate::APP_ID;
#[derive(Deserialize, Serialize, Default)] #[derive(Deserialize, Serialize, Default)]
pub struct Preferences { pub struct Preferences {
pub instance_url: String, pub instance_url: String,
pub jwt: Option<Sensitive<String>>
} }
pub fn data_path() -> PathBuf { pub fn data_path() -> PathBuf {

View File

@ -1,6 +1,8 @@
use lemmy_api_common::lemmy_db_schema::newtypes::DbUrl; use lemmy_api_common::{lemmy_db_schema::newtypes::DbUrl, sensitive::Sensitive};
use relm4_components::web_image::WebImageMsg; use relm4_components::web_image::WebImageMsg;
use crate::settings;
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())
@ -15,4 +17,14 @@ pub fn get_web_image_url(url: Option<DbUrl>) -> String {
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)
} }
pub fn set_auth_token(token: Option<Sensitive<String>>) {
let mut settings = settings::get_prefs();
settings.jwt = token.clone();
settings::save_prefs(&settings);
}
pub fn get_auth_token() -> Option<Sensitive<String>> {
settings::get_prefs().jwt
}