Compare commits

...

3 Commits

Author SHA1 Message Date
iris 24299bfa17 don't panic on keyring fail + disable button while connecting 2026-04-14 16:26:20 -04:00
iris 3e6df06411 accept / deny friend requests 2026-03-15 21:29:41 -04:00
iris a32f6392b7 USER CACHE 2026-03-15 19:34:55 -04:00
12 changed files with 169 additions and 70 deletions
+1 -1
Submodule iris updated: 1102dc7338...c118bb446b
+3 -4
View File
@@ -29,11 +29,10 @@ impl ClientData {
self.data.load::<AccountList>().push(info); self.data.load::<AccountList>().push(info);
} }
pub fn password(&self, info: &AccountInfo) -> String { pub fn password(&self, info: &AccountInfo) -> Result<String, String> {
let user_path = info.path(); let user_path = info.path();
let entry = let entry = keyring_core::Entry::new("openworm", &user_path).map_err(|e| e.to_string())?;
keyring_core::Entry::new("openworm", &user_path).expect("failed to open keyring entry"); entry.get_password().map_err(|e| e.to_string())
entry.get_password().unwrap()
} }
pub fn accounts(&self) -> DataGuard<AccountList> { pub fn accounts(&self) -> DataGuard<AccountList> {
+1 -1
View File
@@ -29,7 +29,7 @@ pub struct Client {
} }
pub type Rsc = DefaultRsc<Client>; pub type Rsc = DefaultRsc<Client>;
pub type ClientSender = EventSender<Rsc>; pub type ClientSender = EventSender<Client>;
pub enum ClientEvent { pub enum ClientEvent {
CacheUpdate, CacheUpdate,
+27 -12
View File
@@ -1,5 +1,6 @@
use crate::ClientSender; use crate::{Client, ClientSender, Rsc};
use dashmap::DashMap; use dashmap::DashMap;
use iris::prelude::DefaultRsc;
use openworm::net::{ use openworm::net::{
ClientMsg, ClientMsgInst, RecvHandler, RequestId, RequestMsg, SERVER_NAME, ServerMsg, ClientMsg, ClientMsgInst, RecvHandler, RequestId, RequestMsg, SERVER_NAME, ServerMsg,
ServerMsgInst, SkipServerVerification, recv_uni, send_uni, ServerMsgInst, SkipServerVerification, recv_uni, send_uni,
@@ -30,16 +31,27 @@ pub struct NetHandle {
} }
type NetResult<T> = Result<T, String>; type NetResult<T> = Result<T, String>;
type SyncReqFn = Box<dyn FnOnce(ServerMsg, &mut Rsc) + Send + Sync>;
pub enum NetCtrlMsg { pub enum NetCtrlMsg {
Send(ClientMsg), Send(ClientMsg),
Request(ClientMsg, oneshot::Sender<ServerMsg>), Request(ClientMsg, oneshot::Sender<ServerMsg>),
RequestSync(ClientMsg, Box<dyn FnOnce(ServerMsg) + Send + Sync>), RequestSync(ClientMsg, SyncReqFn),
Exit, Exit,
} }
type Resp<R: RequestMsg> = Result<R::Result, ()>; type Resp<R: RequestMsg> = Result<R::Result, ()>;
// TODO: move into iris?
pub trait MainDataCallback<Data, State>:
FnOnce(Data, &mut DefaultRsc<State>) + Sync + Send + 'static
{
}
impl<F: FnOnce(Data, &mut DefaultRsc<State>) + Sync + Send + 'static, Data, State>
MainDataCallback<Data, State> for F
{
}
impl NetHandle { impl NetHandle {
fn send_(&self, msg: NetCtrlMsg) { fn send_(&self, msg: NetCtrlMsg) {
let _ = self.send.send(msg); let _ = self.send.send(msg);
@@ -60,21 +72,22 @@ impl NetHandle {
} }
} }
pub fn request_sync<R: RequestMsg>(&self, msg: R) -> SyncRecv<R> { pub fn request_sync<R: RequestMsg>(
let (send, recv) = oneshot::channel(); &self,
let sender = self.event_sender.clone(); msg: R,
callback: impl MainDataCallback<Result<R::Result, ()>, Client>,
) {
self.send_(NetCtrlMsg::RequestSync( self.send_(NetCtrlMsg::RequestSync(
msg.into(), msg.into(),
Box::new(move |msg| { Box::new(move |msg, rsc| {
let _ = send.send(if let Some(res) = R::result(msg) { let res = if let Some(res) = R::result(msg) {
Ok(res) Ok(res)
} else { } else {
Err(()) Err(())
}); };
sender.run(); callback(res, rsc);
}), }),
)); ));
SyncRecv::<R> { recv }
} }
pub fn exit(self) { pub fn exit(self) {
@@ -174,6 +187,7 @@ impl NetHandle {
msg, msg,
requests_sync: DashMap::default(), requests_sync: DashMap::default(),
requests: DashMap::default(), requests: DashMap::default(),
event_sender: event_sender.clone(),
}); });
tokio::spawn(recv_uni(conn_, recv.clone())); tokio::spawn(recv_uni(conn_, recv.clone()));
tokio::spawn(async move { tokio::spawn(async move {
@@ -239,7 +253,8 @@ where
struct ServerRecv<F: MsgHandler> { struct ServerRecv<F: MsgHandler> {
requests: DashMap<RequestId, oneshot::Sender<ServerMsg>>, requests: DashMap<RequestId, oneshot::Sender<ServerMsg>>,
requests_sync: DashMap<RequestId, Box<dyn FnOnce(ServerMsg) + Send + Sync>>, requests_sync: DashMap<RequestId, SyncReqFn>,
event_sender: ClientSender,
msg: F, msg: F,
} }
@@ -249,7 +264,7 @@ impl<F: MsgHandler> RecvHandler<ServerMsgInst> for ServerRecv<F> {
if let Some((_, send)) = self.requests.remove(&id) { if let Some((_, send)) = self.requests.remove(&id) {
let _ = send.send(resp.msg); let _ = send.send(resp.msg);
} else if let Some((_, f)) = self.requests_sync.remove(&id) { } else if let Some((_, f)) = self.requests_sync.remove(&id) {
f(resp.msg) self.event_sender.run(|rsc| f(resp.msg, rsc));
} }
} else { } else {
self.msg.run(resp.msg).await; self.msg.run(resp.msg).await;
+18 -2
View File
@@ -1,8 +1,16 @@
use std::sync::{Arc, Mutex, MutexGuard};
use openworm::net::UserId; use openworm::net::UserId;
use crate::{net::NetHandle, ui::UserCache}; use crate::{net::NetHandle, ui::UserCache};
pub struct Session { // TODO: this really should not be async...
// I mean it could be used async but all widgets
// are sync so I don't really think it makes sense to be...
#[derive(Clone)]
pub struct Session(Arc<Mutex<SessionInner>>);
pub struct SessionInner {
pub con: NetHandle, pub con: NetHandle,
pub user_id: UserId, pub user_id: UserId,
pub cache: UserCache, pub cache: UserCache,
@@ -10,10 +18,18 @@ pub struct Session {
impl Session { impl Session {
pub fn new(con: NetHandle, user_id: UserId) -> Self { pub fn new(con: NetHandle, user_id: UserId) -> Self {
Self { Self(Arc::new(Mutex::new(SessionInner {
cache: UserCache::new(con.clone()), cache: UserCache::new(con.clone()),
con, con,
user_id, user_id,
})))
} }
pub fn con(&self) -> NetHandle {
self.get().con.clone()
}
pub fn get(&self) -> MutexGuard<'_, SessionInner> {
self.0.try_lock().unwrap()
} }
} }
+1
View File
@@ -2,4 +2,5 @@ use super::*;
pub const MODAL_BG: UiColor = UiColor::BLACK.brighter(0.05); pub const MODAL_BG: UiColor = UiColor::BLACK.brighter(0.05);
pub const GREEN: UiColor = UiColor::rgb(0, 150, 0); pub const GREEN: UiColor = UiColor::rgb(0, 150, 0);
pub const RED: UiColor = UiColor::rgb(255, 100, 100);
pub const DARK: UiColor = UiColor::BLACK.brighter(0.02); pub const DARK: UiColor = UiColor::BLACK.brighter(0.02);
+12 -5
View File
@@ -31,8 +31,7 @@ pub fn start(rsc: &mut Rsc, data: &ClientData) -> WeakWidget {
.add(rsc), .add(rsc),
color::DARK, color::DARK,
rsc, rsc,
) );
.add(rsc);
let account = account.clone(); let account = account.clone();
let cert_hex = data let cert_hex = data
.data .data
@@ -44,14 +43,22 @@ pub fn start(rsc: &mut Rsc, data: &ClientData) -> WeakWidget {
let cert = decode_hex(&cert_hex).unwrap(); let cert = decode_hex(&cert_hex).unwrap();
keyring::use_native_store(true).unwrap(); keyring::use_native_store(true).unwrap();
rsc.events.register(button, Submit, move |ctx, rsc| { rsc.events.register(button, Submit, move |ctx, rsc| {
let password = match ctx.state.data.password(&account) {
Ok(v) => v,
Err(err) => {
ctx.state.error(&err, rsc);
return;
}
};
button.disable(rsc);
let account = account.clone(); let account = account.clone();
let cert = cert.clone(); let cert = cert.clone();
let password = ctx.state.data.password(&account);
let event_sender = rsc.window_event.clone(); let event_sender = rsc.window_event.clone();
rsc.spawn_task(async move |mut ctx| { rsc.spawn_task(async move |mut ctx| {
let mut fail = |reason: &str| { let mut fail = |reason: &str| {
let reason = reason.to_string(); let reason = reason.to_string();
ctx.update(move |ctx, rsc| { ctx.update(move |ctx, rsc| {
button.enable(rsc);
ctx.error(&reason, rsc); ctx.error(&reason, rsc);
}) })
}; };
@@ -91,8 +98,8 @@ pub fn start(rsc: &mut Rsc, data: &ClientData) -> WeakWidget {
return fail("invalid password"); return fail("invalid password");
} }
}; };
let session = Session::new(con, user_id);
ctx.update(move |ctx, rsc| { ctx.update(move |ctx, rsc| {
let session = Session::new(con, user_id);
main_view(rsc, session).set_ptr(ctx.main_ui, rsc); main_view(rsc, session).set_ptr(ctx.main_ui, rsc);
}); });
}); });
@@ -190,8 +197,8 @@ pub fn create_account(rsc: &mut Rsc) -> WeakWidget {
return fail("invalid account token"); return fail("invalid account token");
} }
}; };
let session = Session::new(con, user_id);
ctx.update(move |ctx, rsc| { ctx.update(move |ctx, rsc| {
let session = Session::new(con, user_id);
main_view(rsc, session).set_ptr(ctx.main_ui, rsc); main_view(rsc, session).set_ptr(ctx.main_ui, rsc);
ctx.data.create_account( ctx.data.create_account(
ServerInfo { cert_hex }, ServerInfo { cert_hex },
+66 -8
View File
@@ -1,4 +1,9 @@
use openworm::net::{AddFriend, AddFriendResp, GenerateToken, RequestFriends, ServerPerms}; use openworm::net::{
AddFriend, AddFriendResp, AnswerFriendRequest, FriendRequestAction, GenerateToken,
RequestFriends, ServerPerms, UserId,
};
use crate::net::NetHandle;
use super::*; use super::*;
@@ -15,8 +20,8 @@ pub fn view(rsc: &mut Rsc, session: &Session) -> StrongWidget {
fn add_friend_area(rsc: &mut Rsc, session: &Session) -> WeakWidget { fn add_friend_area(rsc: &mut Rsc, session: &Session) -> WeakWidget {
let username_field = field("", "username").add(rsc); let username_field = field("", "username").add(rsc);
let add = Button::submit("add friend", rsc); let add = Button::submit("add friend", rsc);
let con = session.con.clone(); let con = session.con();
rsc.events.register(add, Submit, move |ctx, rsc| { rsc.events.register(add, Submit, move |_, rsc| {
let con = con.clone(); let con = con.clone();
let username = username_field.edit(rsc).take(); let username = username_field.edit(rsc).take();
add.disable(rsc); add.disable(rsc);
@@ -54,7 +59,7 @@ fn add_friend_area(rsc: &mut Rsc, session: &Session) -> WeakWidget {
fn gen_token(rsc: &mut Rsc, session: &Session) -> WeakWidget { fn gen_token(rsc: &mut Rsc, session: &Session) -> WeakWidget {
let generate = Button::normal("generate token", rsc); let generate = Button::normal("generate token", rsc);
let con = session.con.clone(); let con = session.con();
let token = wtext("") let token = wtext("")
.size(30) .size(30)
.editable(EditMode::SingleLine) .editable(EditMode::SingleLine)
@@ -84,9 +89,11 @@ fn gen_token(rsc: &mut Rsc, session: &Session) -> WeakWidget {
fn friends_list(rsc: &mut Rsc, session: &Session) -> WeakWidget { fn friends_list(rsc: &mut Rsc, session: &Session) -> WeakWidget {
let ptr = loading_area("loading friends").add(rsc); let ptr = loading_area("loading friends").add(rsc);
let con = session.con.clone(); let con = session.con();
let session = session.clone();
rsc.events.register(ptr, Draw, move |_, rsc| { rsc.events.register(ptr, Draw, move |_, rsc| {
let con = con.clone(); let con = con.clone();
let session = session.clone();
// TODO: maybe have rsc.request method that takes in &con? // TODO: maybe have rsc.request method that takes in &con?
// need to also handle error tho // need to also handle error tho
rsc.spawn_task(async move |mut ctx| { rsc.spawn_task(async move |mut ctx| {
@@ -97,13 +104,34 @@ fn friends_list(rsc: &mut Rsc, session: &Session) -> WeakWidget {
let mut all = Span::empty(Dir::DOWN); let mut all = Span::empty(Dir::DOWN);
if !resp.incoming.is_empty() { if !resp.incoming.is_empty() {
all.push(section_label("incoming").add_strong(rsc)); all.push(section_label("incoming").add_strong(rsc));
all.push(user_list(&resp.incoming, rsc).add_strong(rsc)) all.push(
resp.incoming
.rsc_map(|id, rsc| {
(
user_rect(id, &session, rsc),
accept_req(id, &con, rsc),
deny_req(id, &con, rsc),
)
.span(Dir::RIGHT)
.add(rsc)
})
.span(Dir::DOWN)
.add_strong(rsc),
)
} }
all.push(section_label("friends").add_strong(rsc)); all.push(section_label("friends").add_strong(rsc));
all.push(user_list(&resp.current, rsc).add_strong(rsc)); all.push(
user_list(&resp.current, &session)
.span(Dir::DOWN)
.add_strong(rsc),
);
if !resp.outgoing.is_empty() { if !resp.outgoing.is_empty() {
all.push(section_label("outgoing").add_strong(rsc)); all.push(section_label("outgoing").add_strong(rsc));
all.push(user_list(&resp.outgoing, rsc).add_strong(rsc)) all.push(
user_list(&resp.outgoing, &session)
.span(Dir::DOWN)
.add_strong(rsc),
)
} }
all.set_ptr(ptr, rsc); all.set_ptr(ptr, rsc);
}); });
@@ -111,3 +139,33 @@ fn friends_list(rsc: &mut Rsc, session: &Session) -> WeakWidget {
}); });
ptr ptr
} }
pub fn accept_req(id: UserId, con: &NetHandle, rsc: &mut Rsc) -> WeakWidget {
let button = Button::submit("󰸞", rsc);
let con = con.clone();
rsc.events.register(button, Submit, move |_, rsc| {
let con = con.clone();
rsc.tasks.spawn(async move |_| {
con.send(AnswerFriendRequest {
id,
action: FriendRequestAction::Accept,
});
});
});
button.width(60).add(rsc)
}
pub fn deny_req(id: UserId, con: &NetHandle, rsc: &mut Rsc) -> WeakWidget {
let button = Button::new("X", color::RED, rsc);
let con = con.clone();
rsc.events.register(button, Submit, move |_, rsc| {
let con = con.clone();
rsc.tasks.spawn(async move |_| {
con.send(AnswerFriendRequest {
id,
action: FriendRequestAction::Deny,
});
});
});
button.width(60).add(rsc)
}
-12
View File
@@ -152,18 +152,6 @@ pub fn section_label(text: impl Into<String>) -> TextBuilder<Rsc> {
wtext(text).size(30) wtext(text).size(30)
} }
pub fn user_list<'a>(ids: impl IntoIterator<Item = &'a UserId>, rsc: &mut Rsc) -> Span {
let mut span = Span::empty(Dir::DOWN);
for id in ids {
let thing = (wtext(id.to_string()).size(20),)
.span(Dir::RIGHT)
.gap(30)
.pad(15);
span.push(thing.add_strong(rsc));
}
span
}
impl Client { impl Client {
pub fn error(&self, text: &str, rsc: &mut Rsc) { pub fn error(&self, text: &str, rsc: &mut Rsc) {
rsc[self.notif].inner = Some(werror(text, rsc)); rsc[self.notif].inner = Some(werror(text, rsc));
+1 -1
View File
@@ -29,7 +29,7 @@ fn info(rsc: &mut Rsc) -> StrongWidget {
fn users(rsc: &mut Rsc, session: &Session) -> StrongWidget { fn users(rsc: &mut Rsc, session: &Session) -> StrongWidget {
let ptr = loading_area("loading users").add(rsc); let ptr = loading_area("loading users").add(rsc);
let con = session.con.clone(); let con = session.con();
rsc.events.register(ptr, Draw, move |_, rsc| { rsc.events.register(ptr, Draw, move |_, rsc| {
let con = con.clone(); let con = con.clone();
rsc.spawn_task(async move |mut ctx| { rsc.spawn_task(async move |mut ctx| {
+33 -22
View File
@@ -19,35 +19,46 @@ impl UserCache {
widgets: Default::default(), widgets: Default::default(),
} }
} }
}
pub fn username(&mut self, id: UserId, rsc: &mut Rsc) -> WeakWidget { impl Session {
let text = if let Some(user) = self.users.get(&id) { pub fn username(&self, id: UserId, rsc: &mut Rsc) -> WeakWidget {
let session = self.clone();
let s = &mut self.get().cache;
let text = if let Some(user) = s.users.get(&id) {
&user.username &user.username
} else { } else {
if !self.requests.contains_key(&id) { if !s.requests.contains_key(&id) {
let recv = self.con.request_sync(RequestUserInfo { id }); s.con
self.requests.insert(id, recv); .request_sync(RequestUserInfo { id }, move |resp, rsc| {
}
"loading..."
};
let wid = wtext(text).add(rsc);
self.widgets.entry(id).or_default().push(wid);
wid
}
pub fn update(&mut self, rsc: &mut Rsc) {
self.requests.retain(|id, req| {
if let Some(resp) = req.try_recv() {
if let Ok(info) = resp { if let Ok(info) = resp {
for &widget in self.widgets.get(id).into_iter().flatten() { let s = &mut session.get().cache;
for &widget in s.widgets.get(&id).into_iter().flatten() {
*rsc[widget].content = info.username.clone(); *rsc[widget].content = info.username.clone();
} }
self.users.insert(*id, info); s.users.insert(id, info);
}
false
} else {
true
} }
}); });
} }
"loading..."
};
let wid = wtext(text).size(20).add(rsc);
s.widgets.entry(id).or_default().push(wid);
wid
}
}
pub fn user_list<'a>(
ids: impl IntoIterator<Item = &'a UserId>,
session: &Session,
) -> impl IntoIterator<Item = impl FnOnce(&mut Rsc) -> WeakWidget> {
ids.rsc_map(|id, rsc: &mut Rsc| user_rect(*id, session, rsc))
}
pub fn user_rect(id: UserId, session: &Session, rsc: &mut Rsc) -> WeakWidget {
(session.username(id, rsc),)
.span(Dir::RIGHT)
.gap(30)
.pad(15)
.add(rsc) as WeakWidget
} }
+4
View File
@@ -164,6 +164,10 @@ impl ClientHandler {
if user.friends.outgoing.contains(&other_id) { if user.friends.outgoing.contains(&other_id) {
reply!(AddFriendResp::AlreadySent); reply!(AddFriendResp::AlreadySent);
} }
// TODO: just accept in this case?
if user.friends.incoming.contains(&other_id) {
reply!(AddFriendResp::AlreadySent);
}
if other_id == user_id { if other_id == user_id {
reply!(AddFriendResp::CannotAddSelf); reply!(AddFriendResp::CannotAddSelf);
} }