fix: multiple bugs
* Hard-refresh saves coords before disconnect * Position persisted between sessions * Scaling isn't assumed on initial, it waits for calculations to finish
This commit is contained in:
parent
a96581cbf0
commit
98f38c9714
3 changed files with 99 additions and 8 deletions
|
|
@ -8,16 +8,31 @@ use chattyness_error::AppError;
|
||||||
|
|
||||||
/// Join a channel as an authenticated user.
|
/// Join a channel as an authenticated user.
|
||||||
///
|
///
|
||||||
/// Creates a channel_members entry with default position (400, 300).
|
/// Restores the user's last position if they were previously in the same scene,
|
||||||
|
/// otherwise uses the default position (400, 300).
|
||||||
|
/// Note: channel_id is actually scene_id in this system (scenes are used directly as channels).
|
||||||
pub async fn join_channel<'e>(
|
pub async fn join_channel<'e>(
|
||||||
executor: impl PgExecutor<'e>,
|
executor: impl PgExecutor<'e>,
|
||||||
channel_id: Uuid,
|
channel_id: Uuid,
|
||||||
user_id: Uuid,
|
user_id: Uuid,
|
||||||
) -> Result<ChannelMember, AppError> {
|
) -> Result<ChannelMember, AppError> {
|
||||||
|
// Note: channel_id is actually scene_id in this system
|
||||||
let member = sqlx::query_as::<_, ChannelMember>(
|
let member = sqlx::query_as::<_, ChannelMember>(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO realm.channel_members (channel_id, user_id, position)
|
INSERT INTO realm.channel_members (channel_id, user_id, position)
|
||||||
VALUES ($1, $2, ST_SetSRID(ST_MakePoint(400, 300), 0))
|
SELECT $1, $2, COALESCE(
|
||||||
|
-- Try to restore last position if user was in the same scene
|
||||||
|
-- Note: channel_id = scene_id in this system
|
||||||
|
(SELECT m.last_position
|
||||||
|
FROM realm.memberships m
|
||||||
|
JOIN realm.scenes s ON s.id = $1
|
||||||
|
WHERE m.user_id = $2
|
||||||
|
AND m.realm_id = s.realm_id
|
||||||
|
AND m.last_scene_id = $1
|
||||||
|
AND m.last_position IS NOT NULL),
|
||||||
|
-- Default position
|
||||||
|
ST_SetSRID(ST_MakePoint(400, 300), 0)
|
||||||
|
)
|
||||||
ON CONFLICT (channel_id, user_id) DO UPDATE
|
ON CONFLICT (channel_id, user_id) DO UPDATE
|
||||||
SET joined_at = now()
|
SET joined_at = now()
|
||||||
RETURNING
|
RETURNING
|
||||||
|
|
@ -67,13 +82,40 @@ pub async fn ensure_active_avatar<'e>(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Leave a channel.
|
/// Leave a channel.
|
||||||
|
///
|
||||||
|
/// Saves the user's current position to memberships.last_position before removing them.
|
||||||
|
/// Note: channel_id is actually scene_id in this system (scenes are used directly as channels).
|
||||||
pub async fn leave_channel<'e>(
|
pub async fn leave_channel<'e>(
|
||||||
executor: impl PgExecutor<'e>,
|
executor: impl PgExecutor<'e>,
|
||||||
channel_id: Uuid,
|
channel_id: Uuid,
|
||||||
user_id: Uuid,
|
user_id: Uuid,
|
||||||
) -> Result<(), AppError> {
|
) -> Result<(), AppError> {
|
||||||
|
// Use data-modifying CTEs with RETURNING to ensure all CTEs execute
|
||||||
|
// Note: channel_id is actually scene_id in this system
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
r#"DELETE FROM realm.channel_members WHERE channel_id = $1 AND user_id = $2"#,
|
r#"
|
||||||
|
WITH member_info AS (
|
||||||
|
SELECT cm.position, cm.channel_id as scene_id, s.realm_id
|
||||||
|
FROM realm.channel_members cm
|
||||||
|
JOIN realm.scenes s ON cm.channel_id = s.id
|
||||||
|
WHERE cm.channel_id = $1 AND cm.user_id = $2
|
||||||
|
),
|
||||||
|
save_position AS (
|
||||||
|
UPDATE realm.memberships m
|
||||||
|
SET last_position = mi.position,
|
||||||
|
last_scene_id = mi.scene_id,
|
||||||
|
last_visited_at = now()
|
||||||
|
FROM member_info mi
|
||||||
|
WHERE m.realm_id = mi.realm_id AND m.user_id = $2
|
||||||
|
RETURNING m.user_id
|
||||||
|
),
|
||||||
|
do_delete AS (
|
||||||
|
DELETE FROM realm.channel_members
|
||||||
|
WHERE channel_id = $1 AND user_id = $2
|
||||||
|
RETURNING user_id
|
||||||
|
)
|
||||||
|
SELECT COUNT(*) FROM save_position, do_delete
|
||||||
|
"#,
|
||||||
)
|
)
|
||||||
.bind(channel_id)
|
.bind(channel_id)
|
||||||
.bind(user_id)
|
.bind(user_id)
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,9 @@ pub fn RealmSceneViewer(
|
||||||
let offset_x = StoredValue::new(0.0_f64);
|
let offset_x = StoredValue::new(0.0_f64);
|
||||||
let offset_y = StoredValue::new(0.0_f64);
|
let offset_y = StoredValue::new(0.0_f64);
|
||||||
|
|
||||||
|
// Signal to track when scale factors have been properly calculated
|
||||||
|
let (scales_ready, set_scales_ready) = signal(false);
|
||||||
|
|
||||||
// Handle canvas click for movement or prop pickup (on avatar canvas - topmost layer)
|
// Handle canvas click for movement or prop pickup (on avatar canvas - topmost layer)
|
||||||
#[cfg(feature = "hydrate")]
|
#[cfg(feature = "hydrate")]
|
||||||
let on_canvas_click = {
|
let on_canvas_click = {
|
||||||
|
|
@ -125,9 +128,9 @@ pub fn RealmSceneViewer(
|
||||||
let scene_x = scene_x.max(0.0).min(scene_width as f64);
|
let scene_x = scene_x.max(0.0).min(scene_width as f64);
|
||||||
let scene_y = scene_y.max(0.0).min(scene_height as f64);
|
let scene_y = scene_y.max(0.0).min(scene_height as f64);
|
||||||
|
|
||||||
// Check if click is within 32px of any loose prop
|
// Check if click is within 40px of any loose prop
|
||||||
let current_props = loose_props.get();
|
let current_props = loose_props.get();
|
||||||
let prop_click_radius = 32.0;
|
let prop_click_radius = 40.0;
|
||||||
let mut clicked_prop: Option<Uuid> = None;
|
let mut clicked_prop: Option<Uuid> = None;
|
||||||
|
|
||||||
for prop in ¤t_props {
|
for prop in ¤t_props {
|
||||||
|
|
@ -220,6 +223,9 @@ pub fn RealmSceneViewer(
|
||||||
offset_x.set_value(draw_x);
|
offset_x.set_value(draw_x);
|
||||||
offset_y.set_value(draw_y);
|
offset_y.set_value(draw_y);
|
||||||
|
|
||||||
|
// Signal that scale factors are ready
|
||||||
|
set_scales_ready.set(true);
|
||||||
|
|
||||||
if let Ok(Some(ctx)) = canvas_el.get_context("2d") {
|
if let Ok(Some(ctx)) = canvas_el.get_context("2d") {
|
||||||
let ctx: web_sys::CanvasRenderingContext2d =
|
let ctx: web_sys::CanvasRenderingContext2d =
|
||||||
ctx.dyn_into::<web_sys::CanvasRenderingContext2d>().unwrap();
|
ctx.dyn_into::<web_sys::CanvasRenderingContext2d>().unwrap();
|
||||||
|
|
@ -271,6 +277,11 @@ pub fn RealmSceneViewer(
|
||||||
let current_members = members.get();
|
let current_members = members.get();
|
||||||
let current_bubbles = active_bubbles.get();
|
let current_bubbles = active_bubbles.get();
|
||||||
|
|
||||||
|
// Skip drawing if scale factors haven't been calculated yet
|
||||||
|
if !scales_ready.get() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let Some(canvas) = avatar_canvas_ref.get() else {
|
let Some(canvas) = avatar_canvas_ref.get() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
@ -335,6 +346,11 @@ pub fn RealmSceneViewer(
|
||||||
// Track loose_props signal
|
// Track loose_props signal
|
||||||
let current_props = loose_props.get();
|
let current_props = loose_props.get();
|
||||||
|
|
||||||
|
// Skip drawing if scale factors haven't been calculated yet
|
||||||
|
if !scales_ready.get() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let Some(canvas) = props_canvas_ref.get() else {
|
let Some(canvas) = props_canvas_ref.get() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
@ -452,7 +468,7 @@ fn draw_avatars(
|
||||||
let x = member.member.position_x * scale_x + offset_x;
|
let x = member.member.position_x * scale_x + offset_x;
|
||||||
let y = member.member.position_y * scale_y + offset_y;
|
let y = member.member.position_y * scale_y + offset_y;
|
||||||
|
|
||||||
let avatar_size = 48.0 * scale_x.min(scale_y);
|
let avatar_size = 60.0 * scale_x.min(scale_y);
|
||||||
|
|
||||||
// Draw avatar placeholder circle
|
// Draw avatar placeholder circle
|
||||||
ctx.begin_path();
|
ctx.begin_path();
|
||||||
|
|
@ -544,7 +560,7 @@ fn draw_speech_bubbles(
|
||||||
current_time_ms: i64,
|
current_time_ms: i64,
|
||||||
) {
|
) {
|
||||||
let scale = scale_x.min(scale_y);
|
let scale = scale_x.min(scale_y);
|
||||||
let avatar_size = 48.0 * scale;
|
let avatar_size = 60.0 * scale;
|
||||||
let max_bubble_width = 200.0 * scale;
|
let max_bubble_width = 200.0 * scale;
|
||||||
let padding = 8.0 * scale;
|
let padding = 8.0 * scale;
|
||||||
let font_size = 12.0 * scale;
|
let font_size = 12.0 * scale;
|
||||||
|
|
@ -704,7 +720,7 @@ fn draw_loose_props(
|
||||||
offset_x: f64,
|
offset_x: f64,
|
||||||
offset_y: f64,
|
offset_y: f64,
|
||||||
) {
|
) {
|
||||||
let prop_size = 48.0 * scale_x.min(scale_y);
|
let prop_size = 60.0 * scale_x.min(scale_y);
|
||||||
|
|
||||||
for prop in props {
|
for prop in props {
|
||||||
let x = prop.position_x * scale_x + offset_x;
|
let x = prop.position_x * scale_x + offset_x;
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,9 @@ pub fn RealmPage() -> impl IntoView {
|
||||||
// Loose props state
|
// Loose props state
|
||||||
let (loose_props, set_loose_props) = signal(Vec::<LooseProp>::new());
|
let (loose_props, set_loose_props) = signal(Vec::<LooseProp>::new());
|
||||||
|
|
||||||
|
// Track user's current position for saving on beforeunload
|
||||||
|
let (current_position, set_current_position) = signal((400.0_f64, 300.0_f64));
|
||||||
|
|
||||||
let realm_data = LocalResource::new(move || {
|
let realm_data = LocalResource::new(move || {
|
||||||
let slug = slug.get();
|
let slug = slug.get();
|
||||||
async move {
|
async move {
|
||||||
|
|
@ -233,6 +236,8 @@ pub fn RealmPage() -> impl IntoView {
|
||||||
// Handle position update via WebSocket
|
// Handle position update via WebSocket
|
||||||
#[cfg(feature = "hydrate")]
|
#[cfg(feature = "hydrate")]
|
||||||
let on_move = Callback::new(move |(x, y): (f64, f64)| {
|
let on_move = Callback::new(move |(x, y): (f64, f64)| {
|
||||||
|
// Track position for saving on beforeunload
|
||||||
|
set_current_position.set((x, y));
|
||||||
ws_sender.with_value(|sender| {
|
ws_sender.with_value(|sender| {
|
||||||
if let Some(send_fn) = sender {
|
if let Some(send_fn) = sender {
|
||||||
send_fn(ClientMessage::UpdatePosition { x, y });
|
send_fn(ClientMessage::UpdatePosition { x, y });
|
||||||
|
|
@ -359,6 +364,34 @@ pub fn RealmPage() -> impl IntoView {
|
||||||
// Store the closure for cleanup
|
// Store the closure for cleanup
|
||||||
*closure_holder_clone.borrow_mut() = Some(closure);
|
*closure_holder_clone.borrow_mut() = Some(closure);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Save position on page unload (beforeunload event)
|
||||||
|
Effect::new({
|
||||||
|
let ws_sender = ws_sender.clone();
|
||||||
|
move |_| {
|
||||||
|
let Some(window) = web_sys::window() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let handler = Closure::<dyn Fn(web_sys::BeforeUnloadEvent)>::new({
|
||||||
|
let ws_sender = ws_sender.clone();
|
||||||
|
move |_: web_sys::BeforeUnloadEvent| {
|
||||||
|
let (x, y) = current_position.get_untracked();
|
||||||
|
ws_sender.with_value(|sender| {
|
||||||
|
if let Some(send_fn) = sender {
|
||||||
|
send_fn(ClientMessage::UpdatePosition { x, y });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let _ = window.add_event_listener_with_callback(
|
||||||
|
"beforeunload",
|
||||||
|
handler.as_ref().unchecked_ref(),
|
||||||
|
);
|
||||||
|
handler.forget();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Callback for chat focus changes
|
// Callback for chat focus changes
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue