Follow-up from PR #278 skeptical review (Important #2).
Problem
already_member = target_room_data.is_some_and(|rd| rd.can_participate().is_ok()). can_participate() returns Err(UserBanned) for banned users, so already_member = false — the Accept button shows the normal "Accept invitation" label. Clicking it tries present_invitation which proceeds to the contract layer, where the join deterministically fails.
Suggested fix
Distinguish ban from not-a-member in the card's button state:
let card_state = if let Some(rd) = target_room_data {
match rd.can_participate() {
Ok(_) => CardState::AlreadyMember,
Err(CanParticipateError::UserBanned) => CardState::Banned,
Err(_) => CardState::Joinable,
}
} else {
CardState::Joinable
};
Render Banned as a disabled button reading "You're banned from this room" with a destructive-tinted color.
Severity
UX nit, not a correctness bug. The contract correctly rejects the join attempt; this is just about giving the user accurate information up-front.
[AI-assisted - Claude]
Follow-up from PR #278 skeptical review (Important #2).
Problem
already_member = target_room_data.is_some_and(|rd| rd.can_participate().is_ok()).can_participate()returnsErr(UserBanned)for banned users, soalready_member = false— the Accept button shows the normal "Accept invitation" label. Clicking it triespresent_invitationwhich proceeds to the contract layer, where the join deterministically fails.Suggested fix
Distinguish ban from not-a-member in the card's button state:
Render
Bannedas a disabled button reading "You're banned from this room" with a destructive-tinted color.Severity
UX nit, not a correctness bug. The contract correctly rejects the join attempt; this is just about giving the user accurate information up-front.
[AI-assisted - Claude]