feat: two-step registration with avatar/color picker, fix focus ring clipping
Refactor RegisterPage into a two-step flow: credentials first, then personalization (display name, avatar upload, avatar color). Replace the dual-panel sliding layout with conditional rendering and CSS keyframe animations to eliminate overflow-hidden clipping of focus rings. Supporting changes: - Server accepts avatarColor on registration - Auth store resets all user-scoped stores on login/register/logout - Voice store gains resetSession() for full session cleanup - Sync presence status to federated instances - Propagate presence_update to socialStore regardless of origin
This commit is contained in:
@@ -14,7 +14,7 @@ interface AuthState {
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
login: (username: string, password: string) => Promise<void>;
|
||||
register: (username: string, password: string, displayName?: string) => Promise<void>;
|
||||
register: (username: string, password: string, displayName?: string, avatarColor?: string) => Promise<void>;
|
||||
logout: () => void;
|
||||
loadUser: () => Promise<void>;
|
||||
updateProfile: (data: { displayName?: string; avatar?: string; banner?: string; accentColor?: string; avatarColor?: string; bio?: string; customStatus?: string; status?: UserStatus }) => Promise<void>;
|
||||
@@ -22,6 +22,15 @@ interface AuthState {
|
||||
clearError: () => void;
|
||||
}
|
||||
|
||||
/** Reset all user-scoped stores to prevent data leaking between sessions */
|
||||
function resetUserStores() {
|
||||
useChatStore.getState().clearAllMessages();
|
||||
useSpaceStore.getState().populateFromReady('', [], [], []);
|
||||
useSocialStore.getState().reset();
|
||||
useVoiceStore.getState().resetSession();
|
||||
useInstanceStore.getState().reset();
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
token: localStorage.getItem('backspace_token'),
|
||||
user: null,
|
||||
@@ -32,6 +41,7 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const response = await api.auth.login({ username, password });
|
||||
resetUserStores();
|
||||
localStorage.setItem('backspace_token', response.token);
|
||||
set({ token: response.token, user: response.user, isLoading: false });
|
||||
// Auto-connect to remote instances (fire-and-forget)
|
||||
@@ -42,12 +52,15 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
}
|
||||
},
|
||||
|
||||
register: async (username: string, password: string, displayName?: string) => {
|
||||
register: async (username: string, password: string, displayName?: string, avatarColor?: string) => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const response = await api.auth.register({ username, password, displayName });
|
||||
const response = await api.auth.register({ username, password, displayName, avatarColor });
|
||||
resetUserStores();
|
||||
localStorage.setItem('backspace_token', response.token);
|
||||
set({ token: response.token, user: response.user, isLoading: false });
|
||||
// Auto-connect to remote instances (fire-and-forget)
|
||||
useInstanceStore.getState().autoConnectAll().catch(() => {});
|
||||
} catch (err) {
|
||||
set({ isLoading: false, error: err instanceof Error ? err.message : 'Registration failed' });
|
||||
throw err;
|
||||
@@ -56,12 +69,7 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
|
||||
logout: () => {
|
||||
localStorage.removeItem('backspace_token');
|
||||
// Clear all user-scoped state to prevent data leaking between sessions
|
||||
useChatStore.getState().clearAllMessages();
|
||||
useSpaceStore.getState().populateFromReady('', [], [], []);
|
||||
useSocialStore.getState().reset();
|
||||
useVoiceStore.getState().clearAllVoiceUsers();
|
||||
useInstanceStore.getState().reset();
|
||||
resetUserStores();
|
||||
set({ token: null, user: null });
|
||||
},
|
||||
|
||||
|
||||
@@ -104,6 +104,7 @@ interface VoiceState {
|
||||
setPermissionMutedUser: (spaceId: string, userId: string, muted: boolean) => void;
|
||||
getVoiceUsers: (channelId: string) => string[];
|
||||
clearAllVoiceUsers: () => void;
|
||||
resetSession: () => void;
|
||||
clearVoiceUsersForOrigin: (origin: string) => void;
|
||||
leaveVoice: () => void;
|
||||
handleForceDisconnect: () => void;
|
||||
@@ -365,6 +366,38 @@ export const useVoiceStore = create<VoiceState>()(
|
||||
|
||||
clearAllVoiceUsers: () => set({ voiceUsers: new Map(), voiceUserStates: new Map() }),
|
||||
|
||||
resetSession: () => set({
|
||||
// Connection state
|
||||
voiceUsers: new Map(),
|
||||
voiceUserStates: new Map(),
|
||||
currentVoiceChannelId: null,
|
||||
participants: [],
|
||||
speakingParticipantIds: new Set(),
|
||||
connectionError: null,
|
||||
isLiveKitConnected: false,
|
||||
connectionQuality: 'unknown',
|
||||
focusedParticipantId: null,
|
||||
// Call state
|
||||
incomingCall: null,
|
||||
outgoingCall: null,
|
||||
activeDmCall: null,
|
||||
// Per-session media state
|
||||
isCameraOn: false,
|
||||
isScreenSharing: false,
|
||||
// Per-session maps
|
||||
participantVolumes: new Map(),
|
||||
participantMutes: new Map(),
|
||||
deafenedUserIds: new Set(),
|
||||
streamVolumes: new Map(),
|
||||
streamMutes: new Map(),
|
||||
watchingStreams: new Set(),
|
||||
unwatchedCameras: new Set(),
|
||||
// Server-enforced restrictions
|
||||
serverMutedUserIds: new Set(),
|
||||
serverDeafenedUserIds: new Set(),
|
||||
permissionMutedUserIds: new Set(),
|
||||
}),
|
||||
|
||||
clearVoiceUsersForOrigin: (origin: string) => {
|
||||
const { channelOriginMap } = useSpaceStore.getState();
|
||||
set((state) => {
|
||||
|
||||
Reference in New Issue
Block a user