diff --git a/Cargo.lock b/Cargo.lock
index fc9fcc0..617c17e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2404,7 +2404,7 @@ dependencies = [
"tracing-forest",
"tracing-log",
"tracing-subscriber",
- "uuid 0.8.2",
+ "uuid 1.3.0",
"webpki-roots",
]
@@ -2530,12 +2530,6 @@ dependencies = [
"digest 0.10.6",
]
-[[package]]
-name = "md5"
-version = "0.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
-
[[package]]
name = "memchr"
version = "2.5.0"
@@ -4404,9 +4398,6 @@ name = "uuid"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
-dependencies = [
- "md5",
-]
[[package]]
name = "uuid"
@@ -4415,6 +4406,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79"
dependencies = [
"getrandom 0.2.8",
+ "md-5",
]
[[package]]
diff --git a/app/queries/get_user_avatar.graphql b/app/queries/get_user_avatar.graphql
new file mode 100644
index 0000000..28d2bd8
--- /dev/null
+++ b/app/queries/get_user_avatar.graphql
@@ -0,0 +1,6 @@
+query GetUserAvatar($id: String!) {
+ user(userId: $id) {
+ id
+ avatar
+ }
+}
diff --git a/app/src/components/app.rs b/app/src/components/app.rs
index 3d03afb..57c8b0f 100644
--- a/app/src/components/app.rs
+++ b/app/src/components/app.rs
@@ -1,5 +1,7 @@
use crate::{
components::{
+ avatar::ShowAvatar,
+ avatar_cache::*,
change_password::ChangePasswordForm,
create_group::CreateGroupForm,
create_user::CreateUserForm,
@@ -136,8 +138,19 @@ impl Component for App {
let link = ctx.link().clone();
let is_admin = self.is_admin();
let password_reset_enabled = self.password_reset_enabled;
+ let mode = self
+ .user_info
+ .clone()
+ .map(|(username, is_admin)| {
+ if is_admin {
+ CacheMode::AllUsers
+ } else {
+ CacheMode::SingleUser(username)
+ }
+ })
+ .unwrap_or(CacheMode::None);
html! {
-
+
{self.view_banner(ctx)}
@@ -149,7 +162,7 @@ impl Component for App {
{self.view_footer()}
-
+
}
}
}
@@ -312,15 +325,7 @@ impl App {
id="dropdownUser"
data-bs-toggle="dropdown"
aria-expanded="false">
-
+
{user_id}
diff --git a/app/src/components/avatar.rs b/app/src/components/avatar.rs
new file mode 100644
index 0000000..c5ea1ab
--- /dev/null
+++ b/app/src/components/avatar.rs
@@ -0,0 +1,39 @@
+use crate::components::avatar_cache::AvatarCacheContext;
+use yew::{function_component, prelude::*};
+
+#[derive(Properties, PartialEq)]
+pub struct Props {
+ pub username: String,
+ pub width: i32,
+ pub height: i32,
+}
+
+#[function_component(ShowAvatar)]
+pub fn show_avatar(props: &Props) -> Html {
+ let cache = use_context::().expect("no ctx found");
+ let avatar = cache
+ .avatars
+ .get(&props.username)
+ .map(|val| val.clone())
+ .unwrap_or(None);
+ match avatar {
+ Some(avatar) => html! {
+
+ },
+ None => html! {
+
+ },
+ }
+}
diff --git a/app/src/components/avatar_cache.rs b/app/src/components/avatar_cache.rs
new file mode 100644
index 0000000..0c1c316
--- /dev/null
+++ b/app/src/components/avatar_cache.rs
@@ -0,0 +1,148 @@
+use crate::infra::api::HostService;
+use anyhow::Result;
+use gloo_console::error;
+use graphql_client::GraphQLQuery;
+use std::{collections::HashMap, rc::Rc};
+use yew::prelude::*;
+
+#[derive(GraphQLQuery)]
+#[graphql(
+ schema_path = "../schema.graphql",
+ query_path = "queries/get_user_avatar.graphql",
+ response_derives = "Debug",
+ custom_scalars_module = "crate::infra::graphql"
+)]
+pub struct GetUserAvatar;
+
+#[derive(GraphQLQuery)]
+#[graphql(
+ schema_path = "../schema.graphql",
+ query_path = "queries/list_users.graphql",
+ response_derives = "Debug",
+ variables_derives = "Clone",
+ custom_scalars_module = "crate::infra::graphql"
+)]
+pub struct ListUserNames;
+
+#[derive(PartialEq, Clone)]
+pub enum CacheAction {
+ Clear,
+ AddAvatar((String, Option)),
+}
+
+#[derive(PartialEq, Eq, Clone)]
+pub struct AvatarCache {
+ pub avatars: HashMap>,
+}
+
+impl Reducible for AvatarCache {
+ type Action = CacheAction;
+
+ fn reduce(self: Rc, action: Self::Action) -> Rc {
+ match action {
+ CacheAction::AddAvatar((username, avatar)) => {
+ let mut avatars = self.avatars.clone();
+ avatars.insert(username, avatar);
+ AvatarCache { avatars }.into()
+ }
+ CacheAction::Clear => AvatarCache {
+ avatars: HashMap::new(),
+ }
+ .into(),
+ }
+ }
+}
+
+pub type AvatarCacheContext = UseReducerHandle;
+
+#[derive(PartialEq, Clone)]
+pub enum CacheMode {
+ AllUsers,
+ SingleUser(String),
+ None,
+}
+
+#[derive(Properties, PartialEq)]
+pub struct AvatarCacheProviderProps {
+ #[prop_or_default]
+ pub children: Children,
+
+ pub mode: CacheMode,
+}
+
+#[function_component(AvatarCacheProvider)]
+pub fn avatar_cache_provider(props: &AvatarCacheProviderProps) -> Html {
+ let cache = use_reducer(|| AvatarCache {
+ avatars: HashMap::new(),
+ });
+ {
+ let cache = cache.clone();
+ let mode = props.mode.clone();
+ use_effect_with_deps(
+ move |mode| {
+ match mode {
+ CacheMode::None => cache.dispatch(CacheAction::Clear),
+ CacheMode::AllUsers => {
+ let cache = cache.clone();
+ wasm_bindgen_futures::spawn_local(async move {
+ let result = fetch_all_avatars(cache).await;
+ if let Err(e) = result {
+ error!(&format!("Could not fetch all avatars: {e:#}"))
+ }
+ });
+ }
+ CacheMode::SingleUser(username) => {
+ let cache = cache.clone();
+ let username = username.clone();
+ wasm_bindgen_futures::spawn_local(async move {
+ let result = HostService::graphql_query::(
+ get_user_avatar::Variables { id: username },
+ "Error trying to fetch user avatar",
+ )
+ .await;
+ if let Ok(response) = result {
+ cache.dispatch(CacheAction::AddAvatar((
+ response.user.id,
+ response.user.avatar,
+ )))
+ }
+ });
+ }
+ };
+ move || cache.dispatch(CacheAction::Clear)
+ },
+ mode,
+ )
+ }
+
+ html! {
+ context={cache}>
+ {props.children.clone()}
+ >
+ }
+}
+
+async fn fetch_all_avatars(cache: UseReducerHandle) -> Result<()> {
+ let response = HostService::graphql_query::(
+ list_user_names::Variables { filters: None },
+ "Error trying to fetch user list",
+ )
+ .await?;
+ for user in &response.users {
+ let result = HostService::graphql_query::(
+ get_user_avatar::Variables {
+ id: user.id.clone(),
+ },
+ "Error trying to fetch user avatar",
+ )
+ .await;
+ if let Ok(response) = result {
+ cache.dispatch(CacheAction::AddAvatar((
+ response.user.id,
+ response.user.avatar,
+ )));
+ }
+ }
+
+ return Ok(());
+}
diff --git a/app/src/components/mod.rs b/app/src/components/mod.rs
index f78dcf9..6690e04 100644
--- a/app/src/components/mod.rs
+++ b/app/src/components/mod.rs
@@ -1,6 +1,8 @@
pub mod add_group_member;
pub mod add_user_to_group;
pub mod app;
+pub mod avatar;
+pub mod avatar_cache;
pub mod change_password;
pub mod create_group;
pub mod create_user;
diff --git a/app/src/components/user_details_form.rs b/app/src/components/user_details_form.rs
index 3ad9530..b231026 100644
--- a/app/src/components/user_details_form.rs
+++ b/app/src/components/user_details_form.rs
@@ -1,6 +1,7 @@
use std::str::FromStr;
use crate::{
+ components::avatar_cache::*,
components::user_details::User,
infra::common_component::{CommonComponent, CommonComponentParts},
};
@@ -12,6 +13,7 @@ use gloo_file::{
use graphql_client::GraphQLQuery;
use validator_derive::Validate;
use web_sys::{FileList, HtmlInputElement, InputEvent};
+use yew::context::ContextHandle;
use yew::prelude::*;
use yew_form_derive::Model;
@@ -72,6 +74,8 @@ pub struct UserDetailsForm {
/// True if we just successfully updated the user, to display a success message.
just_updated: bool,
user: User,
+ avatar_cache: AvatarCacheContext,
+ _context_handle: ContextHandle,
}
pub enum Msg {
@@ -156,6 +160,10 @@ impl Component for UserDetailsForm {
first_name: ctx.props().user.first_name.clone(),
last_name: ctx.props().user.last_name.clone(),
};
+ let (avatar_cache, _context_handle) = ctx
+ .link()
+ .context(Callback::noop())
+ .expect("No Message Context Provided");
Self {
common: CommonComponentParts::::create(),
form: yew_form::Form::new(model),
@@ -163,6 +171,8 @@ impl Component for UserDetailsForm {
just_updated: false,
reader: None,
user: ctx.props().user.clone(),
+ avatar_cache,
+ _context_handle,
}
}
@@ -402,6 +412,10 @@ impl UserDetailsForm {
self.user.avatar = Some(avatar);
}
self.just_updated = true;
+ self.avatar_cache.dispatch(CacheAction::AddAvatar((
+ self.user.id.clone(),
+ self.user.avatar.clone(),
+ )));
Ok(true)
}
diff --git a/app/static/style.css b/app/static/style.css
index be2db44..26d24a8 100644
--- a/app/static/style.css
+++ b/app/static/style.css
@@ -29,4 +29,8 @@ html.dark .nav-link {
.nav-link {
color: #212529
+}
+
+.avatar {
+ border-radius: 50%;
}
\ No newline at end of file