Skip to content

Commit a809c16

Browse files
committed
Lots of UX and UI fixes and tweaks including fixing more bugs with user permissions
1 parent f3aebd7 commit a809c16

5 files changed

Lines changed: 332 additions & 4 deletions

File tree

backend/routes/rooms.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,16 @@ def share_room(roomId):
547547
})
548548
except Exception:
549549
pass
550+
# Send dedicated invite event for real-time dashboard updates
551+
try:
552+
push_to_user(uid, 'invite_received', {
553+
'roomId': str(room['_id']),
554+
'roomName': room.get('name'),
555+
'inviterName': claims['username'],
556+
'role': user_role
557+
})
558+
except Exception:
559+
pass
550560
results["invited"].append({"username": uname, "role": role})
551561
else:
552562
shares_coll.update_one(
@@ -577,6 +587,25 @@ def share_room(roomId):
577587
})
578588
except Exception:
579589
pass
590+
# Send real-time event for room access granted
591+
try:
592+
push_to_user(uid, 'room_access_granted', {
593+
'roomId': str(room["_id"]),
594+
'roomName': room.get('name'),
595+
'role': user_role
596+
})
597+
except Exception:
598+
pass
599+
# Notify all room members that someone was added
600+
try:
601+
push_to_room(str(room["_id"]), 'member_added', {
602+
'roomId': str(room["_id"]),
603+
'userId': uid,
604+
'username': user["username"],
605+
'role': user_role
606+
})
607+
except Exception:
608+
pass
580609
results["updated"].append({"username": uname, "role": user_role, "note": "added to public room"})
581610
return jsonify({"status":"ok","results": results})
582611

@@ -2069,6 +2098,23 @@ def update_permissions(roomId):
20692098
})
20702099
except Exception:
20712100
pass
2101+
# Send real-time event to user so they get kicked immediately
2102+
try:
2103+
push_to_user(target_user_id, 'user_removed_from_room', {
2104+
'roomId': roomId,
2105+
'roomName': room.get('name'),
2106+
'message': f"You were removed from room '{room.get('name')}'"
2107+
})
2108+
except Exception:
2109+
pass
2110+
# Notify all room members that someone was removed
2111+
try:
2112+
push_to_room(roomId, 'member_removed', {
2113+
'roomId': roomId,
2114+
'userId': target_user_id
2115+
})
2116+
except Exception:
2117+
pass
20722118
return jsonify({"status":"ok","removed": target_user_id})
20732119
role = (data.get("role") or "").lower()
20742120
if role not in ("admin","editor","viewer"):
@@ -2101,6 +2147,25 @@ def update_permissions(roomId):
21012147
})
21022148
except Exception:
21032149
pass
2150+
# Send real-time event to user whose role changed so UI updates immediately
2151+
try:
2152+
push_to_user(target_user_id, 'role_changed', {
2153+
'roomId': roomId,
2154+
'roomName': room.get('name'),
2155+
'newRole': role,
2156+
'message': f"Your role in room '{room.get('name')}' was changed to '{role}'"
2157+
})
2158+
except Exception:
2159+
pass
2160+
# Notify all room members that someone's role changed
2161+
try:
2162+
push_to_room(roomId, 'member_role_changed', {
2163+
'roomId': roomId,
2164+
'userId': target_user_id,
2165+
'newRole': role
2166+
})
2167+
except Exception:
2168+
pass
21042169
return jsonify({"status":"ok","userId": target_user_id, "role": role})
21052170

21062171
@rooms_bp.route("/rooms/<roomId>", methods=["PATCH"])
@@ -2225,6 +2290,15 @@ def update_room(roomId):
22252290
"createdAt": room_refreshed.get("createdAt"),
22262291
"updatedAt": room_refreshed.get("updatedAt")
22272292
}
2293+
# Notify all room members that room settings were updated
2294+
try:
2295+
push_to_room(roomId, 'room_updated', {
2296+
'roomId': roomId,
2297+
'updates': updates,
2298+
'room': resp_room
2299+
})
2300+
except Exception:
2301+
pass
22282302
return jsonify({"status": "ok", "room": resp_room})
22292303
except Exception:
22302304
return jsonify({"status":"ok","updated": updates})
@@ -2286,6 +2360,33 @@ def transfer_ownership(roomId):
22862360
"read": False,
22872361
"createdAt": datetime.utcnow()
22882362
})
2363+
# Send real-time events for ownership transfer
2364+
try:
2365+
push_to_user(str(target_user["_id"]), 'role_changed', {
2366+
'roomId': roomId,
2367+
'roomName': room.get('name'),
2368+
'newRole': 'owner',
2369+
'message': f"You are now the owner of room '{room.get('name')}'"
2370+
})
2371+
except Exception:
2372+
pass
2373+
try:
2374+
push_to_user(claims["sub"], 'role_changed', {
2375+
'roomId': roomId,
2376+
'roomName': room.get('name'),
2377+
'newRole': 'editor',
2378+
'message': f"You transferred ownership of room '{room.get('name')}' to {target_user['username']}"
2379+
})
2380+
except Exception:
2381+
pass
2382+
try:
2383+
# Notify all room members about ownership change
2384+
push_to_room(roomId, 'room_updated', {
2385+
'roomId': roomId,
2386+
'updates': {'ownerId': str(target_user["_id"]), 'ownerName': target_user["username"]}
2387+
})
2388+
except Exception:
2389+
pass
22892390
return jsonify({"status":"ok"})
22902391

22912392
@rooms_bp.route("/rooms/<roomId>/leave", methods=["POST"])
@@ -2330,6 +2431,15 @@ def leave_room(roomId):
23302431
"read": False,
23312432
"createdAt": datetime.utcnow()
23322433
})
2434+
# Send real-time event to notify room members that someone left
2435+
try:
2436+
push_to_room(roomId, 'member_removed', {
2437+
'roomId': roomId,
2438+
'userId': user_id,
2439+
'username': claims.get('username')
2440+
})
2441+
except Exception:
2442+
pass
23332443
return jsonify({"status":"ok", "removed": True})
23342444

23352445
@rooms_bp.route("/rooms/<roomId>", methods=["DELETE"])
@@ -2543,6 +2653,35 @@ def accept_invite(inviteId):
25432653
})
25442654
except Exception:
25452655
pass
2656+
# Send real-time events when invite is accepted
2657+
try:
2658+
# Notify the user who accepted (for dashboard room list update)
2659+
push_to_user(inv["invitedUserId"], 'room_access_granted', {
2660+
'roomId': inv["roomId"],
2661+
'roomName': inv.get('roomName'),
2662+
'role': inv["role"]
2663+
})
2664+
except Exception:
2665+
pass
2666+
try:
2667+
# Also send invite_accepted event so their pending invites list updates
2668+
push_to_user(inv["invitedUserId"], 'invite_accepted', {
2669+
'inviteId': inviteId,
2670+
'roomId': inv["roomId"],
2671+
'roomName': inv.get('roomName')
2672+
})
2673+
except Exception:
2674+
pass
2675+
try:
2676+
# Notify all room members that someone was added
2677+
push_to_room(inv["roomId"], 'member_added', {
2678+
'roomId': inv["roomId"],
2679+
'userId': inv["invitedUserId"],
2680+
'username': inv["invitedUsername"],
2681+
'role': inv["role"]
2682+
})
2683+
except Exception:
2684+
pass
25462685
return jsonify({"status":"ok"})
25472686

25482687
@rooms_bp.route("/invites/<inviteId>/decline", methods=["POST"])
@@ -2584,6 +2723,15 @@ def decline_invite(inviteId):
25842723
})
25852724
except Exception:
25862725
pass
2726+
# Send real-time event to user who declined so their invites list updates
2727+
try:
2728+
push_to_user(inv["invitedUserId"], 'invite_declined', {
2729+
'inviteId': inviteId,
2730+
'roomId': inv["roomId"],
2731+
'roomName': inv.get('roomName')
2732+
})
2733+
except Exception:
2734+
pass
25872735
return jsonify({"status":"ok"})
25882736

25892737
@rooms_bp.route("/notifications", methods=["GET"])

frontend/src/components/NotificationsMenu.jsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { handleAuthError } from '../utils/authUtils';
88
export default function NotificationsMenu({ auth }) {
99
const [anchor, setAnchor] = useState(null);
1010
const [items, setItems] = useState([]);
11-
const unread = items.filter(i => !i.read).length;
11+
const [unreadCount, setUnreadCount] = useState(0);
1212
const [highlightedIds, setHighlightedIds] = useState(new Set());
1313

1414
async function refresh() {
@@ -19,6 +19,9 @@ export default function NotificationsMenu({ auth }) {
1919
// highlight unread items
2020
const unreadIds = new Set((res || []).filter(r => !r.read).map(r => r.id));
2121
setHighlightedIds(unreadIds);
22+
// Update unread count immediately
23+
const count = (res || []).filter(r => !r.read).length;
24+
setUnreadCount(count);
2225
} catch (err) {
2326
console.error('Failed to load notifications:', err);
2427
handleAuthError(err);
@@ -33,6 +36,8 @@ export default function NotificationsMenu({ auth }) {
3336
// new notifications are highlighted (unread)
3437
setItems(prev => [{ ...n, id }, ...prev]);
3538
setHighlightedIds(prev => new Set(Array.from(prev).concat([id])));
39+
// Increment unread count for new notification
40+
setUnreadCount(prev => prev + 1);
3641
});
3742
setSocketToken(auth.token);
3843
getSocket(auth.token);
@@ -75,6 +80,8 @@ export default function NotificationsMenu({ auth }) {
7580
if (!n.read) {
7681
await markNotificationRead(auth.token, n.id);
7782
setItems(prev => prev.map(it => it.id === n.id ? { ...it, read: true } : it));
83+
// Decrement unread count when marking as read
84+
setUnreadCount(prev => Math.max(0, prev - 1));
7885
}
7986
} catch (e) { console.error('mark read failed', e); }
8087
setHighlightedIds(prev => { const s = new Set(Array.from(prev)); s.delete(n.id); return s; });
@@ -85,6 +92,10 @@ export default function NotificationsMenu({ auth }) {
8592
await deleteNotification(auth.token, n.id);
8693
setItems(prev => prev.filter(it => it.id !== n.id));
8794
setHighlightedIds(prev => { const s = new Set(Array.from(prev)); s.delete(n.id); return s; });
95+
// Decrement unread count if the deleted notification was unread
96+
if (!n.read) {
97+
setUnreadCount(prev => Math.max(0, prev - 1));
98+
}
8899
} catch (e) { console.error('delete failed', e); }
89100
}
90101

@@ -93,6 +104,7 @@ export default function NotificationsMenu({ auth }) {
93104
await clearNotifications(auth.token);
94105
setItems([]);
95106
setHighlightedIds(new Set());
107+
setUnreadCount(0);
96108
} catch (e) { console.error('clear all failed', e); }
97109
}
98110

@@ -157,7 +169,7 @@ export default function NotificationsMenu({ auth }) {
157169
return (
158170
<>
159171
<IconButton color="inherit" onClick={handleOpen} sx={{ '&:hover': { boxShadow: '0 2px 8px rgba(37,216,197,0.12)', transform: 'translateY(-1px)' }, transition: 'all 120ms ease' }}>
160-
<Badge badgeContent={unread} color="error">
172+
<Badge badgeContent={unreadCount} color="error">
161173
<NotificationsIcon />
162174
</Badge>
163175
</IconButton>

frontend/src/pages/Dashboard.jsx

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { useNavigate, Link } from 'react-router-dom';
1616
import RouterLinkWrapper from '../components/RouterLinkWrapper';
1717
import { handleAuthError } from '../utils/authUtils';
1818
import { formatErrorMessage, clientValidation } from '../utils/errorHandling';
19+
import { getSocket, setSocketToken } from '../services/socket';
1920

2021
export default function Dashboard({ auth }) {
2122
const nav = useNavigate();
@@ -132,6 +133,73 @@ export default function Dashboard({ auth }) {
132133
return () => window.removeEventListener('rescanvas:rooms-updated', onRoomsUpdated);
133134
}, []);
134135

136+
// Socket.IO listener for real-time dashboard updates
137+
useEffect(() => {
138+
if (!auth?.token) return;
139+
140+
try {
141+
setSocketToken(auth.token);
142+
const sock = getSocket(auth.token);
143+
144+
const onRoomAccessGranted = (payload) => {
145+
console.log('Room access granted:', payload);
146+
setSnack({ open: true, message: `You were added to room: ${payload.roomName || 'a room'}` });
147+
refresh();
148+
};
149+
150+
const onUserRemovedFromRoom = (payload) => {
151+
console.log('User removed from room:', payload);
152+
setSnack({ open: true, message: payload.message || 'You were removed from a room' });
153+
refresh();
154+
};
155+
156+
const onRoomDeleted = (payload) => {
157+
console.log('Room deleted:', payload);
158+
setSnack({ open: true, message: 'A room was deleted' });
159+
refresh();
160+
};
161+
162+
const onInviteReceived = (payload) => {
163+
console.log('Invite received:', payload);
164+
setSnack({ open: true, message: `You were invited to room: ${payload.roomName || 'a room'}` });
165+
// Refresh invites list immediately
166+
listInvites(auth.token).then(inv => setInvites(inv)).catch(err => console.error('Failed to refresh invites:', err));
167+
};
168+
169+
const onInviteAccepted = (payload) => {
170+
console.log('Invite accepted:', payload);
171+
// Remove from pending invites list immediately
172+
setInvites(prev => prev.filter(inv => inv.id !== payload.inviteId));
173+
};
174+
175+
const onInviteDeclined = (payload) => {
176+
console.log('Invite declined:', payload);
177+
// Remove from pending invites list immediately
178+
setInvites(prev => prev.filter(inv => inv.id !== payload.inviteId));
179+
};
180+
181+
sock.on('room_access_granted', onRoomAccessGranted);
182+
sock.on('user_removed_from_room', onUserRemovedFromRoom);
183+
sock.on('room_deleted', onRoomDeleted);
184+
sock.on('invite_received', onInviteReceived);
185+
sock.on('invite_accepted', onInviteAccepted);
186+
sock.on('invite_declined', onInviteDeclined);
187+
188+
return () => {
189+
try {
190+
sock.off('room_access_granted', onRoomAccessGranted);
191+
sock.off('user_removed_from_room', onUserRemovedFromRoom);
192+
sock.off('room_deleted', onRoomDeleted);
193+
sock.off('invite_received', onInviteReceived);
194+
sock.off('invite_accepted', onInviteAccepted);
195+
sock.off('invite_declined', onInviteDeclined);
196+
} catch (e) { }
197+
};
198+
} catch (e) {
199+
console.error('Failed to setup socket listeners:', e);
200+
}
201+
}, [auth?.token]);
202+
135203
// Persist sort preferences
136204
useEffect(() => {
137205
try { localStorage.setItem('rescanvas:publicSortKey', publicSortKey); } catch (e) { }

0 commit comments

Comments
 (0)