Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 38 additions & 96 deletions ui/src/components/conversation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ use dioxus::prelude::*;
use dioxus_free_icons::icons::fa_solid_icons::{FaBars, FaCircleInfo, FaUsers};
use dioxus_free_icons::Icon;
use freenet_scaffold::ComposableState;
use river_core::room_state::member::MemberId;
use river_core::room_state::member_info::MemberInfoV1;
use river_core::room_state::member::{MemberId, MembersDelta};
use river_core::room_state::member_info::{AuthorizedMemberInfo, MemberInfoV1};
use river_core::room_state::message::{
AuthorizedMessageV1, MessageId, MessageV1, MessagesV1, RoomMessageBody,
};
Expand All @@ -33,6 +33,25 @@ use wasm_bindgen::JsCast;
use wasm_bindgen_futures::spawn_local;
use web_sys;

/// Try to build a rejoin delta for the current user in the given room.
/// Returns (None, None) if the user is already a member or ROOMS is busy.
fn try_rejoin_delta(
room_key: &ed25519_dalek::VerifyingKey,
action: &str,
) -> (Option<MembersDelta>, Option<Vec<AuthorizedMemberInfo>>) {
let rooms_guard = ROOMS.try_read();
if let Ok(rooms_read) = rooms_guard {
if let Some(room_data) = rooms_read.map.get(room_key) {
room_data.build_rejoin_delta()
} else {
(None, None)
}
} else {
warn!("ROOMS signal busy during {action}, skipping re-add check");
(None, None)
}
}

/// Context for a reply-in-progress (held in a signal)
#[derive(Clone, PartialEq, Debug)]
struct ReplyContext {
Expand Down Expand Up @@ -737,8 +756,12 @@ pub fn Conversation() -> Element {

// Apply all messages in one delta
if !auth_messages.is_empty() {
let (members_delta, member_info_delta) =
try_rejoin_delta(&current_room, "reaction");
let delta = ChatRoomStateV1Delta {
recent_messages: Some(auth_messages),
members: members_delta,
member_info: member_info_delta,
..Default::default()
};
info!(
Expand Down Expand Up @@ -837,8 +860,12 @@ pub fn Conversation() -> Element {
.await;

let auth_message = AuthorizedMessageV1::with_signature(message, signature);
let (members_delta, member_info_delta) =
try_rejoin_delta(&current_room, "delete");
let delta = ChatRoomStateV1Delta {
recent_messages: Some(vec![auth_message]),
members: members_delta,
member_info: member_info_delta,
..Default::default()
};
info!("Sending delete action");
Expand Down Expand Up @@ -939,8 +966,12 @@ pub fn Conversation() -> Element {
.await;

let auth_message = AuthorizedMessageV1::with_signature(message, signature);
let (members_delta, member_info_delta) =
try_rejoin_delta(&current_room, "edit");
let delta = ChatRoomStateV1Delta {
recent_messages: Some(vec![auth_message]),
members: members_delta,
member_info: member_info_delta,
..Default::default()
};
info!("Sending edit action");
Expand Down Expand Up @@ -1123,100 +1154,11 @@ pub fn Conversation() -> Element {

let auth_message = AuthorizedMessageV1::with_signature(message, signature);

// Check if we need to re-add ourselves (pruned for inactivity)
// Use try_read() instead of read() to avoid RefCell
// re-entrant borrow panics inside spawn_local (see
// AGENTS.md "Dioxus WASM Signal Safety Rules").
let (members_delta, member_info_delta) = {
let rooms_guard = ROOMS.try_read();
if let Ok(rooms_read) = rooms_guard {
if let Some(room_data) = rooms_read.map.get(&current_room) {
let self_vk = room_data.self_sk.verifying_key();
let is_in_members = self_vk == current_room
|| room_data
.room_state
.members
.members
.iter()
.any(|m| m.member.member_vk == self_vk);

if !is_in_members {
if let Some(ref authorized_member) =
room_data.self_authorized_member
{
let current_member_ids: std::collections::HashSet<_> =
room_data
.room_state
.members
.members
.iter()
.map(|m| m.member.id())
.collect();
let mut members_to_add = vec![authorized_member.clone()];
for chain_member in &room_data.invite_chain {
if !current_member_ids
.contains(&chain_member.member.id())
{
members_to_add.push(chain_member.clone());
}
}

// Use stored member_info to preserve nickname, or fall back to "Member"
use river_core::room_state::member_info::{
AuthorizedMemberInfo, MemberInfo,
};
let authorized_info = if let Some(ref stored_info) =
room_data.self_member_info
{
stored_info.clone()
} else {
use river_core::room_state::privacy::SealedBytes;
let member_id = MemberId::from(&self_vk);
let existing_version = room_data
.room_state
.member_info
.member_info
.iter()
.find(|i| i.member_info.member_id == member_id)
.map(|i| i.member_info.version)
.unwrap_or(0);
let member_info = MemberInfo {
member_id,
version: existing_version,
preferred_nickname: SealedBytes::public(
"Member".to_string().into_bytes(),
),
};
AuthorizedMemberInfo::new_with_member_key(
member_info,
&room_data.self_sk,
)
};

(
Some(
river_core::room_state::member::MembersDelta::new(
members_to_add,
),
),
Some(vec![authorized_info]),
)
} else {
(None, None)
}
} else {
(None, None)
}
} else {
(None, None)
}
} else {
// Safe to skip: message still sends without the re-add delta.
// If the user was pruned, the next message send will retry.
warn!("ROOMS signal busy during send, skipping re-add check");
(None, None)
}
};
// Re-add ourselves if pruned for inactivity.
// Uses try_read() to avoid RefCell re-entrant borrow panics
// inside spawn_local (see AGENTS.md "Dioxus WASM Signal Safety Rules").
let (members_delta, member_info_delta) =
try_rejoin_delta(&current_room, "send");

// Build message list. No join event here — join events are
// published at invitation acceptance time (in get_response.rs).
Expand Down
41 changes: 5 additions & 36 deletions ui/src/components/members/member_info_modal/nickname_field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,47 +82,16 @@ pub fn NicknameField(member_info: AuthorizedMemberInfo) -> Element {
let new_authorized_member_info =
AuthorizedMemberInfo::new_with_member_key(new_member_info, &signing_key);
// Check if user needs to re-add themselves (pruned for inactivity)
// Re-add ourselves if pruned (only need members delta;
// the member_info delta is the nickname change itself).
let members_delta = {
let Ok(rooms) = ROOMS.try_read() else {
return;
};
let current_room = CURRENT_ROOM.read();
if let (Some(owner_key), Some(room_data)) = (
current_room.owner_key,
current_room.owner_key.and_then(|k| rooms.map.get(&k)),
) {
let self_vk = signing_key.verifying_key();
let is_in_members = self_vk == owner_key
|| room_data
.room_state
.members
.members
.iter()
.any(|m| m.member.member_vk == self_vk);
if !is_in_members {
if let Some(ref authorized_member) = room_data.self_authorized_member {
let current_member_ids: std::collections::HashSet<_> = room_data
.room_state
.members
.members
.iter()
.map(|m| m.member.id())
.collect();
let mut members_to_add = vec![authorized_member.clone()];
for chain_member in &room_data.invite_chain {
if !current_member_ids.contains(&chain_member.member.id()) {
members_to_add.push(chain_member.clone());
}
}
Some(river_core::room_state::member::MembersDelta::new(
members_to_add,
))
} else {
None
}
} else {
None
}
if let Some(room_data) = current_room.owner_key.and_then(|k| rooms.map.get(&k))
{
room_data.build_rejoin_delta().0
} else {
None
}
Expand Down
Loading
Loading