ui: inform guests of restricted interfaces
This commit is contained in:
parent
1f922f8221
commit
39750c1d82
8 changed files with 137 additions and 49 deletions
|
|
@ -1829,6 +1829,9 @@ pub struct ChannelMemberInfo {
|
||||||
/// Current emotion slot (0-9)
|
/// Current emotion slot (0-9)
|
||||||
pub current_emotion: i16,
|
pub current_emotion: i16,
|
||||||
pub joined_at: DateTime<Utc>,
|
pub joined_at: DateTime<Utc>,
|
||||||
|
/// Whether this user is a guest (has the 'guest' tag)
|
||||||
|
#[serde(default)]
|
||||||
|
pub is_guest: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Request to update position in a channel.
|
/// Request to update position in a channel.
|
||||||
|
|
|
||||||
|
|
@ -176,7 +176,8 @@ pub async fn get_channel_members<'e>(
|
||||||
cm.is_moving,
|
cm.is_moving,
|
||||||
cm.is_afk,
|
cm.is_afk,
|
||||||
COALESCE(aa.current_emotion, 0::smallint) as current_emotion,
|
COALESCE(aa.current_emotion, 0::smallint) as current_emotion,
|
||||||
cm.joined_at
|
cm.joined_at,
|
||||||
|
COALESCE('guest' = ANY(u.tags), false) as is_guest
|
||||||
FROM scene.instance_members cm
|
FROM scene.instance_members cm
|
||||||
LEFT JOIN auth.users u ON cm.user_id = u.id
|
LEFT JOIN auth.users u ON cm.user_id = u.id
|
||||||
LEFT JOIN auth.guest_sessions gs ON cm.guest_session_id = gs.id
|
LEFT JOIN auth.guest_sessions gs ON cm.guest_session_id = gs.id
|
||||||
|
|
@ -214,7 +215,8 @@ pub async fn get_channel_member<'e>(
|
||||||
cm.is_moving,
|
cm.is_moving,
|
||||||
cm.is_afk,
|
cm.is_afk,
|
||||||
COALESCE(aa.current_emotion, 0::smallint) as current_emotion,
|
COALESCE(aa.current_emotion, 0::smallint) as current_emotion,
|
||||||
cm.joined_at
|
cm.joined_at,
|
||||||
|
COALESCE('guest' = ANY(u.tags), false) as is_guest
|
||||||
FROM scene.instance_members cm
|
FROM scene.instance_members cm
|
||||||
LEFT JOIN auth.users u ON cm.user_id = u.id
|
LEFT JOIN auth.users u ON cm.user_id = u.id
|
||||||
LEFT JOIN auth.active_avatars aa ON cm.user_id = aa.user_id AND aa.realm_id = $3
|
LEFT JOIN auth.active_avatars aa ON cm.user_id = aa.user_id AND aa.realm_id = $3
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ use chattyness_db::models::{AvatarWithPaths, InventoryItem};
|
||||||
#[cfg(feature = "hydrate")]
|
#[cfg(feature = "hydrate")]
|
||||||
use chattyness_db::ws_messages::ClientMessage;
|
use chattyness_db::ws_messages::ClientMessage;
|
||||||
|
|
||||||
|
use super::modals::GuestLockedOverlay;
|
||||||
use super::ws_client::WsSenderStorage;
|
use super::ws_client::WsSenderStorage;
|
||||||
#[cfg(feature = "hydrate")]
|
#[cfg(feature = "hydrate")]
|
||||||
use crate::utils::normalize_asset_path;
|
use crate::utils::normalize_asset_path;
|
||||||
|
|
@ -216,6 +217,7 @@ fn RenderedPreview(#[prop(into)] avatar: Signal<Option<AvatarWithPaths>>) -> imp
|
||||||
/// - `realm_slug`: Current realm slug for API calls
|
/// - `realm_slug`: Current realm slug for API calls
|
||||||
/// - `on_avatar_update`: Callback when avatar is updated
|
/// - `on_avatar_update`: Callback when avatar is updated
|
||||||
/// - `ws_sender`: WebSocket sender for broadcasting avatar changes
|
/// - `ws_sender`: WebSocket sender for broadcasting avatar changes
|
||||||
|
/// - `is_guest`: Whether the current user is a guest (shows locked overlay)
|
||||||
#[component]
|
#[component]
|
||||||
pub fn AvatarEditorPopup(
|
pub fn AvatarEditorPopup(
|
||||||
#[prop(into)] open: Signal<bool>,
|
#[prop(into)] open: Signal<bool>,
|
||||||
|
|
@ -224,7 +226,11 @@ pub fn AvatarEditorPopup(
|
||||||
#[prop(into)] realm_slug: Signal<String>,
|
#[prop(into)] realm_slug: Signal<String>,
|
||||||
on_avatar_update: Callback<AvatarWithPaths>,
|
on_avatar_update: Callback<AvatarWithPaths>,
|
||||||
ws_sender: WsSenderStorage,
|
ws_sender: WsSenderStorage,
|
||||||
|
/// Whether the current user is a guest. Guests see a locked overlay.
|
||||||
|
#[prop(optional, into)]
|
||||||
|
is_guest: Option<Signal<bool>>,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
|
let is_guest = is_guest.unwrap_or_else(|| Signal::derive(|| false));
|
||||||
// Tab state
|
// Tab state
|
||||||
let (active_tab, set_active_tab) = signal(EditorTab::BaseLayers);
|
let (active_tab, set_active_tab) = signal(EditorTab::BaseLayers);
|
||||||
|
|
||||||
|
|
@ -798,6 +804,11 @@ pub fn AvatarEditorPopup(
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
// Guest locked overlay
|
||||||
|
<Show when=move || is_guest.get()>
|
||||||
|
<GuestLockedOverlay />
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
// Context menu
|
// Context menu
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use chattyness_db::models::{InventoryItem, PublicProp};
|
||||||
#[cfg(feature = "hydrate")]
|
#[cfg(feature = "hydrate")]
|
||||||
use chattyness_db::ws_messages::ClientMessage;
|
use chattyness_db::ws_messages::ClientMessage;
|
||||||
|
|
||||||
use super::modals::Modal;
|
use super::modals::{GuestLockedOverlay, Modal};
|
||||||
use super::tabs::{Tab, TabBar};
|
use super::tabs::{Tab, TabBar};
|
||||||
use super::ws_client::WsSender;
|
use super::ws_client::WsSender;
|
||||||
|
|
||||||
|
|
@ -24,13 +24,18 @@ use super::ws_client::WsSender;
|
||||||
/// - `on_close`: Callback when popup should close
|
/// - `on_close`: Callback when popup should close
|
||||||
/// - `ws_sender`: WebSocket sender for dropping props
|
/// - `ws_sender`: WebSocket sender for dropping props
|
||||||
/// - `realm_slug`: Current realm slug for fetching realm props
|
/// - `realm_slug`: Current realm slug for fetching realm props
|
||||||
|
/// - `is_guest`: Whether the current user is a guest (shows locked overlay)
|
||||||
#[component]
|
#[component]
|
||||||
pub fn InventoryPopup(
|
pub fn InventoryPopup(
|
||||||
#[prop(into)] open: Signal<bool>,
|
#[prop(into)] open: Signal<bool>,
|
||||||
on_close: Callback<()>,
|
on_close: Callback<()>,
|
||||||
ws_sender: StoredValue<Option<WsSender>, LocalStorage>,
|
ws_sender: StoredValue<Option<WsSender>, LocalStorage>,
|
||||||
#[prop(into)] realm_slug: Signal<String>,
|
#[prop(into)] realm_slug: Signal<String>,
|
||||||
|
/// Whether the current user is a guest. Guests see a locked overlay.
|
||||||
|
#[prop(optional, into)]
|
||||||
|
is_guest: Option<Signal<bool>>,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
|
let is_guest = is_guest.unwrap_or_else(|| Signal::derive(|| false));
|
||||||
// Tab state
|
// Tab state
|
||||||
let (active_tab, set_active_tab) = signal("my_inventory");
|
let (active_tab, set_active_tab) = signal("my_inventory");
|
||||||
|
|
||||||
|
|
@ -238,6 +243,7 @@ pub fn InventoryPopup(
|
||||||
max_width="max-w-2xl"
|
max_width="max-w-2xl"
|
||||||
class="max-h-[80vh] flex flex-col"
|
class="max-h-[80vh] flex flex-col"
|
||||||
>
|
>
|
||||||
|
<div class="relative flex-1 flex flex-col">
|
||||||
// Tab bar
|
// Tab bar
|
||||||
<TabBar
|
<TabBar
|
||||||
tabs=vec![
|
tabs=vec![
|
||||||
|
|
@ -286,6 +292,12 @@ pub fn InventoryPopup(
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
// Guest locked overlay
|
||||||
|
<Show when=move || is_guest.get()>
|
||||||
|
<GuestLockedOverlay />
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -314,3 +314,45 @@ pub fn ConfirmModal(
|
||||||
</Show>
|
</Show>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Guest Locked Overlay
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Overlay displayed when a feature is restricted to registered users.
|
||||||
|
///
|
||||||
|
/// Shows a semi-transparent backdrop with a padlock icon and diagonal
|
||||||
|
/// "Registered Users" text. Designed to be placed inside a modal container
|
||||||
|
/// with `position: relative`.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// <div class="relative">
|
||||||
|
/// // Modal content here
|
||||||
|
/// <GuestLockedOverlay />
|
||||||
|
/// </div>
|
||||||
|
/// ```
|
||||||
|
#[component]
|
||||||
|
pub fn GuestLockedOverlay() -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 z-50 flex items-center justify-center bg-gray-900/80 backdrop-blur-sm rounded-lg"
|
||||||
|
role="alert"
|
||||||
|
aria-label="This feature is restricted to registered users"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col items-center gap-4 transform -rotate-12">
|
||||||
|
<img
|
||||||
|
src="/icons/padlock.svg"
|
||||||
|
alt=""
|
||||||
|
class="w-16 h-16 text-gray-400"
|
||||||
|
style="filter: invert(60%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(90%) contrast(90%);"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<span class="text-2xl font-bold text-gray-400 tracking-wider uppercase whitespace-nowrap">
|
||||||
|
"Registered Users"
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,10 +56,14 @@ pub fn RealmSceneViewer(
|
||||||
/// Current user's guest_session_id (for context menu filtering).
|
/// Current user's guest_session_id (for context menu filtering).
|
||||||
#[prop(optional, into)]
|
#[prop(optional, into)]
|
||||||
current_guest_session_id: Option<Signal<Option<Uuid>>>,
|
current_guest_session_id: Option<Signal<Option<Uuid>>>,
|
||||||
|
/// Whether the current user is a guest (guests cannot use context menu).
|
||||||
|
#[prop(optional, into)]
|
||||||
|
is_guest: Option<Signal<bool>>,
|
||||||
/// Callback when whisper is requested on a member.
|
/// Callback when whisper is requested on a member.
|
||||||
#[prop(optional, into)]
|
#[prop(optional, into)]
|
||||||
on_whisper_request: Option<Callback<String>>,
|
on_whisper_request: Option<Callback<String>>,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
|
let is_guest = is_guest.unwrap_or_else(|| Signal::derive(|| false));
|
||||||
// Use default settings if none provided
|
// Use default settings if none provided
|
||||||
let settings = settings.unwrap_or_else(|| Signal::derive(ViewerSettings::default));
|
let settings = settings.unwrap_or_else(|| Signal::derive(ViewerSettings::default));
|
||||||
let dimensions = parse_bounds_dimensions(&scene.bounds_wkt);
|
let dimensions = parse_bounds_dimensions(&scene.bounds_wkt);
|
||||||
|
|
@ -183,14 +187,19 @@ pub fn RealmSceneViewer(
|
||||||
move |ev: web_sys::MouseEvent| {
|
move |ev: web_sys::MouseEvent| {
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
|
|
||||||
// Get click position
|
// Guests cannot message other users - don't show context menu
|
||||||
let client_x = ev.client_x() as f64;
|
if is_guest.get() {
|
||||||
let client_y = ev.client_y() as f64;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Get current user identity for filtering
|
// Get current user identity for filtering
|
||||||
let my_user_id = current_user_id.map(|s| s.get()).flatten();
|
let my_user_id = current_user_id.map(|s| s.get()).flatten();
|
||||||
let my_guest_session_id = current_guest_session_id.map(|s| s.get()).flatten();
|
let my_guest_session_id = current_guest_session_id.map(|s| s.get()).flatten();
|
||||||
|
|
||||||
|
// Get click position
|
||||||
|
let client_x = ev.client_x() as f64;
|
||||||
|
let client_y = ev.client_y() as f64;
|
||||||
|
|
||||||
// Query all avatar canvases and check for hit
|
// Query all avatar canvases and check for hit
|
||||||
let document = web_sys::window().unwrap().document().unwrap();
|
let document = web_sys::window().unwrap().document().unwrap();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,8 @@ pub struct ChannelMemberInfo {
|
||||||
pub guest_session_id: Option<uuid::Uuid>,
|
pub guest_session_id: Option<uuid::Uuid>,
|
||||||
/// The user's display name.
|
/// The user's display name.
|
||||||
pub display_name: String,
|
pub display_name: String,
|
||||||
|
/// Whether this user is a guest (has the 'guest' tag).
|
||||||
|
pub is_guest: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// WebSocket error info for UI display.
|
/// WebSocket error info for UI display.
|
||||||
|
|
@ -240,6 +242,7 @@ pub fn use_channel_websocket(
|
||||||
user_id: member.user_id,
|
user_id: member.user_id,
|
||||||
guest_session_id: member.guest_session_id,
|
guest_session_id: member.guest_session_id,
|
||||||
display_name: member.display_name.clone(),
|
display_name: member.display_name.clone(),
|
||||||
|
is_guest: member.is_guest,
|
||||||
};
|
};
|
||||||
callback.run(info);
|
callback.run(info);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,8 @@ pub fn RealmPage() -> impl IntoView {
|
||||||
// Current user identity (received from WebSocket Welcome message)
|
// Current user identity (received from WebSocket Welcome message)
|
||||||
let (current_user_id, set_current_user_id) = signal(Option::<Uuid>::None);
|
let (current_user_id, set_current_user_id) = signal(Option::<Uuid>::None);
|
||||||
let (current_guest_session_id, set_current_guest_session_id) = signal(Option::<Uuid>::None);
|
let (current_guest_session_id, set_current_guest_session_id) = signal(Option::<Uuid>::None);
|
||||||
|
// Whether the current user is a guest (has the 'guest' tag)
|
||||||
|
let (is_guest, set_is_guest) = signal(false);
|
||||||
|
|
||||||
// Whisper target - when set, triggers pre-fill in ChatInput
|
// Whisper target - when set, triggers pre-fill in ChatInput
|
||||||
let (whisper_target, set_whisper_target) = signal(Option::<String>::None);
|
let (whisper_target, set_whisper_target) = signal(Option::<String>::None);
|
||||||
|
|
@ -313,6 +315,7 @@ pub fn RealmPage() -> impl IntoView {
|
||||||
set_current_user_id.set(info.user_id);
|
set_current_user_id.set(info.user_id);
|
||||||
set_current_guest_session_id.set(info.guest_session_id);
|
set_current_guest_session_id.set(info.guest_session_id);
|
||||||
set_current_display_name.set(info.display_name.clone());
|
set_current_display_name.set(info.display_name.clone());
|
||||||
|
set_is_guest.set(info.is_guest);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Callback for WebSocket errors (whisper failures, etc.)
|
// Callback for WebSocket errors (whisper failures, etc.)
|
||||||
|
|
@ -772,6 +775,7 @@ pub fn RealmPage() -> impl IntoView {
|
||||||
fading_members=Signal::derive(move || fading_members.get())
|
fading_members=Signal::derive(move || fading_members.get())
|
||||||
current_user_id=Signal::derive(move || current_user_id.get())
|
current_user_id=Signal::derive(move || current_user_id.get())
|
||||||
current_guest_session_id=Signal::derive(move || current_guest_session_id.get())
|
current_guest_session_id=Signal::derive(move || current_guest_session_id.get())
|
||||||
|
is_guest=Signal::derive(move || is_guest.get())
|
||||||
on_whisper_request=on_whisper_request_cb
|
on_whisper_request=on_whisper_request_cb
|
||||||
/>
|
/>
|
||||||
<div class="absolute bottom-0 left-0 right-0 z-10 pb-4 px-4 pointer-events-none">
|
<div class="absolute bottom-0 left-0 right-0 z-10 pb-4 px-4 pointer-events-none">
|
||||||
|
|
@ -823,6 +827,7 @@ pub fn RealmPage() -> impl IntoView {
|
||||||
})
|
})
|
||||||
ws_sender=ws_sender_for_inv
|
ws_sender=ws_sender_for_inv
|
||||||
realm_slug=Signal::derive(move || slug.get())
|
realm_slug=Signal::derive(move || slug.get())
|
||||||
|
is_guest=Signal::derive(move || is_guest.get())
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -867,6 +872,7 @@ pub fn RealmPage() -> impl IntoView {
|
||||||
set_skin_preview_path.set(updated.skin_layer[4].clone());
|
set_skin_preview_path.set(updated.skin_layer[4].clone());
|
||||||
})
|
})
|
||||||
ws_sender=ws_sender_for_avatar
|
ws_sender=ws_sender_for_avatar
|
||||||
|
is_guest=Signal::derive(move || is_guest.get())
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue