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);
}
pub fn password(&self, info: &AccountInfo) -> String {
pub fn password(&self, info: &AccountInfo) -> Result<String, String> {
let user_path = info.path();
let entry =
keyring_core::Entry::new("openworm", &user_path).expect("failed to open keyring entry");
entry.get_password().unwrap()
let entry = keyring_core::Entry::new("openworm", &user_path).map_err(|e| e.to_string())?;
entry.get_password().map_err(|e| e.to_string())
}
pub fn accounts(&self) -> DataGuard<AccountList> {
+1 -1
View File
@@ -29,7 +29,7 @@ pub struct Client {
}
pub type Rsc = DefaultRsc<Client>;
pub type ClientSender = EventSender<Rsc>;
pub type ClientSender = EventSender<Client>;
pub enum ClientEvent {
CacheUpdate,
+27 -12
View File
@@ -1,5 +1,6 @@
use crate::ClientSender;
use crate::{Client, ClientSender, Rsc};
use dashmap::DashMap;
use iris::prelude::DefaultRsc;
use openworm::net::{
ClientMsg, ClientMsgInst, RecvHandler, RequestId, RequestMsg, SERVER_NAME, ServerMsg,
ServerMsgInst, SkipServerVerification, recv_uni, send_uni,
@@ -30,16 +31,27 @@ pub struct NetHandle {
}
type NetResult<T> = Result<T, String>;
type SyncReqFn = Box<dyn FnOnce(ServerMsg, &mut Rsc) + Send + Sync>;
pub enum NetCtrlMsg {
Send(ClientMsg),
Request(ClientMsg, oneshot::Sender<ServerMsg>),
RequestSync(ClientMsg, Box<dyn FnOnce(ServerMsg) + Send + Sync>),
RequestSync(ClientMsg, SyncReqFn),
Exit,
}
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 {
fn send_(&self, msg: NetCtrlMsg) {
let _ = self.send.send(msg);
@@ -60,21 +72,22 @@ impl NetHandle {
}
}
pub fn request_sync<R: RequestMsg>(&self, msg: R) -> SyncRecv<R> {
let (send, recv) = oneshot::channel();
let sender = self.event_sender.clone();
pub fn request_sync<R: RequestMsg>(
&self,
msg: R,
callback: impl MainDataCallback<Result<R::Result, ()>, Client>,
) {
self.send_(NetCtrlMsg::RequestSync(
msg.into(),
Box::new(move |msg| {
let _ = send.send(if let Some(res) = R::result(msg) {
Box::new(move |msg, rsc| {
let res = if let Some(res) = R::result(msg) {
Ok(res)
} else {
Err(())
});
sender.run();
};
callback(res, rsc);
}),
));
SyncRecv::<R> { recv }
}
pub fn exit(self) {
@@ -174,6 +187,7 @@ impl NetHandle {
msg,
requests_sync: DashMap::default(),
requests: DashMap::default(),
event_sender: event_sender.clone(),
});
tokio::spawn(recv_uni(conn_, recv.clone()));
tokio::spawn(async move {
@@ -239,7 +253,8 @@ where
struct ServerRecv<F: MsgHandler> {
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,
}
@@ -249,7 +264,7 @@ impl<F: MsgHandler> RecvHandler<ServerMsgInst> for ServerRecv<F> {
if let Some((_, send)) = self.requests.remove(&id) {
let _ = send.send(resp.msg);
} else if let Some((_, f)) = self.requests_sync.remove(&id) {
f(resp.msg)
self.event_sender.run(|rsc| f(resp.msg, rsc));
}
} else {
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 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 user_id: UserId,
pub cache: UserCache,
@@ -10,10 +18,18 @@ pub struct Session {
impl Session {
pub fn new(con: NetHandle, user_id: UserId) -> Self {
Self {
Self(Arc::new(Mutex::new(SessionInner {
cache: UserCache::new(con.clone()),
con,
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 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);
+12 -5
View File
@@ -31,8 +31,7 @@ pub fn start(rsc: &mut Rsc, data: &ClientData) -> WeakWidget {
.add(rsc),
color::DARK,
rsc,
)
.add(rsc);
);
let account = account.clone();
let cert_hex = data
.data
@@ -44,14 +43,22 @@ pub fn start(rsc: &mut Rsc, data: &ClientData) -> WeakWidget {
let cert = decode_hex(&cert_hex).unwrap();
keyring::use_native_store(true).unwrap();
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 cert = cert.clone();
let password = ctx.state.data.password(&account);
let event_sender = rsc.window_event.clone();
rsc.spawn_task(async move |mut ctx| {
let mut fail = |reason: &str| {
let reason = reason.to_string();
ctx.update(move |ctx, rsc| {
button.enable(rsc);
ctx.error(&reason, rsc);
})
};
@@ -91,8 +98,8 @@ pub fn start(rsc: &mut Rsc, data: &ClientData) -> WeakWidget {
return fail("invalid password");
}
};
let session = Session::new(con, user_id);
ctx.update(move |ctx, rsc| {
let session = Session::new(con, user_id);
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");
}
};
let session = Session::new(con, user_id);
ctx.update(move |ctx, rsc| {
let session = Session::new(con, user_id);
main_view(rsc, session).set_ptr(ctx.main_ui, rsc);
ctx.data.create_account(
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::*;
@@ -15,8 +20,8 @@ pub fn view(rsc: &mut Rsc, session: &Session) -> StrongWidget {
fn add_friend_area(rsc: &mut Rsc, session: &Session) -> WeakWidget {
let username_field = field("", "username").add(rsc);
let add = Button::submit("add friend", rsc);
let con = session.con.clone();
rsc.events.register(add, Submit, move |ctx, rsc| {
let con = session.con();
rsc.events.register(add, Submit, move |_, rsc| {
let con = con.clone();
let username = username_field.edit(rsc).take();
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 {
let generate = Button::normal("generate token", rsc);
let con = session.con.clone();
let con = session.con();
let token = wtext("")
.size(30)
.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 {
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| {
let con = con.clone();
let session = session.clone();
// TODO: maybe have rsc.request method that takes in &con?
// need to also handle error tho
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);
if !resp.incoming.is_empty() {
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(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() {
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);
});
@@ -111,3 +139,33 @@ fn friends_list(rsc: &mut Rsc, session: &Session) -> WeakWidget {
});
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)
}
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 {
pub fn error(&self, text: &str, rsc: &mut 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 {
let ptr = loading_area("loading users").add(rsc);
let con = session.con.clone();
let con = session.con();
rsc.events.register(ptr, Draw, move |_, rsc| {
let con = con.clone();
rsc.spawn_task(async move |mut ctx| {
+33 -22
View File
@@ -19,35 +19,46 @@ impl UserCache {
widgets: Default::default(),
}
}
}
pub fn username(&mut self, id: UserId, rsc: &mut Rsc) -> WeakWidget {
let text = if let Some(user) = self.users.get(&id) {
impl Session {
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
} else {
if !self.requests.contains_key(&id) {
let recv = self.con.request_sync(RequestUserInfo { id });
self.requests.insert(id, recv);
}
"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 !s.requests.contains_key(&id) {
s.con
.request_sync(RequestUserInfo { id }, move |resp, rsc| {
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();
}
self.users.insert(*id, info);
}
false
} else {
true
s.users.insert(id, info);
}
});
}
"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) {
reply!(AddFriendResp::AlreadySent);
}
// TODO: just accept in this case?
if user.friends.incoming.contains(&other_id) {
reply!(AddFriendResp::AlreadySent);
}
if other_id == user_id {
reply!(AddFriendResp::CannotAddSelf);
}