Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
090d4dac2b | ||
|
|
f865ce16ec | ||
|
|
50f45ab08e | ||
|
|
a2d53680f2 |
14
CHANGELOG.md
14
CHANGELOG.md
@@ -1,3 +1,17 @@
|
||||
## [3.8.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v3.8.0...v3.8.1) (2025-10-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* improve kicked modal message for retired room ejections ([f865ce1](https://github.com/antialias/soroban-abacus-flashcards/commit/f865ce16ecf7648e41549795c8137f4fc33e34ac))
|
||||
|
||||
## [3.8.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v3.7.1...v3.8.0) (2025-10-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* implement proper retired room behavior with member expulsion ([a2d5368](https://github.com/antialias/soroban-abacus-flashcards/commit/a2d53680f27db04b2cd09973e62a76c5a7d4ce06))
|
||||
|
||||
## [3.7.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v3.7.0...v3.7.1) (2025-10-14)
|
||||
|
||||
|
||||
|
||||
@@ -38,9 +38,10 @@ export async function POST(req: NextRequest, context: RouteContext) {
|
||||
return NextResponse.json({ error: 'You are banned from this room' }, { status: 403 })
|
||||
}
|
||||
|
||||
// Check if user is already a member (for locked room access)
|
||||
// Check if user is already a member (for locked/retired room access)
|
||||
const members = await getRoomMembers(roomId)
|
||||
const isExistingMember = members.some((m) => m.userId === viewerId)
|
||||
const isRoomCreator = room.createdBy === viewerId
|
||||
|
||||
// Validate access mode
|
||||
switch (room.accessMode) {
|
||||
@@ -55,7 +56,14 @@ export async function POST(req: NextRequest, context: RouteContext) {
|
||||
break
|
||||
|
||||
case 'retired':
|
||||
return NextResponse.json({ error: 'This room has been retired' }, { status: 410 })
|
||||
// Only the room creator can access retired rooms
|
||||
if (!isRoomCreator) {
|
||||
return NextResponse.json(
|
||||
{ error: 'This room has been retired and is only accessible to the owner' },
|
||||
{ status: 410 }
|
||||
)
|
||||
}
|
||||
break
|
||||
|
||||
case 'password': {
|
||||
if (!body.password) {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { eq } from 'drizzle-orm'
|
||||
import bcrypt from 'bcryptjs'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { db, schema } from '@/db'
|
||||
import { getRoomActivePlayers } from '@/lib/arcade/player-manager'
|
||||
import { recordRoomMemberHistory } from '@/lib/arcade/room-member-history'
|
||||
import { getRoomMembers } from '@/lib/arcade/room-membership'
|
||||
import { getSocketIO } from '@/lib/socket-io'
|
||||
import { getViewerId } from '@/lib/viewer'
|
||||
import bcrypt from 'bcryptjs'
|
||||
|
||||
type RouteContext = {
|
||||
params: Promise<{ roomId: string }>
|
||||
@@ -79,6 +82,72 @@ export async function PATCH(req: NextRequest, context: RouteContext) {
|
||||
.where(eq(schema.arcadeRooms.id, roomId))
|
||||
.returning()
|
||||
|
||||
// If setting to retired, expel all non-owner members
|
||||
if (body.accessMode === 'retired') {
|
||||
const nonOwnerMembers = members.filter((m) => !m.isCreator)
|
||||
|
||||
if (nonOwnerMembers.length > 0) {
|
||||
// Remove all non-owner members from the room
|
||||
await db.delete(schema.roomMembers).where(
|
||||
and(
|
||||
eq(schema.roomMembers.roomId, roomId),
|
||||
// Delete all members except the creator
|
||||
eq(schema.roomMembers.isCreator, false)
|
||||
)
|
||||
)
|
||||
|
||||
// Record in history for each expelled member
|
||||
for (const member of nonOwnerMembers) {
|
||||
await recordRoomMemberHistory({
|
||||
roomId,
|
||||
userId: member.userId,
|
||||
displayName: member.displayName,
|
||||
action: 'left',
|
||||
})
|
||||
}
|
||||
|
||||
// Broadcast updates via socket
|
||||
const io = await getSocketIO()
|
||||
if (io) {
|
||||
try {
|
||||
// Get updated member list (should only be the owner now)
|
||||
const updatedMembers = await getRoomMembers(roomId)
|
||||
const memberPlayers = await getRoomActivePlayers(roomId)
|
||||
|
||||
// Convert memberPlayers Map to object for JSON serialization
|
||||
const memberPlayersObj: Record<string, any[]> = {}
|
||||
for (const [uid, players] of memberPlayers.entries()) {
|
||||
memberPlayersObj[uid] = players
|
||||
}
|
||||
|
||||
// Notify each expelled member
|
||||
for (const member of nonOwnerMembers) {
|
||||
io.to(`user:${member.userId}`).emit('kicked-from-room', {
|
||||
roomId,
|
||||
kickedBy: currentMember.displayName,
|
||||
reason: 'Room has been retired',
|
||||
})
|
||||
}
|
||||
|
||||
// Notify the owner that members were expelled
|
||||
io.to(`room:${roomId}`).emit('member-left', {
|
||||
roomId,
|
||||
userId: nonOwnerMembers.map((m) => m.userId),
|
||||
members: updatedMembers,
|
||||
memberPlayers: memberPlayersObj,
|
||||
reason: 'room-retired',
|
||||
})
|
||||
|
||||
console.log(
|
||||
`[Settings API] Expelled ${nonOwnerMembers.length} members from retired room ${roomId}`
|
||||
)
|
||||
} catch (socketError) {
|
||||
console.error('[Settings API] Failed to broadcast member expulsion:', socketError)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ room: updatedRoom }, { status: 200 })
|
||||
} catch (error: any) {
|
||||
console.error('Failed to update room settings:', error)
|
||||
|
||||
@@ -129,6 +129,8 @@ export function ModerationNotifications({
|
||||
|
||||
// Kicked modal
|
||||
if (moderationEvent?.type === 'kicked') {
|
||||
const isRetired = moderationEvent.data.reason?.includes('retired')
|
||||
|
||||
return (
|
||||
<Modal isOpen={true} onClose={() => {}}>
|
||||
<div
|
||||
@@ -139,7 +141,7 @@ export function ModerationNotifications({
|
||||
minWidth: '400px',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: '48px', marginBottom: '16px' }}>⚠️</div>
|
||||
<div style={{ fontSize: '48px', marginBottom: '16px' }}>{isRetired ? '🏁' : '⚠️'}</div>
|
||||
<h2
|
||||
style={{
|
||||
fontSize: '24px',
|
||||
@@ -148,7 +150,7 @@ export function ModerationNotifications({
|
||||
color: 'rgba(253, 186, 116, 1)',
|
||||
}}
|
||||
>
|
||||
Kicked from Room
|
||||
{isRetired ? 'Room Retired' : 'Kicked from Room'}
|
||||
</h2>
|
||||
<p
|
||||
style={{
|
||||
@@ -157,10 +159,16 @@ export function ModerationNotifications({
|
||||
marginBottom: '8px',
|
||||
}}
|
||||
>
|
||||
You were kicked from the room by{' '}
|
||||
<strong style={{ color: 'rgba(253, 186, 116, 1)' }}>
|
||||
{moderationEvent.data.kickedBy}
|
||||
</strong>
|
||||
{isRetired ? (
|
||||
<>The room owner has retired this room and access has been closed</>
|
||||
) : (
|
||||
<>
|
||||
You were kicked from the room by{' '}
|
||||
<strong style={{ color: 'rgba(253, 186, 116, 1)' }}>
|
||||
{moderationEvent.data.kickedBy}
|
||||
</strong>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
<p
|
||||
style={{
|
||||
@@ -169,7 +177,9 @@ export function ModerationNotifications({
|
||||
marginBottom: '24px',
|
||||
}}
|
||||
>
|
||||
You can rejoin if the host sends you a new invite
|
||||
{isRetired
|
||||
? 'Only the room owner can access retired rooms'
|
||||
: 'You can rejoin if the host sends you a new invite'}
|
||||
</p>
|
||||
|
||||
<button
|
||||
|
||||
@@ -347,13 +347,14 @@ export function useRoomData() {
|
||||
}
|
||||
|
||||
// Moderation event handlers
|
||||
const handleKickedFromRoom = (data: { roomId: string; kickedBy: string }) => {
|
||||
const handleKickedFromRoom = (data: { roomId: string; kickedBy: string; reason?: string }) => {
|
||||
console.log('[useRoomData] User was kicked from room:', data)
|
||||
setModerationEvent({
|
||||
type: 'kicked',
|
||||
data: {
|
||||
roomId: data.roomId,
|
||||
kickedBy: data.kickedBy,
|
||||
reason: data.reason,
|
||||
},
|
||||
})
|
||||
// Clear room data since user was kicked
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "soroban-monorepo",
|
||||
"version": "3.7.1",
|
||||
"version": "3.8.1",
|
||||
"private": true,
|
||||
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
|
||||
"workspaces": [
|
||||
|
||||
Reference in New Issue
Block a user