mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Refactor state
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import { BrowserRouter, Routes, Route, useNavigate, useLocation, matchRoutes, Navigate, type RouteObject } from "react-router-dom";
|
import { BrowserRouter, Routes, Route, useNavigate, useLocation, matchRoutes, Navigate, type RouteObject } from "react-router-dom";
|
||||||
import { AnimatePresence, motion } from "motion/react";
|
import { AnimatePresence, motion } from "motion/react";
|
||||||
import { ElectronTitleBar } from "./Electron";
|
import { ElectronTitleBar } from "./Electron";
|
||||||
import { useAppState } from "./pages/chat/state";
|
import { useUserStore } from "./state/user";
|
||||||
import { lazy, useEffect, useRef, useState } from "react";
|
import { lazy, useEffect, useRef, useState } from "react";
|
||||||
import { parseProfileLink } from "./core/profileLinks";
|
import { parseProfileLink } from "./core/profileLinks";
|
||||||
import NotFoundPage from "./pages/not-found/NotFoundPage";
|
import NotFoundPage from "./pages/not-found/NotFoundPage";
|
||||||
@@ -117,14 +117,14 @@ function AnimatedRoutes() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const { restoreUserFromStorage, user } = useAppState();
|
const { restoreFromStorage, user } = useUserStore();
|
||||||
const [authReady, setAuthReady] = useState(false);
|
const [authReady, setAuthReady] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
restoreUserFromStorage().finally(() => {
|
restoreFromStorage().finally(() => {
|
||||||
setAuthReady(true);
|
setAuthReady(true);
|
||||||
});
|
});
|
||||||
}, [restoreUserFromStorage]);
|
}, [restoreFromStorage]);
|
||||||
|
|
||||||
return authReady && (
|
return authReady && (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { checkUserSimilarity } from "@/core/api/account/profile";
|
import { checkUserSimilarity } from "@/core/api/account/profile";
|
||||||
import { useAppState } from "@/pages/chat/state";
|
import { useUserStore } from "@/state/user";
|
||||||
import { MaterialIcon } from "@/utils/material";
|
import { MaterialIcon } from "@/utils/material";
|
||||||
|
|
||||||
interface StatusBadgeProps {
|
interface StatusBadgeProps {
|
||||||
@@ -11,7 +11,7 @@ interface StatusBadgeProps {
|
|||||||
|
|
||||||
export function StatusBadge({ verified, userId, size = "small" }: StatusBadgeProps) {
|
export function StatusBadge({ verified, userId, size = "small" }: StatusBadgeProps) {
|
||||||
const [isSimilarToVerified, setIsSimilarToVerified] = useState(false);
|
const [isSimilarToVerified, setIsSimilarToVerified] = useState(false);
|
||||||
const { user } = useAppState();
|
const { user } = useUserStore();
|
||||||
|
|
||||||
const className = `status-badge ${size}`;
|
const className = `status-badge ${size}`;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { verifyUser } from "@/core/api/account/profile";
|
import { verifyUser } from "@/core/api/account/profile";
|
||||||
import { useAppState } from "@/pages/chat/state";
|
import { useUserStore } from "@/state/user";
|
||||||
import { MaterialButton } from "@/utils/material";
|
import { MaterialButton } from "@/utils/material";
|
||||||
|
|
||||||
interface VerifyButtonProps {
|
interface VerifyButtonProps {
|
||||||
@@ -11,7 +11,7 @@ interface VerifyButtonProps {
|
|||||||
|
|
||||||
export function VerifyButton({ userId, verified, onVerificationChange }: VerifyButtonProps) {
|
export function VerifyButton({ userId, verified, onVerificationChange }: VerifyButtonProps) {
|
||||||
const [isVerifying, setIsVerifying] = useState(false);
|
const [isVerifying, setIsVerifying] = useState(false);
|
||||||
const { user } = useAppState();
|
const { user } = useUserStore();
|
||||||
|
|
||||||
// Only show for owner
|
// Only show for owner
|
||||||
if (user.currentUser?.id !== 1) {
|
if (user.currentUser?.id !== 1) {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import type {
|
|||||||
SubscribeStatusWebSocketMessage,
|
SubscribeStatusWebSocketMessage,
|
||||||
UnsubscribeStatusWebSocketMessage
|
UnsubscribeStatusWebSocketMessage
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { useAppState } from "@/pages/chat/state";
|
import { usePresenceStore } from "@/state/presence";
|
||||||
|
|
||||||
export interface UserStatus {
|
export interface UserStatus {
|
||||||
online: boolean;
|
online: boolean;
|
||||||
@@ -96,7 +96,7 @@ export class OnlineStatusManager {
|
|||||||
this.statusCache.set(userId, { online, lastSeen });
|
this.statusCache.set(userId, { online, lastSeen });
|
||||||
|
|
||||||
// Update the global state
|
// Update the global state
|
||||||
const { updateOnlineStatus } = useAppState.getState();
|
const { updateOnlineStatus } = usePresenceStore.getState();
|
||||||
updateOnlineStatus(userId, online, lastSeen);
|
updateOnlineStatus(userId, online, lastSeen);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import type {
|
|||||||
DmTypingRequest,
|
DmTypingRequest,
|
||||||
StopDmTypingRequest
|
StopDmTypingRequest
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { useAppState } from "@/pages/chat/state";
|
import { usePresenceStore } from "@/state/presence";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages typing indicators for public chat and DMs
|
* Manages typing indicators for public chat and DMs
|
||||||
@@ -133,7 +133,7 @@ export class TypingManager {
|
|||||||
* Handle incoming typing indicator from WebSocket
|
* Handle incoming typing indicator from WebSocket
|
||||||
*/
|
*/
|
||||||
handleTyping(message: TypingWebSocketMessage): void {
|
handleTyping(message: TypingWebSocketMessage): void {
|
||||||
const { addTypingUser } = useAppState.getState();
|
const { addTypingUser } = usePresenceStore.getState();
|
||||||
addTypingUser(message.data.userId, message.data.username);
|
addTypingUser(message.data.userId, message.data.username);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,7 +141,7 @@ export class TypingManager {
|
|||||||
* Handle incoming stop typing indicator from WebSocket
|
* Handle incoming stop typing indicator from WebSocket
|
||||||
*/
|
*/
|
||||||
handleStopTyping(message: StopTypingWebSocketMessage): void {
|
handleStopTyping(message: StopTypingWebSocketMessage): void {
|
||||||
const { removeTypingUser } = useAppState.getState();
|
const { removeTypingUser } = usePresenceStore.getState();
|
||||||
removeTypingUser(message.data.userId);
|
removeTypingUser(message.data.userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,7 +149,7 @@ export class TypingManager {
|
|||||||
* Handle incoming DM typing indicator from WebSocket
|
* Handle incoming DM typing indicator from WebSocket
|
||||||
*/
|
*/
|
||||||
handleDmTyping(message: DmTypingWebSocketMessage): void {
|
handleDmTyping(message: DmTypingWebSocketMessage): void {
|
||||||
const { setDmTypingUser } = useAppState.getState();
|
const { setDmTypingUser } = usePresenceStore.getState();
|
||||||
setDmTypingUser(message.data.userId, true);
|
setDmTypingUser(message.data.userId, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,7 +157,7 @@ export class TypingManager {
|
|||||||
* Handle incoming stop DM typing indicator from WebSocket
|
* Handle incoming stop DM typing indicator from WebSocket
|
||||||
*/
|
*/
|
||||||
handleStopDmTyping(message: StopDmTypingWebSocketMessage): void {
|
handleStopDmTyping(message: StopDmTypingWebSocketMessage): void {
|
||||||
const { setDmTypingUser } = useAppState.getState();
|
const { setDmTypingUser } = usePresenceStore.getState();
|
||||||
setDmTypingUser(message.data.userId, false);
|
setDmTypingUser(message.data.userId, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { delay } from "@/utils/utils";
|
|||||||
import { CallSignalingHandler } from "./calls/signaling";
|
import { CallSignalingHandler } from "./calls/signaling";
|
||||||
import { onlineStatusManager } from "./onlineStatusManager";
|
import { onlineStatusManager } from "./onlineStatusManager";
|
||||||
import { typingManager } from "./typingManager";
|
import { typingManager } from "./typingManager";
|
||||||
import { useAppState } from "@/pages/chat/state";
|
import { useUserStore } from "@/state/user";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new WebSocket connection to the chat server
|
* Creates a new WebSocket connection to the chat server
|
||||||
@@ -170,14 +170,14 @@ function setupEventHandlers(): void {
|
|||||||
typingManager.handleStopDmTyping(response as any);
|
typingManager.handleStopDmTyping(response as any);
|
||||||
} else if (response.type === "suspended") {
|
} else if (response.type === "suspended") {
|
||||||
// Handle account suspension
|
// Handle account suspension
|
||||||
const { setSuspended } = useAppState.getState();
|
const { setSuspended } = useUserStore.getState();
|
||||||
const reason = response.data?.reason || "No reason provided";
|
const reason = response.data?.reason || "No reason provided";
|
||||||
setSuspended(reason);
|
setSuspended(reason);
|
||||||
// Close WebSocket connection
|
// Close WebSocket connection
|
||||||
websocket.close();
|
websocket.close();
|
||||||
} else if (response.type === "account_deleted") {
|
} else if (response.type === "account_deleted") {
|
||||||
// Handle account deletion - silent logout
|
// Handle account deletion - silent logout
|
||||||
const { logout } = useAppState.getState();
|
const { logout } = useUserStore.getState();
|
||||||
logout();
|
logout();
|
||||||
// Close WebSocket connection
|
// Close WebSocket connection
|
||||||
websocket.close();
|
websocket.close();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { useAppState } from "./chat/state";
|
import { useUserStore } from "@/state/user";
|
||||||
import { Navigate } from "react-router-dom";
|
import { Navigate } from "react-router-dom";
|
||||||
|
|
||||||
interface ProtectedRouteProps {
|
interface ProtectedRouteProps {
|
||||||
@@ -7,7 +7,7 @@ interface ProtectedRouteProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function ProtectedRoute({ children }: ProtectedRouteProps) {
|
export default function ProtectedRoute({ children }: ProtectedRouteProps) {
|
||||||
const { user } = useAppState();
|
const { user } = useUserStore();
|
||||||
|
|
||||||
return !user.authToken ? <Navigate to="/login" /> : children;
|
return !user.authToken ? <Navigate to="/login" /> : children;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { motion, type Transition, type Variants } from "motion/react";
|
import { motion, type Transition, type Variants } from "motion/react";
|
||||||
import { useImmer } from "use-immer";
|
import { useImmer } from "use-immer";
|
||||||
import type { LoginRequest } from "@/core/types";
|
import type { LoginRequest } from "@/core/types";
|
||||||
import { useAppState } from "@/pages/chat/state";
|
import { useUserStore } from "@/state/user";
|
||||||
import { MaterialButton } from "@/utils/material";
|
import { MaterialButton } from "@/utils/material";
|
||||||
import { ensureKeysOnLogin, deriveAuthSecret, login } from "@/core/api/account";
|
import { ensureKeysOnLogin, deriveAuthSecret, login } from "@/core/api/account";
|
||||||
import { AuthTextField, type AuthTextFieldHandle } from "./AuthTextField";
|
import { AuthTextField, type AuthTextFieldHandle } from "./AuthTextField";
|
||||||
@@ -53,7 +53,7 @@ interface LoginFormProps {
|
|||||||
export function LoginForm({ onSwitchMode }: LoginFormProps) {
|
export function LoginForm({ onSwitchMode }: LoginFormProps) {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [alerts, updateAlerts] = useImmer<Alert[]>([]);
|
const [alerts, updateAlerts] = useImmer<Alert[]>([]);
|
||||||
const setUser = useAppState(state => state.setUser);
|
const setUser = useUserStore(state => state.setUser);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
function showAlert(type: AlertType, message: string) {
|
function showAlert(type: AlertType, message: string) {
|
||||||
@@ -119,7 +119,7 @@ export function LoginForm({ onSwitchMode }: LoginFormProps) {
|
|||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error.message && error.message.includes("suspension")) {
|
if (error.message && error.message.includes("suspension")) {
|
||||||
const setSuspended = useAppState.getState().setSuspended;
|
const setSuspended = useUserStore.getState().setSuspended;
|
||||||
setSuspended(error.message || "No reason provided");
|
setSuspended(error.message || "No reason provided");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { motion, type Transition, type Variants } from "motion/react";
|
import { motion, type Transition, type Variants } from "motion/react";
|
||||||
import { useImmer } from "use-immer";
|
import { useImmer } from "use-immer";
|
||||||
import type { RegisterRequest } from "@/core/types";
|
import type { RegisterRequest } from "@/core/types";
|
||||||
import { useAppState } from "@/pages/chat/state";
|
import { useUserStore } from "@/state/user";
|
||||||
import { MaterialButton, MaterialIconButton } from "@/utils/material";
|
import { MaterialButton, MaterialIconButton } from "@/utils/material";
|
||||||
import { ensureKeysOnLogin, deriveAuthSecret, register } from "@/core/api/account";
|
import { ensureKeysOnLogin, deriveAuthSecret, register } from "@/core/api/account";
|
||||||
import { AuthTextField, type AuthTextFieldHandle } from "./AuthTextField";
|
import { AuthTextField, type AuthTextFieldHandle } from "./AuthTextField";
|
||||||
@@ -51,7 +51,7 @@ interface RegisterFormProps {
|
|||||||
export function RegisterForm({ onSwitchMode }: RegisterFormProps) {
|
export function RegisterForm({ onSwitchMode }: RegisterFormProps) {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [alerts, updateAlerts] = useImmer<Alert[]>([]);
|
const [alerts, updateAlerts] = useImmer<Alert[]>([]);
|
||||||
const setUser = useAppState(state => state.setUser);
|
const setUser = useUserStore(state => state.setUser);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
function showAlert(type: AlertType, message: string) {
|
function showAlert(type: AlertType, message: string) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useAppState } from "@/pages/chat/state";
|
import { useCallStore } from "@/state/call";
|
||||||
|
import { useUserStore } from "@/state/user";
|
||||||
import * as WebRTC from "@/core/calls/webrtc";
|
import * as WebRTC from "@/core/calls/webrtc";
|
||||||
import { CallSignalingHandler } from "@/core/calls/signaling";
|
import { CallSignalingHandler } from "@/core/calls/signaling";
|
||||||
import { setCallSignalingHandler } from "@/core/websocket";
|
import { setCallSignalingHandler } from "@/core/websocket";
|
||||||
@@ -15,7 +16,7 @@ let globalRemoteScreenShareRef = createRef<HTMLVideoElement>();
|
|||||||
|
|
||||||
export default function useCall() {
|
export default function useCall() {
|
||||||
const {
|
const {
|
||||||
chat,
|
call,
|
||||||
startCall,
|
startCall,
|
||||||
endCall,
|
endCall,
|
||||||
setCallStatus,
|
setCallStatus,
|
||||||
@@ -26,8 +27,9 @@ export default function useCall() {
|
|||||||
setCallSessionKeyHash,
|
setCallSessionKeyHash,
|
||||||
setRemoteVideoEnabled,
|
setRemoteVideoEnabled,
|
||||||
setRemoteScreenSharing,
|
setRemoteScreenSharing,
|
||||||
user
|
receiveCall
|
||||||
} = useAppState();
|
} = useCallStore();
|
||||||
|
const { user } = useUserStore();
|
||||||
|
|
||||||
const remoteAudioRef = globalRemoteAudioRef;
|
const remoteAudioRef = globalRemoteAudioRef;
|
||||||
const localVideoRef = globalLocalVideoRef;
|
const localVideoRef = globalLocalVideoRef;
|
||||||
@@ -40,8 +42,7 @@ export default function useCall() {
|
|||||||
const signalingHandler = new CallSignalingHandler(() => ({
|
const signalingHandler = new CallSignalingHandler(() => ({
|
||||||
receiveCall: (userId: number, username: string) => {
|
receiveCall: (userId: number, username: string) => {
|
||||||
// Use the receiveCall function from state
|
// Use the receiveCall function from state
|
||||||
const state = useAppState.getState();
|
receiveCall(userId, username);
|
||||||
state.receiveCall(userId, username);
|
|
||||||
},
|
},
|
||||||
endCall,
|
endCall,
|
||||||
setCallSessionKeyHash,
|
setCallSessionKeyHash,
|
||||||
@@ -52,8 +53,8 @@ export default function useCall() {
|
|||||||
|
|
||||||
// Set up call state change handler
|
// Set up call state change handler
|
||||||
WebRTC.callbacks.onCallStateChange = (userId: number, state: string) => {
|
WebRTC.callbacks.onCallStateChange = (userId: number, state: string) => {
|
||||||
const call = chat.call;
|
const currentCall = call;
|
||||||
if (call.remoteUserId === userId) {
|
if (currentCall.remoteUserId === userId) {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case "connecting":
|
case "connecting":
|
||||||
setCallStatus("connecting");
|
setCallStatus("connecting");
|
||||||
@@ -183,15 +184,15 @@ export default function useCall() {
|
|||||||
WebRTC.cleanup();
|
WebRTC.cleanup();
|
||||||
setCallSignalingHandler(null);
|
setCallSignalingHandler(null);
|
||||||
};
|
};
|
||||||
}, [user.authToken, chat.call.remoteUserId, setCallStatus, endCall, startCall, setRemoteVideoEnabled, setRemoteScreenSharing]);
|
}, [user.authToken, call.remoteUserId, setCallStatus, endCall, startCall, setRemoteVideoEnabled, setRemoteScreenSharing]);
|
||||||
|
|
||||||
// Watch for session key hash changes and generate emojis
|
// Watch for session key hash changes and generate emojis
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (chat.call.sessionKeyHash && chat.call.encryptionEmojis.length === 0) {
|
if (call.sessionKeyHash && call.encryptionEmojis.length === 0) {
|
||||||
const emojis = generateCallEmojis(chat.call.sessionKeyHash);
|
const emojis = generateCallEmojis(call.sessionKeyHash);
|
||||||
setCallEncryption(chat.call.sessionKeyHash, emojis);
|
setCallEncryption(call.sessionKeyHash, emojis);
|
||||||
}
|
}
|
||||||
}, [chat.call.sessionKeyHash, chat.call.encryptionEmojis.length, setCallEncryption]);
|
}, [call.sessionKeyHash, call.encryptionEmojis.length, setCallEncryption]);
|
||||||
|
|
||||||
async function requestAudioPermissions(): Promise<boolean> {
|
async function requestAudioPermissions(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
@@ -249,12 +250,12 @@ export default function useCall() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function acceptCall() {
|
async function acceptCall() {
|
||||||
if (!chat.call.remoteUserId) {
|
if (!call.remoteUserId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setCallStatus("connecting");
|
setCallStatus("connecting");
|
||||||
const success = await WebRTC.acceptCall(chat.call.remoteUserId);
|
const success = await WebRTC.acceptCall(call.remoteUserId);
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
endCall();
|
endCall();
|
||||||
@@ -262,46 +263,46 @@ export default function useCall() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function rejectCall() {
|
async function rejectCall() {
|
||||||
if (!chat.call.remoteUserId) {
|
if (!call.remoteUserId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await WebRTC.rejectCall(chat.call.remoteUserId);
|
await WebRTC.rejectCall(call.remoteUserId);
|
||||||
endCall();
|
endCall();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleEndCall() {
|
async function handleEndCall() {
|
||||||
if (chat.call.remoteUserId) {
|
if (call.remoteUserId) {
|
||||||
await WebRTC.endCall(chat.call.remoteUserId);
|
await WebRTC.endCall(call.remoteUserId);
|
||||||
}
|
}
|
||||||
endCall();
|
endCall();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleToggleMute() {
|
function handleToggleMute() {
|
||||||
if (chat.call.remoteUserId) {
|
if (call.remoteUserId) {
|
||||||
const isMuted = WebRTC.toggleMute(chat.call.remoteUserId);
|
const isMuted = WebRTC.toggleMute(call.remoteUserId);
|
||||||
// Update mute state in store
|
// Update mute state in store
|
||||||
if (isMuted !== chat.call.isMuted) {
|
if (isMuted !== call.isMuted) {
|
||||||
toggleMute();
|
toggleMute();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleToggleVideo() {
|
async function handleToggleVideo() {
|
||||||
if (chat.call.remoteUserId) {
|
if (call.remoteUserId) {
|
||||||
const isEnabled = await WebRTC.toggleVideo(chat.call.remoteUserId);
|
const isEnabled = await WebRTC.toggleVideo(call.remoteUserId);
|
||||||
// Update video state in store
|
// Update video state in store
|
||||||
if (isEnabled !== chat.call.isVideoEnabled) {
|
if (isEnabled !== call.isVideoEnabled) {
|
||||||
toggleVideo();
|
toggleVideo();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleToggleScreenShare() {
|
async function handleToggleScreenShare() {
|
||||||
if (chat.call.remoteUserId) {
|
if (call.remoteUserId) {
|
||||||
const isEnabled = await WebRTC.toggleScreenShare(chat.call.remoteUserId);
|
const isEnabled = await WebRTC.toggleScreenShare(call.remoteUserId);
|
||||||
// Update screen share state in store
|
// Update screen share state in store
|
||||||
if (isEnabled !== chat.call.isSharingScreen) {
|
if (isEnabled !== call.isSharingScreen) {
|
||||||
toggleScreenShare();
|
toggleScreenShare();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -336,7 +337,7 @@ export default function useCall() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
call: chat.call,
|
call: call,
|
||||||
initiateCall,
|
initiateCall,
|
||||||
acceptCall,
|
acceptCall,
|
||||||
rejectCall,
|
rejectCall,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from "react";
|
import { useState, useEffect, useCallback, useRef } from "react";
|
||||||
import { useAppState } from "@/pages/chat/state";
|
import { useUserStore } from "@/state/user";
|
||||||
|
import { useChatStore } from "@/state/chat";
|
||||||
import {
|
import {
|
||||||
fetchUserPublicKey,
|
fetchUserPublicKey,
|
||||||
fetchDMHistory,
|
fetchDMHistory,
|
||||||
@@ -44,7 +45,8 @@ export function formatDMMessageContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useDM() {
|
export function useDM() {
|
||||||
const { user, setDmUsers, setActiveDm, addMessage, clearMessages } = useAppState();
|
const { user } = useUserStore();
|
||||||
|
const { setDmUsers, setActiveDm, addMessage, clearMessages } = useChatStore();
|
||||||
const [dmUsers, setDmUsersState] = useState<DMUser[]>([]);
|
const [dmUsers, setDmUsersState] = useState<DMUser[]>([]);
|
||||||
const [isLoadingUsers, setIsLoadingUsers] = useState(false);
|
const [isLoadingUsers, setIsLoadingUsers] = useState(false);
|
||||||
const [isLoadingHistory, setIsLoadingHistory] = useState(false);
|
const [isLoadingHistory, setIsLoadingHistory] = useState(false);
|
||||||
@@ -295,7 +297,7 @@ export function useDM() {
|
|||||||
// If conversation no longer exists, remove the user from the list
|
// If conversation no longer exists, remove the user from the list
|
||||||
setDmUsersState(prev => prev.filter(u => u.id !== userId));
|
setDmUsersState(prev => prev.filter(u => u.id !== userId));
|
||||||
// Get current dmUsers and filter out the removed user
|
// Get current dmUsers and filter out the removed user
|
||||||
const currentDmUsers = useAppState.getState().chat.dmUsers;
|
const currentDmUsers = useChatStore.getState().dmUsers;
|
||||||
setDmUsers(currentDmUsers.filter((u: User) => u.id !== userId));
|
setDmUsers(currentDmUsers.filter((u: User) => u.id !== userId));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { useState, useCallback, useEffect } from "react";
|
import { useState, useCallback, useEffect } from "react";
|
||||||
import { useAppState } from "@/pages/chat/state";
|
import { useUserStore } from "@/state/user";
|
||||||
import { loadProfile, updateProfile, uploadProfilePicture, type ProfileData } from "@/core/api/account/profile";
|
import { loadProfile, updateProfile, uploadProfilePicture, type ProfileData } from "@/core/api/account/profile";
|
||||||
import { showSuccess, showError } from "@/utils/notification";
|
import { showSuccess, showError } from "@/utils/notification";
|
||||||
|
|
||||||
export default function useProfile() {
|
export default function useProfile() {
|
||||||
const { user } = useAppState();
|
const { user } = useUserStore();
|
||||||
const [profileData, setProfileData] = useState<ProfileData | null>(null);
|
const [profileData, setProfileData] = useState<ProfileData | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isUpdating, setIsUpdating] = useState(false);
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
|
|||||||
@@ -1,728 +0,0 @@
|
|||||||
import { create } from "zustand";
|
|
||||||
import type { Message, User } from "@/core/types";
|
|
||||||
import { request } from "@/core/websocket";
|
|
||||||
import { MessagePanel } from "./ui/right/panels/MessagePanel";
|
|
||||||
import { PublicChatPanel } from "./ui/right/panels/PublicChatPanel";
|
|
||||||
import { DMPanel, type DMPanelData } from "./ui/right/panels/DMPanel";
|
|
||||||
import { restoreKeys } from "@/core/api/account";
|
|
||||||
import { API_BASE_URL } from "@/core/config";
|
|
||||||
import { getAuthHeaders } from "@/core/api/account";
|
|
||||||
import { initialize, subscribe, startElectronReceiver, isSupported } from "@/core/push-notifications/push-notifications";
|
|
||||||
import { isElectron } from "@/core/electron/electron";
|
|
||||||
import { onlineStatusManager } from "@/core/onlineStatusManager";
|
|
||||||
import { typingManager } from "@/core/typingManager";
|
|
||||||
|
|
||||||
export type ChatTabs = "chats" | "channels" | "contacts";
|
|
||||||
|
|
||||||
export type CallStatus = "calling" | "connecting" | "active" | "ended";
|
|
||||||
|
|
||||||
export interface ProfileDialogData {
|
|
||||||
userId?: number;
|
|
||||||
username?: string;
|
|
||||||
display_name?: string;
|
|
||||||
profilePicture?: string;
|
|
||||||
bio?: string;
|
|
||||||
memberSince?: string;
|
|
||||||
online?: boolean;
|
|
||||||
isOwnProfile: boolean;
|
|
||||||
verified?: boolean;
|
|
||||||
suspended?: boolean;
|
|
||||||
suspension_reason?: string | null;
|
|
||||||
deleted?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ActiveDM {
|
|
||||||
userId: number;
|
|
||||||
username: string;
|
|
||||||
publicKey: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CallState {
|
|
||||||
isActive: boolean;
|
|
||||||
status: CallStatus;
|
|
||||||
startTime: number | null;
|
|
||||||
isMuted: boolean;
|
|
||||||
remoteUserId: number | null;
|
|
||||||
remoteUsername: string | null;
|
|
||||||
isInitiator: boolean;
|
|
||||||
isMinimized: boolean;
|
|
||||||
sessionKeyHash: string | null;
|
|
||||||
encryptionEmojis: string[];
|
|
||||||
isVideoEnabled: boolean;
|
|
||||||
isRemoteVideoEnabled: boolean;
|
|
||||||
isSharingScreen: boolean;
|
|
||||||
isRemoteScreenSharing: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ChatState {
|
|
||||||
messages: Message[];
|
|
||||||
currentChat: string;
|
|
||||||
activeTab: ChatTabs;
|
|
||||||
dmUsers: User[];
|
|
||||||
activeDm: ActiveDM | null;
|
|
||||||
isSwitching: boolean;
|
|
||||||
setIsSwitching: (value: boolean) => void;
|
|
||||||
activePanel: MessagePanel | null;
|
|
||||||
publicChatPanel: PublicChatPanel | null;
|
|
||||||
dmPanel: DMPanel | null;
|
|
||||||
pendingPanel?: MessagePanel | null;
|
|
||||||
call: CallState;
|
|
||||||
profileDialog: ProfileDialogData | null;
|
|
||||||
onlineStatuses: Map<number, {online: boolean, lastSeen: string}>;
|
|
||||||
typingUsers: Map<number, string>; // userId -> username
|
|
||||||
dmTypingUsers: Map<number, boolean>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UserState {
|
|
||||||
currentUser: User | null;
|
|
||||||
authToken: string | null;
|
|
||||||
isSuspended: boolean;
|
|
||||||
suspensionReason: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AppState {
|
|
||||||
// Chat state
|
|
||||||
chat: ChatState;
|
|
||||||
addMessage: (message: Message) => void;
|
|
||||||
updateMessage: (messageId: number, updatedMessage: Partial<Message>) => void;
|
|
||||||
removeMessage: (messageId: number) => void;
|
|
||||||
setCurrentChat: (chat: string) => void;
|
|
||||||
setActiveTab: (tab: ChatState["activeTab"]) => void;
|
|
||||||
setDmUsers: (users: User[]) => void;
|
|
||||||
setActiveDm: (dm: ChatState["activeDm"]) => void;
|
|
||||||
clearMessages: () => void;
|
|
||||||
setActivePanel: (panel: MessagePanel | null) => void;
|
|
||||||
setPendingPanel: (panel: MessagePanel | null) => void;
|
|
||||||
applyPendingPanel: () => void;
|
|
||||||
switchToPublicChat: (chatName: string) => Promise<void>;
|
|
||||||
switchToDM: (dmData: DMPanelData) => Promise<void>;
|
|
||||||
|
|
||||||
// Call state
|
|
||||||
startCall: (userId: number, username: string) => void;
|
|
||||||
endCall: () => void;
|
|
||||||
setCallStatus: (status: CallStatus) => void;
|
|
||||||
toggleMute: () => void;
|
|
||||||
toggleCallMinimize: () => void;
|
|
||||||
receiveCall: (userId: number, username: string) => void;
|
|
||||||
setCallEncryption: (sessionKeyHash: string, encryptionEmojis: string[]) => void;
|
|
||||||
setCallSessionKeyHash: (sessionKeyHash: string) => void;
|
|
||||||
toggleVideo: () => void;
|
|
||||||
toggleScreenShare: () => void;
|
|
||||||
setRemoteVideoEnabled: (enabled: boolean) => void;
|
|
||||||
setRemoteScreenSharing: (enabled: boolean) => void;
|
|
||||||
toggleCallMinimized: () => void;
|
|
||||||
|
|
||||||
// User state
|
|
||||||
user: UserState;
|
|
||||||
setUser: (token: string, user: User) => void;
|
|
||||||
logout: () => void;
|
|
||||||
restoreUserFromStorage: () => Promise<void>;
|
|
||||||
setSuspended: (reason: string) => void;
|
|
||||||
|
|
||||||
// Profile dialog state
|
|
||||||
setProfileDialog: (data: ProfileDialogData | null) => void;
|
|
||||||
closeProfileDialog: () => void;
|
|
||||||
|
|
||||||
// Online status and typing state
|
|
||||||
updateOnlineStatus: (userId: number, online: boolean, lastSeen: string) => void;
|
|
||||||
addTypingUser: (userId: number, username: string) => void;
|
|
||||||
removeTypingUser: (userId: number) => void;
|
|
||||||
setDmTypingUser: (userId: number, isTyping: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useAppState = create<AppState>((set, get) => ({
|
|
||||||
// Chat state
|
|
||||||
chat: {
|
|
||||||
messages: [],
|
|
||||||
currentChat: "Общий чат",
|
|
||||||
activeTab: "chats",
|
|
||||||
dmUsers: [],
|
|
||||||
activeDm: null,
|
|
||||||
isSwitching: false,
|
|
||||||
setIsSwitching: (value: boolean) => set((state) => ({
|
|
||||||
chat: {
|
|
||||||
...state.chat,
|
|
||||||
isSwitching: value
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
activePanel: null,
|
|
||||||
publicChatPanel: null,
|
|
||||||
dmPanel: null,
|
|
||||||
pendingPanel: null,
|
|
||||||
profileDialog: null,
|
|
||||||
call: {
|
|
||||||
isActive: false,
|
|
||||||
status: "ended",
|
|
||||||
startTime: null,
|
|
||||||
isMuted: false,
|
|
||||||
remoteUserId: null,
|
|
||||||
remoteUsername: null,
|
|
||||||
isInitiator: false,
|
|
||||||
isMinimized: false,
|
|
||||||
sessionKeyHash: null,
|
|
||||||
encryptionEmojis: [],
|
|
||||||
isVideoEnabled: false,
|
|
||||||
isRemoteVideoEnabled: false,
|
|
||||||
isSharingScreen: false,
|
|
||||||
isRemoteScreenSharing: false
|
|
||||||
},
|
|
||||||
onlineStatuses: new Map(),
|
|
||||||
typingUsers: new Map(),
|
|
||||||
dmTypingUsers: new Map()
|
|
||||||
},
|
|
||||||
addMessage: (message: Message) => set((state) => {
|
|
||||||
// Check if message already exists to prevent duplicates
|
|
||||||
const messageExists = state.chat.messages.some(msg => msg.id === message.id);
|
|
||||||
if (messageExists) {
|
|
||||||
return state; // Return unchanged state if message already exists
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
chat: {
|
|
||||||
...state.chat,
|
|
||||||
messages: [...state.chat.messages, message]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
updateMessage: (messageId: number, updatedMessage: Partial<Message>) => set((state) => ({
|
|
||||||
chat: {
|
|
||||||
...state.chat,
|
|
||||||
messages: state.chat.messages.map(msg =>
|
|
||||||
msg.id === messageId ? { ...msg, ...updatedMessage } : msg
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
removeMessage: (messageId: number) => set((state) => ({
|
|
||||||
chat: {
|
|
||||||
...state.chat,
|
|
||||||
messages: state.chat.messages.filter(msg => msg.id !== messageId)
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
clearMessages: () => set((state) => ({
|
|
||||||
chat: {
|
|
||||||
...state.chat,
|
|
||||||
messages: []
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
setCurrentChat: (chat: string) => set((state) => ({
|
|
||||||
chat: {
|
|
||||||
...state.chat,
|
|
||||||
currentChat: chat
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
setActiveTab: (tab: ChatState["activeTab"]) => set((state) => ({
|
|
||||||
chat: {
|
|
||||||
...state.chat,
|
|
||||||
activeTab: tab
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
setDmUsers: (users: User[]) => set((state) => ({
|
|
||||||
chat: {
|
|
||||||
...state.chat,
|
|
||||||
dmUsers: users
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
setActiveDm: (dm: ChatState["activeDm"]) => set((state) => ({
|
|
||||||
chat: {
|
|
||||||
...state.chat,
|
|
||||||
activeDm: dm
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
|
|
||||||
// User state
|
|
||||||
user: {
|
|
||||||
currentUser: null,
|
|
||||||
authToken: null,
|
|
||||||
isSuspended: false,
|
|
||||||
suspensionReason: null
|
|
||||||
},
|
|
||||||
setUser: (token: string, user: User) => {
|
|
||||||
set(() => ({
|
|
||||||
user: {
|
|
||||||
currentUser: user,
|
|
||||||
authToken: token,
|
|
||||||
isSuspended: user.suspended || false,
|
|
||||||
suspensionReason: user.suspension_reason || null
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Initialize managers with auth token
|
|
||||||
onlineStatusManager.setAuthToken(token);
|
|
||||||
typingManager.setAuthToken(token);
|
|
||||||
|
|
||||||
// Store credentials in localStorage
|
|
||||||
try {
|
|
||||||
localStorage.setItem('authToken', token);
|
|
||||||
localStorage.setItem('currentUser', JSON.stringify(user));
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to store credentials in localStorage:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
request({
|
|
||||||
type: "ping",
|
|
||||||
credentials: {
|
|
||||||
scheme: "Bearer",
|
|
||||||
credentials: token
|
|
||||||
},
|
|
||||||
data: {}
|
|
||||||
})
|
|
||||||
} catch {}
|
|
||||||
},
|
|
||||||
logout: () => {
|
|
||||||
// Clear localStorage
|
|
||||||
try {
|
|
||||||
localStorage.removeItem('authToken');
|
|
||||||
localStorage.removeItem('currentUser');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to clear localStorage:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup managers
|
|
||||||
onlineStatusManager.setAuthToken(null);
|
|
||||||
typingManager.setAuthToken(null);
|
|
||||||
onlineStatusManager.cleanup();
|
|
||||||
typingManager.cleanup();
|
|
||||||
|
|
||||||
set(() => ({
|
|
||||||
user: {
|
|
||||||
currentUser: null,
|
|
||||||
authToken: null,
|
|
||||||
isSuspended: false,
|
|
||||||
suspensionReason: null
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
restoreUserFromStorage: async () => {
|
|
||||||
try {
|
|
||||||
const token = localStorage.getItem('authToken');
|
|
||||||
|
|
||||||
if (token) {
|
|
||||||
// Fetch full user profile
|
|
||||||
const fullResponse = await fetch(`${API_BASE_URL}/user/profile`, {
|
|
||||||
headers: getAuthHeaders(token, true)
|
|
||||||
});
|
|
||||||
if (fullResponse.ok) {
|
|
||||||
const user: User = await fullResponse.json();
|
|
||||||
restoreKeys();
|
|
||||||
|
|
||||||
// Check if user is suspended
|
|
||||||
if (user.suspended) {
|
|
||||||
set(() => ({
|
|
||||||
user: {
|
|
||||||
currentUser: user,
|
|
||||||
authToken: token,
|
|
||||||
isSuspended: true,
|
|
||||||
suspensionReason: user.suspension_reason || null
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
return; // Don't initialize managers or notifications for suspended users
|
|
||||||
}
|
|
||||||
|
|
||||||
set(() => ({
|
|
||||||
user: {
|
|
||||||
currentUser: user,
|
|
||||||
authToken: token,
|
|
||||||
isSuspended: false,
|
|
||||||
suspensionReason: null
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Initialize managers with auth token
|
|
||||||
onlineStatusManager.setAuthToken(token);
|
|
||||||
typingManager.setAuthToken(token);
|
|
||||||
|
|
||||||
try {
|
|
||||||
request({
|
|
||||||
type: "ping",
|
|
||||||
credentials: {
|
|
||||||
scheme: "Bearer",
|
|
||||||
credentials: token
|
|
||||||
},
|
|
||||||
data: {}
|
|
||||||
})
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
// Initialize notifications after successful credential restoration
|
|
||||||
try {
|
|
||||||
if (isSupported()) {
|
|
||||||
const initialized = await initialize();
|
|
||||||
if (initialized) {
|
|
||||||
await subscribe(token);
|
|
||||||
|
|
||||||
// For Electron, start the notification receiver
|
|
||||||
if (isElectron) {
|
|
||||||
await startElectronReceiver();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Notification setup failed (restored):", e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error("Unable to authenticate");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to restore user from localStorage:', error);
|
|
||||||
// Clear invalid data
|
|
||||||
localStorage.removeItem('authToken');
|
|
||||||
localStorage.removeItem('currentUser');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Panel management
|
|
||||||
setActivePanel: (panel: MessagePanel | null) => {
|
|
||||||
const state = get();
|
|
||||||
// Deactivate the current panel before switching
|
|
||||||
if (state.chat.activePanel && state.chat.activePanel !== panel) {
|
|
||||||
state.chat.activePanel.deactivate();
|
|
||||||
}
|
|
||||||
return set((state) => ({
|
|
||||||
chat: {
|
|
||||||
...state.chat,
|
|
||||||
activePanel: panel
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
// Stash a panel to be applied after switch-out animation ends
|
|
||||||
setPendingPanel: (panel: MessagePanel | null) => set((state) => ({
|
|
||||||
chat: {
|
|
||||||
...state.chat,
|
|
||||||
pendingPanel: panel
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
// Apply pending panel atomically and update related fields
|
|
||||||
applyPendingPanel: () => {
|
|
||||||
const state = get();
|
|
||||||
// Deactivate the current panel before switching
|
|
||||||
if (state.chat.activePanel) {
|
|
||||||
state.chat.activePanel.deactivate();
|
|
||||||
}
|
|
||||||
return set((state) => ({
|
|
||||||
chat: {
|
|
||||||
...state.chat,
|
|
||||||
activePanel: state.chat.pendingPanel || state.chat.activePanel,
|
|
||||||
// when switching to public chat, keep reference if type matches
|
|
||||||
publicChatPanel: (state.chat.pendingPanel instanceof PublicChatPanel)
|
|
||||||
? (state.chat.pendingPanel as PublicChatPanel)
|
|
||||||
: state.chat.publicChatPanel,
|
|
||||||
dmPanel: (state.chat.pendingPanel instanceof DMPanel)
|
|
||||||
? (state.chat.pendingPanel as DMPanel)
|
|
||||||
: state.chat.dmPanel,
|
|
||||||
// update currentChat from panel title if available
|
|
||||||
currentChat: state.chat.pendingPanel ? state.chat.pendingPanel.getState().title || state.chat.currentChat : state.chat.currentChat,
|
|
||||||
pendingPanel: null
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
switchToPublicChat: async (chatName: string) => {
|
|
||||||
const { user, chat } = get();
|
|
||||||
|
|
||||||
if (!user.authToken) return;
|
|
||||||
|
|
||||||
// Start chat switching animation
|
|
||||||
chat.setIsSwitching(true);
|
|
||||||
|
|
||||||
// Create or get public chat panel
|
|
||||||
let publicChatPanel = chat.publicChatPanel;
|
|
||||||
if (!publicChatPanel) {
|
|
||||||
publicChatPanel = new PublicChatPanel(chatName, user);
|
|
||||||
} else {
|
|
||||||
publicChatPanel.setChatName(chatName);
|
|
||||||
publicChatPanel.setAuthToken(user.authToken);
|
|
||||||
// Reset messages for the new chat
|
|
||||||
publicChatPanel.clearMessages();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Activate panel
|
|
||||||
await publicChatPanel.activate();
|
|
||||||
|
|
||||||
// Defer panel swap until animation switch-out completes
|
|
||||||
set((state) => ({
|
|
||||||
chat: {
|
|
||||||
...state.chat,
|
|
||||||
pendingPanel: publicChatPanel,
|
|
||||||
activeTab: "chats"
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Let MessagePanelRenderer handle the animation timing completely
|
|
||||||
// It will set isChatSwitching to false when the fadeInDown animation completes
|
|
||||||
},
|
|
||||||
|
|
||||||
switchToDM: async (dmData: DMPanelData) => {
|
|
||||||
const { user, chat } = get();
|
|
||||||
|
|
||||||
if (!user.authToken) return;
|
|
||||||
|
|
||||||
// Start chat switching animation
|
|
||||||
chat.setIsSwitching(true);
|
|
||||||
|
|
||||||
// Create or get DM panel
|
|
||||||
let dmPanel = chat.dmPanel;
|
|
||||||
if (!dmPanel) {
|
|
||||||
dmPanel = new DMPanel(user);
|
|
||||||
} else {
|
|
||||||
dmPanel.setAuthToken(user.authToken);
|
|
||||||
// Reset messages for the new DM
|
|
||||||
dmPanel.clearMessages();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set DM data
|
|
||||||
dmPanel.setDMData(dmData);
|
|
||||||
|
|
||||||
// Activate panel
|
|
||||||
await dmPanel.activate();
|
|
||||||
|
|
||||||
// Defer panel swap until animation switch-out completes
|
|
||||||
set((state) => ({
|
|
||||||
chat: {
|
|
||||||
...state.chat,
|
|
||||||
pendingPanel: dmPanel,
|
|
||||||
activeDm: {
|
|
||||||
userId: dmData.userId,
|
|
||||||
username: dmData.username,
|
|
||||||
publicKey: dmData.publicKey
|
|
||||||
},
|
|
||||||
activeTab: "chats"
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Let MessagePanelRenderer handle the animation timing completely
|
|
||||||
// It will set isChatSwitching to false when the fadeInDown animation completes
|
|
||||||
},
|
|
||||||
|
|
||||||
// Call state management
|
|
||||||
startCall: (userId: number, username: string) => set((state) => ({
|
|
||||||
chat: {
|
|
||||||
...state.chat,
|
|
||||||
call: {
|
|
||||||
isActive: true,
|
|
||||||
status: "calling",
|
|
||||||
startTime: null,
|
|
||||||
isMuted: false,
|
|
||||||
remoteUserId: userId,
|
|
||||||
remoteUsername: username,
|
|
||||||
isInitiator: true,
|
|
||||||
isMinimized: false,
|
|
||||||
sessionKeyHash: null,
|
|
||||||
encryptionEmojis: [],
|
|
||||||
isVideoEnabled: false,
|
|
||||||
isRemoteVideoEnabled: false,
|
|
||||||
isSharingScreen: false,
|
|
||||||
isRemoteScreenSharing: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
|
|
||||||
endCall: () => set((state) => ({
|
|
||||||
chat: {
|
|
||||||
...state.chat,
|
|
||||||
call: {
|
|
||||||
isActive: false,
|
|
||||||
status: "ended",
|
|
||||||
startTime: null,
|
|
||||||
isMuted: false,
|
|
||||||
remoteUserId: null,
|
|
||||||
remoteUsername: null,
|
|
||||||
isInitiator: false,
|
|
||||||
isMinimized: false,
|
|
||||||
sessionKeyHash: null,
|
|
||||||
encryptionEmojis: [],
|
|
||||||
isVideoEnabled: false,
|
|
||||||
isRemoteVideoEnabled: false,
|
|
||||||
isSharingScreen: false,
|
|
||||||
isRemoteScreenSharing: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
|
|
||||||
setCallStatus: (status: CallStatus) => set((state) => ({
|
|
||||||
chat: {
|
|
||||||
...state.chat,
|
|
||||||
call: {
|
|
||||||
...state.chat.call,
|
|
||||||
status,
|
|
||||||
startTime: status === "active" && !state.chat.call.startTime ? Date.now() : state.chat.call.startTime
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
|
|
||||||
toggleMute: () => set((state) => ({
|
|
||||||
chat: {
|
|
||||||
...state.chat,
|
|
||||||
call: {
|
|
||||||
...state.chat.call,
|
|
||||||
isMuted: !state.chat.call.isMuted
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
|
|
||||||
toggleCallMinimize: () => set((state) => ({
|
|
||||||
chat: {
|
|
||||||
...state.chat,
|
|
||||||
call: {
|
|
||||||
...state.chat.call,
|
|
||||||
isMinimized: !state.chat.call.isMinimized
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
|
|
||||||
receiveCall: (userId: number, username: string) => set((state) => ({
|
|
||||||
chat: {
|
|
||||||
...state.chat,
|
|
||||||
call: {
|
|
||||||
isActive: true,
|
|
||||||
status: "calling",
|
|
||||||
startTime: null,
|
|
||||||
isMuted: false,
|
|
||||||
remoteUserId: userId,
|
|
||||||
remoteUsername: username,
|
|
||||||
isInitiator: false,
|
|
||||||
isMinimized: false,
|
|
||||||
sessionKeyHash: null,
|
|
||||||
encryptionEmojis: [],
|
|
||||||
isVideoEnabled: false,
|
|
||||||
isRemoteVideoEnabled: false,
|
|
||||||
isSharingScreen: false,
|
|
||||||
isRemoteScreenSharing: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
|
|
||||||
setCallEncryption: (sessionKeyHash: string, encryptionEmojis: string[]) => set((state) => ({
|
|
||||||
chat: {
|
|
||||||
...state.chat,
|
|
||||||
call: {
|
|
||||||
...state.chat.call,
|
|
||||||
sessionKeyHash,
|
|
||||||
encryptionEmojis
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
|
|
||||||
setCallSessionKeyHash: (sessionKeyHash: string) => set((state) => ({
|
|
||||||
chat: {
|
|
||||||
...state.chat,
|
|
||||||
call: {
|
|
||||||
...state.chat.call,
|
|
||||||
sessionKeyHash
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
|
|
||||||
toggleVideo: () => set((state) => ({
|
|
||||||
chat: {
|
|
||||||
...state.chat,
|
|
||||||
call: {
|
|
||||||
...state.chat.call,
|
|
||||||
isVideoEnabled: !state.chat.call.isVideoEnabled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
|
|
||||||
toggleScreenShare: () => set((state) => ({
|
|
||||||
chat: {
|
|
||||||
...state.chat,
|
|
||||||
call: {
|
|
||||||
...state.chat.call,
|
|
||||||
isSharingScreen: !state.chat.call.isSharingScreen
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
|
|
||||||
setRemoteVideoEnabled: (enabled: boolean) => set((state) => ({
|
|
||||||
chat: {
|
|
||||||
...state.chat,
|
|
||||||
call: {
|
|
||||||
...state.chat.call,
|
|
||||||
isRemoteVideoEnabled: enabled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
|
|
||||||
setRemoteScreenSharing: (enabled: boolean) => set((state) => ({
|
|
||||||
chat: {
|
|
||||||
...state.chat,
|
|
||||||
call: {
|
|
||||||
...state.chat.call,
|
|
||||||
isRemoteScreenSharing: enabled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
toggleCallMinimized: () => set((state) => ({
|
|
||||||
chat: {
|
|
||||||
...state.chat,
|
|
||||||
call: {
|
|
||||||
...state.chat.call,
|
|
||||||
isMinimized: !state.chat.call.isMinimized
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
|
|
||||||
// Profile dialog state management
|
|
||||||
setProfileDialog: (data: ProfileDialogData | null) => set((state) => ({
|
|
||||||
chat: {
|
|
||||||
...state.chat,
|
|
||||||
profileDialog: data
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
|
|
||||||
closeProfileDialog: () => set((state) => ({
|
|
||||||
chat: {
|
|
||||||
...state.chat,
|
|
||||||
profileDialog: null
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
|
|
||||||
// Online status and typing state management
|
|
||||||
updateOnlineStatus: (userId: number, online: boolean, lastSeen: string) => set((state) => ({
|
|
||||||
chat: {
|
|
||||||
...state.chat,
|
|
||||||
onlineStatuses: new Map(state.chat.onlineStatuses).set(userId, { online, lastSeen })
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
|
|
||||||
addTypingUser: (userId: number, username: string) => set((state) => ({
|
|
||||||
chat: {
|
|
||||||
...state.chat,
|
|
||||||
typingUsers: new Map(state.chat.typingUsers).set(userId, username)
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
|
|
||||||
removeTypingUser: (userId: number) => set((state) => {
|
|
||||||
const newTypingUsers = new Map(state.chat.typingUsers);
|
|
||||||
newTypingUsers.delete(userId);
|
|
||||||
return {
|
|
||||||
chat: {
|
|
||||||
...state.chat,
|
|
||||||
typingUsers: newTypingUsers
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
|
|
||||||
setDmTypingUser: (userId: number, isTyping: boolean) => set((state) => {
|
|
||||||
const newDmTypingUsers = new Map(state.chat.dmTypingUsers);
|
|
||||||
if (isTyping) {
|
|
||||||
newDmTypingUsers.set(userId, true);
|
|
||||||
} else {
|
|
||||||
newDmTypingUsers.delete(userId);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
chat: {
|
|
||||||
...state.chat,
|
|
||||||
dmTypingUsers: newDmTypingUsers
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
|
|
||||||
setSuspended: (reason: string) => set((state) => ({
|
|
||||||
user: {
|
|
||||||
...state.user,
|
|
||||||
isSuspended: true,
|
|
||||||
suspensionReason: reason
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}));
|
|
||||||
@@ -4,7 +4,8 @@ import useDownloadAppScreen from "@/core/hooks/useDownloadAppScreen";
|
|||||||
import { CallWindow } from "./right/calls/CallWindow";
|
import { CallWindow } from "./right/calls/CallWindow";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import { useAppState } from "@/pages/chat/state";
|
import { useUserStore } from "@/state/user";
|
||||||
|
import { useProfileStore } from "@/state/profile";
|
||||||
import { fetchUserProfileById, fetchUserProfile } from "@/core/api/account/profile";
|
import { fetchUserProfileById, fetchUserProfile } from "@/core/api/account/profile";
|
||||||
import styles from "@/pages/chat/css/layout.module.scss";
|
import styles from "@/pages/chat/css/layout.module.scss";
|
||||||
|
|
||||||
@@ -12,7 +13,8 @@ export default function ChatPage() {
|
|||||||
const { navigate: navigateDownloadApp } = useDownloadAppScreen();
|
const { navigate: navigateDownloadApp } = useDownloadAppScreen();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { user, setProfileDialog } = useAppState();
|
const { user } = useUserStore();
|
||||||
|
const { setProfileDialog } = useProfileStore();
|
||||||
const processedProfile = useRef<string | null>(null);
|
const processedProfile = useRef<string | null>(null);
|
||||||
|
|
||||||
// Handle profile links ONLY from navigation state (from SmartCatchAll)
|
// Handle profile links ONLY from navigation state (from SmartCatchAll)
|
||||||
@@ -64,7 +66,7 @@ export default function ChatPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleProfileLink();
|
handleProfileLink();
|
||||||
}, [location.state, user.authToken, user.currentUser?.id, setProfileDialog, navigate, location.pathname]);
|
}, [location.state, user.authToken, user.currentUser?.id, navigate, location.pathname]);
|
||||||
|
|
||||||
if (navigateDownloadApp) return navigateDownloadApp;
|
if (navigateDownloadApp) return navigateDownloadApp;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect, useRef, useMemo, type ReactNode } from "react";
|
import { useState, useEffect, useRef, useMemo, type ReactNode } from "react";
|
||||||
import { useAppState } from "@/pages/chat/state";
|
import { useProfileStore } from "@/state/profile";
|
||||||
import type { ProfileDialogData } from "@/pages/chat/state";
|
import { useUserStore } from "@/state/user";
|
||||||
|
import type { ProfileDialogData } from "@/state/types";
|
||||||
import defaultAvatar from "@/images/default-avatar.png";
|
import defaultAvatar from "@/images/default-avatar.png";
|
||||||
import { confirm } from "mdui/functions/confirm";
|
import { confirm } from "mdui/functions/confirm";
|
||||||
import { prompt } from "mdui/functions/prompt";
|
import { prompt } from "mdui/functions/prompt";
|
||||||
@@ -70,7 +71,8 @@ function Section({ type, icon, label, error, value, onChange, readOnly, placehol
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ProfileDialog() {
|
export function ProfileDialog() {
|
||||||
const { chat, user, closeProfileDialog, setUser } = useAppState();
|
const { profileDialog, closeProfileDialog } = useProfileStore();
|
||||||
|
const { user, setUser } = useUserStore();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [originalData, setOriginalData] = useState<ProfileDialogData | null>(null);
|
const [originalData, setOriginalData] = useState<ProfileDialogData | null>(null);
|
||||||
const [currentData, setCurrentData] = useState<ProfileDialogData | null>(null);
|
const [currentData, setCurrentData] = useState<ProfileDialogData | null>(null);
|
||||||
@@ -80,13 +82,13 @@ export function ProfileDialog() {
|
|||||||
|
|
||||||
// Handle dialog open/close based on state
|
// Handle dialog open/close based on state
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (chat.profileDialog && !isOpen) {
|
if (profileDialog && !isOpen) {
|
||||||
// Fetch fresh data when opening dialog
|
// Fetch fresh data when opening dialog
|
||||||
fetchFreshProfileData(chat.profileDialog);
|
fetchFreshProfileData(profileDialog);
|
||||||
} else if (!chat.profileDialog && isOpen) {
|
} else if (!profileDialog && isOpen) {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}
|
}
|
||||||
}, [chat.profileDialog, isOpen]);
|
}, [profileDialog, isOpen]);
|
||||||
|
|
||||||
async function fetchFreshProfileData(profileData: ProfileDialogData) {
|
async function fetchFreshProfileData(profileData: ProfileDialogData) {
|
||||||
if (!user.authToken) return;
|
if (!user.authToken) return;
|
||||||
|
|||||||
@@ -2,14 +2,16 @@ import { PRODUCT_NAME } from "@/core/config";
|
|||||||
import useProfile from "@/pages/chat/hooks/useProfile";
|
import useProfile from "@/pages/chat/hooks/useProfile";
|
||||||
import defaultAvatar from "@/images/default-avatar.png";
|
import defaultAvatar from "@/images/default-avatar.png";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useAppState } from "@/pages/chat/state";
|
import { useUserStore } from "@/state/user";
|
||||||
|
import { useProfileStore } from "@/state/profile";
|
||||||
import { MinimizedCallBar } from "@/pages/chat/ui/right/calls/MinimizedCallBar";
|
import { MinimizedCallBar } from "@/pages/chat/ui/right/calls/MinimizedCallBar";
|
||||||
import styles from "@/pages/chat/css/left-panel.module.scss";
|
import styles from "@/pages/chat/css/left-panel.module.scss";
|
||||||
import logoIcon from "@/images/logo.svg";
|
import logoIcon from "@/images/logo.svg";
|
||||||
|
|
||||||
export function ChatHeader({ headerRef }: { headerRef?: React.RefObject<HTMLElement | null> }) {
|
export function ChatHeader({ headerRef }: { headerRef?: React.RefObject<HTMLElement | null> }) {
|
||||||
const { profileData } = useProfile();
|
const { profileData } = useProfile();
|
||||||
const { setProfileDialog, user } = useAppState();
|
const { user } = useUserStore();
|
||||||
|
const { setProfileDialog } = useProfileStore();
|
||||||
const [profilePictureUrl, setProfilePictureUrl] = useState(profileData?.profile_picture || defaultAvatar);
|
const [profilePictureUrl, setProfilePictureUrl] = useState(profileData?.profile_picture || defaultAvatar);
|
||||||
|
|
||||||
function handleProfileClick() {
|
function handleProfileClick() {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useAppState } from "@/pages/chat/state";
|
import { useUserStore } from "@/state/user";
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { SettingsDialog } from "./settings/SettingsDialog";
|
import { SettingsDialog } from "./settings/SettingsDialog";
|
||||||
import { UsernameSearch } from "./UsernameSearch";
|
import { UsernameSearch } from "./UsernameSearch";
|
||||||
@@ -9,7 +9,7 @@ import styles from "@/pages/chat/css/left-panel.module.scss";
|
|||||||
|
|
||||||
function BottomAppBar({ bottomAppBarRef }: { bottomAppBarRef?: React.RefObject<MDUIBottomAppBar | null> }) {
|
function BottomAppBar({ bottomAppBarRef }: { bottomAppBarRef?: React.RefObject<MDUIBottomAppBar | null> }) {
|
||||||
const [settingsOpen, onSettingsOpenChange] = useState(false);
|
const [settingsOpen, onSettingsOpenChange] = useState(false);
|
||||||
const { logout } = useAppState();
|
const { logout } = useUserStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect, useCallback, useMemo } from "react";
|
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||||
import { useAppState } from "@/pages/chat/state";
|
import { useUserStore } from "@/state/user";
|
||||||
|
import { useChatStore } from "@/state/chat";
|
||||||
import { useDM, type DMUser } from "@/pages/chat/hooks/useDM";
|
import { useDM, type DMUser } from "@/pages/chat/hooks/useDM";
|
||||||
import { fetchMessages } from "@/core/api/messaging";
|
import { fetchMessages } from "@/core/api/messaging";
|
||||||
import { fetchUserPublicKey } from "@/core/api/dm";
|
import { fetchUserPublicKey } from "@/core/api/dm";
|
||||||
@@ -42,7 +43,8 @@ const PUBLIC_CHAT: PublicChat = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function UnifiedChatsList() {
|
export function UnifiedChatsList() {
|
||||||
const { user, switchToPublicChat, switchToDM, chat } = useAppState();
|
const { user } = useUserStore();
|
||||||
|
const { switchToPublicChat, switchToDM, activeTab } = useChatStore();
|
||||||
const { dmUsers, isLoadingUsers, loadUsers } = useDM();
|
const { dmUsers, isLoadingUsers, loadUsers } = useDM();
|
||||||
const [lastMessages, setLastMessages] = useState<Record<string, Message | undefined>>({});
|
const [lastMessages, setLastMessages] = useState<Record<string, Message | undefined>>({});
|
||||||
|
|
||||||
@@ -61,11 +63,11 @@ export function UnifiedChatsList() {
|
|||||||
}, [user.authToken]);
|
}, [user.authToken]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (chat.activeTab === "chats") {
|
if (activeTab === "chats") {
|
||||||
loadUsers();
|
loadUsers();
|
||||||
loadLastMessages();
|
loadLastMessages();
|
||||||
}
|
}
|
||||||
}, [chat.activeTab, loadUsers, loadLastMessages]);
|
}, [activeTab, loadUsers, loadLastMessages]);
|
||||||
|
|
||||||
const allChats = useMemo<ChatItem[]>(() => {
|
const allChats = useMemo<ChatItem[]>(() => {
|
||||||
return [
|
return [
|
||||||
@@ -155,7 +157,7 @@ export function UnifiedChatsList() {
|
|||||||
|
|
||||||
async function handleDMClick(dmConversation: DMConversation) {
|
async function handleDMClick(dmConversation: DMConversation) {
|
||||||
if (!dmConversation.publicKey) {
|
if (!dmConversation.publicKey) {
|
||||||
const authToken = useAppState.getState().user.authToken;
|
const authToken = useUserStore.getState().user.authToken;
|
||||||
if (!authToken) return;
|
if (!authToken) return;
|
||||||
|
|
||||||
const publicKey = await fetchUserPublicKey(dmConversation.id, authToken);
|
const publicKey = await fetchUserPublicKey(dmConversation.id, authToken);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { useAppState } from "@/pages/chat/state";
|
import { useUserStore } from "@/state/user";
|
||||||
|
import { useChatStore } from "@/state/chat";
|
||||||
import { searchUsers, fetchUserPublicKey } from "@/core/api/dm";
|
import { searchUsers, fetchUserPublicKey } from "@/core/api/dm";
|
||||||
import { StatusBadge } from "@/core/components/StatusBadge";
|
import { StatusBadge } from "@/core/components/StatusBadge";
|
||||||
import type { User } from "@/core/types";
|
import type { User } from "@/core/types";
|
||||||
@@ -23,7 +24,8 @@ export interface UsernameSearchProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function UsernameSearch({ containerRef, headerRef, bottomAppBarRef }: UsernameSearchProps) {
|
export function UsernameSearch({ containerRef, headerRef, bottomAppBarRef }: UsernameSearchProps) {
|
||||||
const { user, switchToDM, chat } = useAppState();
|
const { user } = useUserStore();
|
||||||
|
const { switchToDM, activeDm } = useChatStore();
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [searchResults, setSearchResults] = useState<SearchUser[]>([]);
|
const [searchResults, setSearchResults] = useState<SearchUser[]>([]);
|
||||||
const [isSearching, setIsSearching] = useState(false);
|
const [isSearching, setIsSearching] = useState(false);
|
||||||
@@ -68,7 +70,7 @@ export function UsernameSearch({ containerRef, headerRef, bottomAppBarRef }: Use
|
|||||||
|
|
||||||
// Subscribe to online status for all search results
|
// Subscribe to online status for all search results
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const activeDmUserId = chat.activeDm?.userId;
|
const activeDmUserId = activeDm?.userId;
|
||||||
const switchingToUserId = switchingToUserIdRef.current;
|
const switchingToUserId = switchingToUserIdRef.current;
|
||||||
const currentSearchResultIds = new Set(searchResults.map(u => u.id));
|
const currentSearchResultIds = new Set(searchResults.map(u => u.id));
|
||||||
const previousSearchResultIds = new Set(previousSearchResultIdsRef.current);
|
const previousSearchResultIds = new Set(previousSearchResultIdsRef.current);
|
||||||
@@ -101,12 +103,12 @@ export function UsernameSearch({ containerRef, headerRef, bottomAppBarRef }: Use
|
|||||||
|
|
||||||
// Clear the ref if the user is now the active DM (state has updated)
|
// Clear the ref if the user is now the active DM (state has updated)
|
||||||
const finalSwitchingToUserId = switchingToUserIdRef.current;
|
const finalSwitchingToUserId = switchingToUserIdRef.current;
|
||||||
const finalActiveDmUserId = chat.activeDm?.userId;
|
const finalActiveDmUserId = activeDm?.userId;
|
||||||
if (finalSwitchingToUserId && finalSwitchingToUserId === finalActiveDmUserId) {
|
if (finalSwitchingToUserId && finalSwitchingToUserId === finalActiveDmUserId) {
|
||||||
switchingToUserIdRef.current = null;
|
switchingToUserIdRef.current = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [searchResults, chat.activeDm?.userId]);
|
}, [searchResults, activeDm?.userId]);
|
||||||
|
|
||||||
|
|
||||||
async function handleUserClick(searchUser: SearchUser) {
|
async function handleUserClick(searchUser: SearchUser) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { MaterialList, MaterialListItem } from "@/utils/material";
|
import { MaterialList, MaterialListItem } from "@/utils/material";
|
||||||
import { useAppState } from "@/pages/chat/state";
|
import { useUserStore } from "@/state/user";
|
||||||
import { deleteAccount } from "@/core/api/account";
|
import { deleteAccount } from "@/core/api/account";
|
||||||
import { confirm } from "mdui/functions/confirm";
|
import { confirm } from "mdui/functions/confirm";
|
||||||
import styles from "@/pages/chat/css/settings-dialog.module.scss";
|
import styles from "@/pages/chat/css/settings-dialog.module.scss";
|
||||||
@@ -9,7 +9,7 @@ interface AccountPanelProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function AccountPanel({ onClose }: AccountPanelProps) {
|
export function AccountPanel({ onClose }: AccountPanelProps) {
|
||||||
const { user, logout } = useAppState();
|
const { user, logout } = useUserStore();
|
||||||
const authToken = user?.authToken;
|
const authToken = user?.authToken;
|
||||||
|
|
||||||
async function handleDeleteAccount() {
|
async function handleDeleteAccount() {
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { StyledDialog } from "@/core/components/StyledDialog";
|
import { StyledDialog } from "@/core/components/StyledDialog";
|
||||||
import type { DialogProps } from "@/core/types";
|
import type { DialogProps } from "@/core/types";
|
||||||
import { useAppState } from "@/pages/chat/state";
|
import { useUserStore } from "@/state/user";
|
||||||
import { changePassword } from "@/core/api/account";
|
import { changePassword } from "@/core/api/account";
|
||||||
import { MaterialButton, MaterialIconButton, MaterialSwitch, MaterialTextField } from "@/utils/material";
|
import { MaterialButton, MaterialIconButton, MaterialSwitch, MaterialTextField } from "@/utils/material";
|
||||||
import styles from "@/pages/chat/css/changePasswordDialog.module.scss";
|
import styles from "@/pages/chat/css/changePasswordDialog.module.scss";
|
||||||
|
|
||||||
export default function ChangePasswordDialog({ isOpen, onOpenChange }: DialogProps) {
|
export default function ChangePasswordDialog({ isOpen, onOpenChange }: DialogProps) {
|
||||||
const { user } = useAppState();
|
const { user } = useUserStore();
|
||||||
|
|
||||||
const [current, setCurrent] = useState("");
|
const [current, setCurrent] = useState("");
|
||||||
const [next, setNext] = useState("");
|
const [next, setNext] = useState("");
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useImmer } from "use-immer";
|
import { useImmer } from "use-immer";
|
||||||
import { MaterialList, MaterialListItem, MaterialButton, MaterialCircularProgress } from "@/utils/material";
|
import { MaterialList, MaterialListItem, MaterialButton, MaterialCircularProgress } from "@/utils/material";
|
||||||
import { useAppState } from "@/pages/chat/state";
|
import { useUserStore } from "@/state/user";
|
||||||
import { listDevices, revokeDevice, logoutAllOtherDevices, type DeviceInfo } from "@/core/api/account/devices";
|
import { listDevices, revokeDevice, logoutAllOtherDevices, type DeviceInfo } from "@/core/api/account/devices";
|
||||||
import { confirm } from "mdui/functions/confirm";
|
import { confirm } from "mdui/functions/confirm";
|
||||||
import styles from "@/pages/chat/css/settings-dialog.module.scss";
|
import styles from "@/pages/chat/css/settings-dialog.module.scss";
|
||||||
|
|
||||||
export function DevicesPanel() {
|
export function DevicesPanel() {
|
||||||
const { user } = useAppState();
|
const { user } = useUserStore();
|
||||||
const authToken = user?.authToken ?? null;
|
const authToken = user?.authToken ?? null;
|
||||||
const [devices, updateDevices] = useImmer<DeviceInfo[]>([]);
|
const [devices, updateDevices] = useImmer<DeviceInfo[]>([]);
|
||||||
const [devicesLoading, setDevicesLoading] = useState(false);
|
const [devicesLoading, setDevicesLoading] = useState(false);
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { useState, useRef } from "react";
|
import { useState, useRef } from "react";
|
||||||
import { MaterialList, MaterialListItem, MaterialSwitch, type MDUISwitch } from "@/utils/material";
|
import { MaterialList, MaterialListItem, MaterialSwitch, type MDUISwitch } from "@/utils/material";
|
||||||
import { useAppState } from "@/pages/chat/state";
|
import { useUserStore } from "@/state/user";
|
||||||
import { initialize, subscribe, unsubscribe, isSupported } from "@/core/push-notifications/push-notifications";
|
import { initialize, subscribe, unsubscribe, isSupported } from "@/core/push-notifications/push-notifications";
|
||||||
import { isElectron } from "@/core/electron/electron";
|
import { isElectron } from "@/core/electron/electron";
|
||||||
import { unsubscribeFromPush } from "@/core/api/push";
|
import { unsubscribeFromPush } from "@/core/api/push";
|
||||||
import styles from "@/pages/chat/css/settings-dialog.module.scss";
|
import styles from "@/pages/chat/css/settings-dialog.module.scss";
|
||||||
|
|
||||||
export function NotificationsPanel() {
|
export function NotificationsPanel() {
|
||||||
const { user } = useAppState();
|
const { user } = useUserStore();
|
||||||
const authToken = user?.authToken ?? null;
|
const authToken = user?.authToken ?? null;
|
||||||
const [pushEnabled, setPushEnabled] = useState(false);
|
const [pushEnabled, setPushEnabled] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useAppState } from "@/pages/chat/state";
|
import { useChatStore } from "@/state/chat";
|
||||||
import defaultAvatar from "@/images/default-avatar.png";
|
import defaultAvatar from "@/images/default-avatar.png";
|
||||||
|
|
||||||
export function ChatMainHeader() {
|
export function ChatMainHeader() {
|
||||||
const { currentChat } = useAppState().chat;
|
const { currentChat } = useChatStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="chat-header">
|
<div className="chat-header">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Message } from "./Message";
|
import { Message } from "./Message";
|
||||||
import { useAppState } from "@/pages/chat/state";
|
import { useUserStore } from "@/state/user";
|
||||||
import type { Message as MessageType } from "@/core/types";
|
import type { Message as MessageType } from "@/core/types";
|
||||||
import { MessageContextMenu, type ContextMenuState } from "./MessageContextMenu";
|
import { MessageContextMenu, type ContextMenuState } from "./MessageContextMenu";
|
||||||
import { useState, type ReactNode } from "react";
|
import { useState, type ReactNode } from "react";
|
||||||
@@ -20,7 +20,7 @@ interface ChatMessagesProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ChatMessages({ messages = [], children, isDm = false, onReplySelect, onEditSelect, onDelete, onRetryMessage, dmRecipientPublicKey }: ChatMessagesProps) {
|
export function ChatMessages({ messages = [], children, isDm = false, onReplySelect, onEditSelect, onDelete, onRetryMessage, dmRecipientPublicKey }: ChatMessagesProps) {
|
||||||
const { user } = useAppState();
|
const { user } = useUserStore();
|
||||||
|
|
||||||
// Context menu state
|
// Context menu state
|
||||||
const [contextMenu, setContextMenu] = useState<ContextMenuState>({
|
const [contextMenu, setContextMenu] = useState<ContextMenuState>({
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import { useEffect, useState, useRef, useMemo } from "react";
|
|||||||
import { getCurrentKeys, getAuthHeaders } from "@/core/api/account";
|
import { getCurrentKeys, getAuthHeaders } from "@/core/api/account";
|
||||||
import { ecdhSharedSecret, deriveWrappingKey } from "@/utils/crypto/asymmetric";
|
import { ecdhSharedSecret, deriveWrappingKey } from "@/utils/crypto/asymmetric";
|
||||||
import { importAesGcmKey, aesGcmDecrypt } from "@/utils/crypto/symmetric";
|
import { importAesGcmKey, aesGcmDecrypt } from "@/utils/crypto/symmetric";
|
||||||
import { useAppState } from "@/pages/chat/state";
|
import { useUserStore } from "@/state/user";
|
||||||
|
import { useProfileStore } from "@/state/profile";
|
||||||
import { fetchUserProfileById, fetchUserProfile } from "@/core/api/account/profile";
|
import { fetchUserProfileById, fetchUserProfile } from "@/core/api/account/profile";
|
||||||
import { StatusBadge } from "@/core/components/StatusBadge";
|
import { StatusBadge } from "@/core/components/StatusBadge";
|
||||||
import { ub64 } from "@/utils/utils";
|
import { ub64 } from "@/utils/utils";
|
||||||
@@ -25,7 +26,7 @@ interface MessageReactionsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Reactions({ reactions, onReactionClick, messageId }: MessageReactionsProps) {
|
function Reactions({ reactions, onReactionClick, messageId }: MessageReactionsProps) {
|
||||||
const { user } = useAppState();
|
const { user } = useUserStore();
|
||||||
const [visibleReactions, setVisibleReactions] = useState<Reaction[]>([]);
|
const [visibleReactions, setVisibleReactions] = useState<Reaction[]>([]);
|
||||||
const [animatingReactions, setAnimatingReactions] = useState<Set<string>>(new Set());
|
const [animatingReactions, setAnimatingReactions] = useState<Set<string>>(new Set());
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
@@ -162,7 +163,8 @@ export function Message({ message, isAuthor, onContextMenu, onReactionClick, isD
|
|||||||
endRect: Rect;
|
endRect: Rect;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [isAnimatingOpen, setIsAnimatingOpen] = useState(false);
|
const [isAnimatingOpen, setIsAnimatingOpen] = useState(false);
|
||||||
const { user, setProfileDialog } = useAppState();
|
const { user } = useUserStore();
|
||||||
|
const { setProfileDialog } = useProfileStore();
|
||||||
const imageRefs = useRef<Map<string, HTMLImageElement>>(new Map());
|
const imageRefs = useRef<Map<string, HTMLImageElement>>(new Map());
|
||||||
const dmEnvelope = message.runtimeData?.dmEnvelope;
|
const dmEnvelope = message.runtimeData?.dmEnvelope;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import type { Message, Size2D } from "@/core/types";
|
import type { Message, Size2D } from "@/core/types";
|
||||||
import { EmojiMenu } from "./EmojiMenu";
|
import { EmojiMenu } from "./EmojiMenu";
|
||||||
import { useAppState } from "@/pages/chat/state";
|
import { useUserStore } from "@/state/user";
|
||||||
import styles from "@/pages/chat/css/MessageContextMenu.module.scss";
|
import styles from "@/pages/chat/css/MessageContextMenu.module.scss";
|
||||||
|
|
||||||
interface MessageContextMenuProps {
|
interface MessageContextMenuProps {
|
||||||
@@ -35,7 +35,7 @@ export function MessageContextMenu({
|
|||||||
isOpen,
|
isOpen,
|
||||||
onOpenChange
|
onOpenChange
|
||||||
}: MessageContextMenuProps) {
|
}: MessageContextMenuProps) {
|
||||||
const { user } = useAppState();
|
const { user } = useUserStore();
|
||||||
// Internal state for closing animation
|
// Internal state for closing animation
|
||||||
const [isClosing, setIsClosing] = useState(false);
|
const [isClosing, setIsClosing] = useState(false);
|
||||||
const [reactionBarPosition, setReactionBarPosition] = useState<Size2D>({ x: 0, y: 0 });
|
const [reactionBarPosition, setReactionBarPosition] = useState<Size2D>({ x: 0, y: 0 });
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { useState, useEffect, useRef, useMemo, type ReactNode } from "react";
|
import { useState, useEffect, useRef, useMemo, type ReactNode } from "react";
|
||||||
import { motion, AnimatePresence } from "motion/react";
|
import { motion, AnimatePresence } from "motion/react";
|
||||||
import { useAppState } from "@/pages/chat/state";
|
import { useChatStore } from "@/state/chat";
|
||||||
|
import { useUserStore } from "@/state/user";
|
||||||
|
import { usePresenceStore } from "@/state/presence";
|
||||||
|
import { useProfileStore } from "@/state/profile";
|
||||||
import { MessagePanel, type MessagePanelState } from "./panels/MessagePanel";
|
import { MessagePanel, type MessagePanelState } from "./panels/MessagePanel";
|
||||||
import { ChatMessages } from "./ChatMessages";
|
import { ChatMessages } from "./ChatMessages";
|
||||||
import { ChatInputWrapper } from "./ChatInputWrapper";
|
import { ChatInputWrapper } from "./ChatInputWrapper";
|
||||||
@@ -23,19 +26,20 @@ interface MessagePanelRendererProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ChatHeaderText({ panel }: { panel: MessagePanel | null }) {
|
function ChatHeaderText({ panel }: { panel: MessagePanel | null }) {
|
||||||
const { chat, user } = useAppState();
|
const { typingUsers, dmTypingUsers } = usePresenceStore();
|
||||||
|
const { user } = useUserStore();
|
||||||
const otherTypingUsers = useMemo(() => {
|
const otherTypingUsers = useMemo(() => {
|
||||||
return Array
|
return Array
|
||||||
.from(chat.typingUsers.entries())
|
.from(typingUsers.entries())
|
||||||
.filter(([userId, username]) => userId !== user.currentUser?.id && username)
|
.filter(([userId, username]) => userId !== user.currentUser?.id && username)
|
||||||
.map(([, username]) => username!);
|
.map(([, username]) => username!);
|
||||||
}, [chat.typingUsers, user.currentUser?.id]);
|
}, [typingUsers, user.currentUser?.id]);
|
||||||
|
|
||||||
let content: ReactNode;
|
let content: ReactNode;
|
||||||
|
|
||||||
if (panel instanceof DMPanel) {
|
if (panel instanceof DMPanel) {
|
||||||
const recipientId = panel.getRecipientId()!;
|
const recipientId = panel.getRecipientId()!;
|
||||||
const isTyping = chat.dmTypingUsers.get(recipientId);
|
const isTyping = dmTypingUsers.get(recipientId);
|
||||||
|
|
||||||
content = isTyping ? <TypingIndicator typingUsers={[]} /> : <OnlineStatus userId={recipientId} />;
|
content = isTyping ? <TypingIndicator typingUsers={[]} /> : <OnlineStatus userId={recipientId} />;
|
||||||
} else if (panel instanceof PublicChatPanel && otherTypingUsers.length > 0) {
|
} else if (panel instanceof PublicChatPanel && otherTypingUsers.length > 0) {
|
||||||
@@ -48,7 +52,8 @@ function ChatHeaderText({ panel }: { panel: MessagePanel | null }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function MessagePanelRenderer({ panel }: MessagePanelRendererProps) {
|
export function MessagePanelRenderer({ panel }: MessagePanelRendererProps) {
|
||||||
const { applyPendingPanel, chat, setProfileDialog } = useAppState();
|
const { applyPendingPanel, isSwitching, pendingPanel, activePanel, setIsSwitching } = useChatStore();
|
||||||
|
const { setProfileDialog } = useProfileStore();
|
||||||
const messagePanelRef = useRef<HTMLDivElement>(null);
|
const messagePanelRef = useRef<HTMLDivElement>(null);
|
||||||
const [panelState, setPanelState] = useState<MessagePanelState | null>(null);
|
const [panelState, setPanelState] = useState<MessagePanelState | null>(null);
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -121,30 +126,30 @@ export function MessagePanelRenderer({ panel }: MessagePanelRendererProps) {
|
|||||||
|
|
||||||
// Handle chat switching animation
|
// Handle chat switching animation
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (chat.isSwitching && chat.pendingPanel) {
|
if (isSwitching && pendingPanel) {
|
||||||
// Apply pending panel when animation starts
|
// Apply pending panel when animation starts
|
||||||
applyPendingPanel();
|
applyPendingPanel();
|
||||||
// End switching state after a brief delay to allow animation
|
// End switching state after a brief delay to allow animation
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
chat.setIsSwitching(false);
|
setIsSwitching(false);
|
||||||
}, 200);
|
}, 200);
|
||||||
}
|
}
|
||||||
}, [chat.isSwitching, chat.pendingPanel, applyPendingPanel]);
|
}, [isSwitching, pendingPanel, applyPendingPanel, setIsSwitching]);
|
||||||
|
|
||||||
// Load messages when panel changes and animation is not running
|
// Load messages when panel changes and animation is not running
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!chat.activePanel || chat.isSwitching) return;
|
if (!activePanel || isSwitching) return;
|
||||||
|
|
||||||
const panelState = chat.activePanel.getState();
|
const panelState = activePanel.getState();
|
||||||
|
|
||||||
if (panelState.messages.length === 0 && !panelState.isLoading) {
|
if (panelState.messages.length === 0 && !panelState.isLoading) {
|
||||||
chat.activePanel.loadMessages();
|
activePanel.loadMessages();
|
||||||
}
|
}
|
||||||
}, [chat.activePanel, chat.isSwitching]);
|
}, [activePanel, isSwitching]);
|
||||||
|
|
||||||
// Scroll to bottom only when new messages are added
|
// Scroll to bottom only when new messages are added
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!panelState || chat.isSwitching) return;
|
if (!panelState || isSwitching) return;
|
||||||
|
|
||||||
const currentMessageCount = panelState.messages.length;
|
const currentMessageCount = panelState.messages.length;
|
||||||
const previousMessageCount = previousMessageCountRef.current;
|
const previousMessageCount = previousMessageCountRef.current;
|
||||||
@@ -168,7 +173,7 @@ export function MessagePanelRenderer({ panel }: MessagePanelRendererProps) {
|
|||||||
|
|
||||||
// Update the previous message count
|
// Update the previous message count
|
||||||
previousMessageCountRef.current = currentMessageCount;
|
previousMessageCountRef.current = currentMessageCount;
|
||||||
}, [panelState?.messages, panelState?.isLoading, chat.isSwitching]);
|
}, [panelState?.messages, panelState?.isLoading, isSwitching]);
|
||||||
|
|
||||||
function handleCallClick() {
|
function handleCallClick() {
|
||||||
if (panel && panelState && panel.isDm()) {
|
if (panel && panelState && panel.isDm()) {
|
||||||
@@ -195,7 +200,7 @@ export function MessagePanelRenderer({ panel }: MessagePanelRendererProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const panelKey = chat.activePanel?.getState().title || "empty";
|
const panelKey = activePanel?.getState().title || "empty";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.chatContainer}>
|
<div className={styles.chatContainer}>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* @version 1.0.0
|
* @version 1.0.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useAppState } from "@/pages/chat/state";
|
import { usePresenceStore } from "@/state/presence";
|
||||||
import styles from "@/pages/chat/css/TypingIndicators.module.scss";
|
import styles from "@/pages/chat/css/TypingIndicators.module.scss";
|
||||||
|
|
||||||
interface OnlineIndicatorProps {
|
interface OnlineIndicatorProps {
|
||||||
@@ -14,8 +14,8 @@ interface OnlineIndicatorProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function OnlineIndicator({ userId, className = "" }: OnlineIndicatorProps) {
|
export function OnlineIndicator({ userId, className = "" }: OnlineIndicatorProps) {
|
||||||
const { chat } = useAppState();
|
const { onlineStatuses } = usePresenceStore();
|
||||||
const status = chat.onlineStatuses.get(userId);
|
const status = onlineStatuses.get(userId);
|
||||||
|
|
||||||
// Only show indicator when user is online
|
// Only show indicator when user is online
|
||||||
if (!status || !status.online) {
|
if (!status || !status.online) {
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
* @version 1.0.0
|
* @version 1.0.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useAppState } from "@/pages/chat/state";
|
import { usePresenceStore } from "@/state/presence";
|
||||||
|
import { useUserStore } from "@/state/user";
|
||||||
import styles from "@/pages/chat/css/TypingIndicators.module.scss";
|
import styles from "@/pages/chat/css/TypingIndicators.module.scss";
|
||||||
|
|
||||||
interface OnlineStatusProps {
|
interface OnlineStatusProps {
|
||||||
@@ -14,8 +15,9 @@ interface OnlineStatusProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function OnlineStatus({ userId, showLastSeen = false }: OnlineStatusProps) {
|
export function OnlineStatus({ userId, showLastSeen = false }: OnlineStatusProps) {
|
||||||
const { chat, user } = useAppState();
|
const { onlineStatuses } = usePresenceStore();
|
||||||
const status = userId === user.currentUser?.id ? { online: true, lastSeen: new Date().toISOString() } : chat.onlineStatuses.get(userId);
|
const { user } = useUserStore();
|
||||||
|
const status = userId === user.currentUser?.id ? { online: true, lastSeen: new Date().toISOString() } : onlineStatuses.get(userId);
|
||||||
|
|
||||||
function formatLastSeen(lastSeen: string): string {
|
function formatLastSeen(lastSeen: string): string {
|
||||||
const date = new Date(lastSeen);
|
const date = new Date(lastSeen);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useAppState } from "@/pages/chat/state";
|
import { useChatStore } from "@/state/chat";
|
||||||
import { MessagePanelRenderer } from "./MessagePanelRenderer";
|
import { MessagePanelRenderer } from "./MessagePanelRenderer";
|
||||||
|
|
||||||
export function RightPanel() {
|
export function RightPanel() {
|
||||||
const { chat } = useAppState();
|
const { activePanel } = useChatStore();
|
||||||
|
|
||||||
return <MessagePanelRenderer panel={chat.activePanel} />
|
return <MessagePanelRenderer panel={activePanel} />
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useAppState } from "@/pages/chat/state";
|
import { useCallStore } from "@/state/call";
|
||||||
|
import { useUserStore } from "@/state/user";
|
||||||
import useCall from "@/pages/chat/hooks/useCall";
|
import useCall from "@/pages/chat/hooks/useCall";
|
||||||
import defaultAvatar from "@/images/default-avatar.png";
|
import defaultAvatar from "@/images/default-avatar.png";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
@@ -9,8 +10,8 @@ import { motion, AnimatePresence } from "motion/react";
|
|||||||
import styles from "@/pages/chat/css/callWindow.module.scss";
|
import styles from "@/pages/chat/css/callWindow.module.scss";
|
||||||
|
|
||||||
export function CallWindow() {
|
export function CallWindow() {
|
||||||
const { chat, toggleCallMinimize, user } = useAppState();
|
const { call, toggleCallMinimized } = useCallStore();
|
||||||
const { call } = chat;
|
const { user } = useUserStore();
|
||||||
const {
|
const {
|
||||||
acceptCall,
|
acceptCall,
|
||||||
rejectCall,
|
rejectCall,
|
||||||
@@ -158,7 +159,7 @@ export function CallWindow() {
|
|||||||
<div className={styles.callHeader}>
|
<div className={styles.callHeader}>
|
||||||
<div className={styles.windowControls}>
|
<div className={styles.windowControls}>
|
||||||
<MaterialIconButton
|
<MaterialIconButton
|
||||||
onClick={toggleCallMinimize}
|
onClick={toggleCallMinimized}
|
||||||
icon={call.isMinimized ? "open_in_full" : "close_fullscreen"}
|
icon={call.isMinimized ? "open_in_full" : "close_fullscreen"}
|
||||||
className={styles.windowControlBtn}
|
className={styles.windowControlBtn}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { useAppState } from "@/pages/chat/state";
|
import { useCallStore } from "@/state/call";
|
||||||
import useCall from "@/pages/chat/hooks/useCall";
|
import useCall from "@/pages/chat/hooks/useCall";
|
||||||
import defaultAvatar from "@/images/default-avatar.png";
|
import defaultAvatar from "@/images/default-avatar.png";
|
||||||
import { MaterialIconButton } from "@/utils/material";
|
import { MaterialIconButton } from "@/utils/material";
|
||||||
|
|
||||||
export function MinimizedCallBar() {
|
export function MinimizedCallBar() {
|
||||||
const { chat, toggleCallMinimize } = useAppState();
|
const { call, toggleCallMinimized } = useCallStore();
|
||||||
const { call } = chat;
|
|
||||||
const { endCall, toggleMute } = useCall();
|
const { endCall, toggleMute } = useCall();
|
||||||
|
|
||||||
function getGradientClass() {
|
function getGradientClass() {
|
||||||
@@ -39,7 +38,7 @@ export function MinimizedCallBar() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`minimized-call-bar ${getGradientClass()}`} onClick={toggleCallMinimize}>
|
<div className={`minimized-call-bar ${getGradientClass()}`} onClick={toggleCallMinimized}>
|
||||||
<div className="call-info">
|
<div className="call-info">
|
||||||
<img src={defaultAvatar} alt="Avatar" className="avatar" />
|
<img src={defaultAvatar} alt="Avatar" className="avatar" />
|
||||||
<div className="user-details">
|
<div className="user-details">
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
} from "@/core/api/dm";
|
} from "@/core/api/dm";
|
||||||
import { fetchUserProfileById } from "@/core/api/account/profile";
|
import { fetchUserProfileById } from "@/core/api/account/profile";
|
||||||
import type { DmEncryptedJSON, DmEnvelope, DMWebSocketMessage, EncryptedMessageJson, Message } from "@/core/types";
|
import type { DmEncryptedJSON, DmEnvelope, DMWebSocketMessage, EncryptedMessageJson, Message } from "@/core/types";
|
||||||
import type { UserState, ProfileDialogData } from "@/pages/chat/state";
|
import type { UserState, ProfileDialogData } from "@/state/types";
|
||||||
import { formatDMUsername } from "@/pages/chat/hooks/useDM";
|
import { formatDMUsername } from "@/pages/chat/hooks/useDM";
|
||||||
import { onlineStatusManager } from "@/core/onlineStatusManager";
|
import { onlineStatusManager } from "@/core/onlineStatusManager";
|
||||||
import { typingManager } from "@/core/typingManager";
|
import { typingManager } from "@/core/typingManager";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Message, WebSocketMessage } from "@/core/types";
|
import type { Message, WebSocketMessage } from "@/core/types";
|
||||||
import type { UserState, ProfileDialogData } from "@/pages/chat/state";
|
import type { UserState, ProfileDialogData } from "@/state/types";
|
||||||
|
|
||||||
export interface MessagePanelState {
|
export interface MessagePanelState {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { MessagePanel } from "./MessagePanel";
|
import { MessagePanel } from "./MessagePanel";
|
||||||
import { request } from "@/core/websocket";
|
import { request } from "@/core/websocket";
|
||||||
import type { ChatWebSocketMessage, Message, ReactionUpdateWebSocketMessage } from "@/core/types";
|
import type { ChatWebSocketMessage, Message, ReactionUpdateWebSocketMessage } from "@/core/types";
|
||||||
import type { UserState, ProfileDialogData } from "@/pages/chat/state";
|
import type { UserState, ProfileDialogData } from "@/state/types";
|
||||||
import { fetchMessages, sendMessage, sendMessageWithFiles } from "@/core/api/messaging";
|
import { fetchMessages, sendMessage, sendMessageWithFiles } from "@/core/api/messaging";
|
||||||
|
|
||||||
export class PublicChatPanel extends MessagePanel {
|
export class PublicChatPanel extends MessagePanel {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useAppState } from "@/pages/chat/state";
|
import { useUserStore } from "@/state/user";
|
||||||
import styles from "./home.module.scss";
|
import styles from "./home.module.scss";
|
||||||
import useDownloadAppScreen from "@/core/hooks/useDownloadAppScreen";
|
import useDownloadAppScreen from "@/core/hooks/useDownloadAppScreen";
|
||||||
import { MaterialButton, MaterialIcon } from "@/utils/material";
|
import { MaterialButton, MaterialIcon } from "@/utils/material";
|
||||||
@@ -18,7 +18,7 @@ function SupportLink({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { user } = useAppState();
|
const { user } = useUserStore();
|
||||||
const { isMobile } = useDownloadAppScreen();
|
const { isMobile } = useDownloadAppScreen();
|
||||||
const isLoggedIn = user.authToken && user.currentUser;
|
const isLoggedIn = user.authToken && user.currentUser;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
import type { CallStatus, CallState } from "./types";
|
||||||
|
|
||||||
|
interface CallStore {
|
||||||
|
call: CallState;
|
||||||
|
startCall: (userId: number, username: string) => void;
|
||||||
|
endCall: () => void;
|
||||||
|
setCallStatus: (status: CallStatus) => void;
|
||||||
|
toggleMute: () => void;
|
||||||
|
toggleCallMinimize: () => void;
|
||||||
|
receiveCall: (userId: number, username: string) => void;
|
||||||
|
setCallEncryption: (sessionKeyHash: string, encryptionEmojis: string[]) => void;
|
||||||
|
setCallSessionKeyHash: (sessionKeyHash: string) => void;
|
||||||
|
toggleVideo: () => void;
|
||||||
|
toggleScreenShare: () => void;
|
||||||
|
setRemoteVideoEnabled: (enabled: boolean) => void;
|
||||||
|
setRemoteScreenSharing: (enabled: boolean) => void;
|
||||||
|
toggleCallMinimized: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialCallState: CallState = {
|
||||||
|
isActive: false,
|
||||||
|
status: "ended",
|
||||||
|
startTime: null,
|
||||||
|
isMuted: false,
|
||||||
|
remoteUserId: null,
|
||||||
|
remoteUsername: null,
|
||||||
|
isInitiator: false,
|
||||||
|
isMinimized: false,
|
||||||
|
sessionKeyHash: null,
|
||||||
|
encryptionEmojis: [],
|
||||||
|
isVideoEnabled: false,
|
||||||
|
isRemoteVideoEnabled: false,
|
||||||
|
isSharingScreen: false,
|
||||||
|
isRemoteScreenSharing: false
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCallStore = create<CallStore>((set) => ({
|
||||||
|
call: initialCallState,
|
||||||
|
startCall: (userId: number, username: string) => set({
|
||||||
|
call: {
|
||||||
|
...initialCallState,
|
||||||
|
isActive: true,
|
||||||
|
status: "calling",
|
||||||
|
remoteUserId: userId,
|
||||||
|
remoteUsername: username,
|
||||||
|
isInitiator: true
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
endCall: () => set({ call: initialCallState }),
|
||||||
|
setCallStatus: (status: CallStatus) => set((state) => ({
|
||||||
|
call: {
|
||||||
|
...state.call,
|
||||||
|
status,
|
||||||
|
startTime: status === "active" && !state.call.startTime ? Date.now() : state.call.startTime
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
toggleMute: () => set((state) => ({
|
||||||
|
call: {
|
||||||
|
...state.call,
|
||||||
|
isMuted: !state.call.isMuted
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
toggleCallMinimize: () => set((state) => ({
|
||||||
|
call: {
|
||||||
|
...state.call,
|
||||||
|
isMinimized: !state.call.isMinimized
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
receiveCall: (userId: number, username: string) => set({
|
||||||
|
call: {
|
||||||
|
...initialCallState,
|
||||||
|
isActive: true,
|
||||||
|
status: "calling",
|
||||||
|
remoteUserId: userId,
|
||||||
|
remoteUsername: username,
|
||||||
|
isInitiator: false
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
setCallEncryption: (sessionKeyHash: string, encryptionEmojis: string[]) => set((state) => ({
|
||||||
|
call: {
|
||||||
|
...state.call,
|
||||||
|
sessionKeyHash,
|
||||||
|
encryptionEmojis
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
setCallSessionKeyHash: (sessionKeyHash: string) => set((state) => ({
|
||||||
|
call: {
|
||||||
|
...state.call,
|
||||||
|
sessionKeyHash
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
toggleVideo: () => set((state) => ({
|
||||||
|
call: {
|
||||||
|
...state.call,
|
||||||
|
isVideoEnabled: !state.call.isVideoEnabled
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
toggleScreenShare: () => set((state) => ({
|
||||||
|
call: {
|
||||||
|
...state.call,
|
||||||
|
isSharingScreen: !state.call.isSharingScreen
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
setRemoteVideoEnabled: (enabled: boolean) => set((state) => ({
|
||||||
|
call: {
|
||||||
|
...state.call,
|
||||||
|
isRemoteVideoEnabled: enabled
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
setRemoteScreenSharing: (enabled: boolean) => set((state) => ({
|
||||||
|
call: {
|
||||||
|
...state.call,
|
||||||
|
isRemoteScreenSharing: enabled
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
toggleCallMinimized: () => set((state) => ({
|
||||||
|
call: {
|
||||||
|
...state.call,
|
||||||
|
isMinimized: !state.call.isMinimized
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}));
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
import type { Message, User } from "@/core/types";
|
||||||
|
import { MessagePanel } from "@/pages/chat/ui/right/panels/MessagePanel";
|
||||||
|
import { PublicChatPanel } from "@/pages/chat/ui/right/panels/PublicChatPanel";
|
||||||
|
import { DMPanel } from "@/pages/chat/ui/right/panels/DMPanel";
|
||||||
|
import type { DMPanelData } from "@/pages/chat/ui/right/panels/DMPanel";
|
||||||
|
import type { ChatTabs, ActiveDM } from "./types";
|
||||||
|
import { useUserStore } from "./user";
|
||||||
|
|
||||||
|
interface ChatStore {
|
||||||
|
messages: Message[];
|
||||||
|
currentChat: string;
|
||||||
|
activeTab: ChatTabs;
|
||||||
|
dmUsers: User[];
|
||||||
|
activeDm: ActiveDM | null;
|
||||||
|
isSwitching: boolean;
|
||||||
|
setIsSwitching: (value: boolean) => void;
|
||||||
|
activePanel: MessagePanel | null;
|
||||||
|
publicChatPanel: PublicChatPanel | null;
|
||||||
|
dmPanel: DMPanel | null;
|
||||||
|
pendingPanel?: MessagePanel | null;
|
||||||
|
addMessage: (message: Message) => void;
|
||||||
|
updateMessage: (messageId: number, updatedMessage: Partial<Message>) => void;
|
||||||
|
removeMessage: (messageId: number) => void;
|
||||||
|
setCurrentChat: (chat: string) => void;
|
||||||
|
setActiveTab: (tab: ChatTabs) => void;
|
||||||
|
setDmUsers: (users: User[]) => void;
|
||||||
|
setActiveDm: (dm: ActiveDM | null) => void;
|
||||||
|
clearMessages: () => void;
|
||||||
|
setActivePanel: (panel: MessagePanel | null) => void;
|
||||||
|
setPendingPanel: (panel: MessagePanel | null) => void;
|
||||||
|
applyPendingPanel: () => void;
|
||||||
|
switchToPublicChat: (chatName: string) => Promise<void>;
|
||||||
|
switchToDM: (dmData: DMPanelData) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useChatStore = create<ChatStore>((set, get) => ({
|
||||||
|
messages: [],
|
||||||
|
currentChat: "Общий чат",
|
||||||
|
activeTab: "chats",
|
||||||
|
dmUsers: [],
|
||||||
|
activeDm: null,
|
||||||
|
isSwitching: false,
|
||||||
|
setIsSwitching: (value: boolean) => set({ isSwitching: value }),
|
||||||
|
activePanel: null,
|
||||||
|
publicChatPanel: null,
|
||||||
|
dmPanel: null,
|
||||||
|
pendingPanel: null,
|
||||||
|
addMessage: (message: Message) => set((state) => {
|
||||||
|
const messageExists = state.messages.some(msg => msg.id === message.id);
|
||||||
|
if (messageExists) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
messages: [...state.messages, message]
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
updateMessage: (messageId: number, updatedMessage: Partial<Message>) => set((state) => ({
|
||||||
|
messages: state.messages.map(msg =>
|
||||||
|
msg.id === messageId ? { ...msg, ...updatedMessage } : msg
|
||||||
|
)
|
||||||
|
})),
|
||||||
|
removeMessage: (messageId: number) => set((state) => ({
|
||||||
|
messages: state.messages.filter(msg => msg.id !== messageId)
|
||||||
|
})),
|
||||||
|
clearMessages: () => set({ messages: [] }),
|
||||||
|
setCurrentChat: (chat: string) => set({ currentChat: chat }),
|
||||||
|
setActiveTab: (tab: ChatTabs) => set({ activeTab: tab }),
|
||||||
|
setDmUsers: (users: User[]) => set({ dmUsers: users }),
|
||||||
|
setActiveDm: (dm: ActiveDM | null) => set({ activeDm: dm }),
|
||||||
|
setActivePanel: (panel: MessagePanel | null) => {
|
||||||
|
const state = get();
|
||||||
|
if (state.activePanel && state.activePanel !== panel) {
|
||||||
|
state.activePanel.deactivate();
|
||||||
|
}
|
||||||
|
return set({ activePanel: panel });
|
||||||
|
},
|
||||||
|
setPendingPanel: (panel: MessagePanel | null) => set({ pendingPanel: panel }),
|
||||||
|
applyPendingPanel: () => {
|
||||||
|
const state = get();
|
||||||
|
if (state.activePanel) {
|
||||||
|
state.activePanel.deactivate();
|
||||||
|
}
|
||||||
|
return set((state) => ({
|
||||||
|
activePanel: state.pendingPanel || state.activePanel,
|
||||||
|
publicChatPanel: (state.pendingPanel instanceof PublicChatPanel)
|
||||||
|
? (state.pendingPanel as PublicChatPanel)
|
||||||
|
: state.publicChatPanel,
|
||||||
|
dmPanel: (state.pendingPanel instanceof DMPanel)
|
||||||
|
? (state.pendingPanel as DMPanel)
|
||||||
|
: state.dmPanel,
|
||||||
|
currentChat: state.pendingPanel ? state.pendingPanel.getState().title || state.currentChat : state.currentChat,
|
||||||
|
pendingPanel: null
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
switchToPublicChat: async (chatName: string) => {
|
||||||
|
const { user } = useUserStore.getState();
|
||||||
|
const state = get();
|
||||||
|
|
||||||
|
if (!user.authToken) return;
|
||||||
|
|
||||||
|
state.setIsSwitching(true);
|
||||||
|
|
||||||
|
let publicChatPanel = state.publicChatPanel;
|
||||||
|
if (!publicChatPanel) {
|
||||||
|
publicChatPanel = new PublicChatPanel(chatName, user);
|
||||||
|
} else {
|
||||||
|
publicChatPanel.setChatName(chatName);
|
||||||
|
publicChatPanel.setAuthToken(user.authToken);
|
||||||
|
publicChatPanel.clearMessages();
|
||||||
|
}
|
||||||
|
|
||||||
|
await publicChatPanel.activate();
|
||||||
|
|
||||||
|
set({
|
||||||
|
pendingPanel: publicChatPanel,
|
||||||
|
activeTab: "chats"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
switchToDM: async (dmData: DMPanelData) => {
|
||||||
|
const { user } = useUserStore.getState();
|
||||||
|
const state = get();
|
||||||
|
|
||||||
|
if (!user.authToken) return;
|
||||||
|
|
||||||
|
state.setIsSwitching(true);
|
||||||
|
|
||||||
|
let dmPanel = state.dmPanel;
|
||||||
|
if (!dmPanel) {
|
||||||
|
dmPanel = new DMPanel(user);
|
||||||
|
} else {
|
||||||
|
dmPanel.setAuthToken(user.authToken);
|
||||||
|
dmPanel.clearMessages();
|
||||||
|
}
|
||||||
|
|
||||||
|
dmPanel.setDMData(dmData);
|
||||||
|
|
||||||
|
await dmPanel.activate();
|
||||||
|
|
||||||
|
set({
|
||||||
|
pendingPanel: dmPanel,
|
||||||
|
activeDm: {
|
||||||
|
userId: dmData.userId,
|
||||||
|
username: dmData.username,
|
||||||
|
publicKey: dmData.publicKey
|
||||||
|
},
|
||||||
|
activeTab: "chats"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}));
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
|
||||||
|
interface PresenceStore {
|
||||||
|
onlineStatuses: Map<number, {online: boolean, lastSeen: string}>;
|
||||||
|
typingUsers: Map<number, string>; // userId -> username
|
||||||
|
dmTypingUsers: Map<number, boolean>;
|
||||||
|
updateOnlineStatus: (userId: number, online: boolean, lastSeen: string) => void;
|
||||||
|
addTypingUser: (userId: number, username: string) => void;
|
||||||
|
removeTypingUser: (userId: number) => void;
|
||||||
|
setDmTypingUser: (userId: number, isTyping: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePresenceStore = create<PresenceStore>((set) => ({
|
||||||
|
onlineStatuses: new Map(),
|
||||||
|
typingUsers: new Map(),
|
||||||
|
dmTypingUsers: new Map(),
|
||||||
|
updateOnlineStatus: (userId: number, online: boolean, lastSeen: string) => set((state) => ({
|
||||||
|
onlineStatuses: new Map(state.onlineStatuses).set(userId, { online, lastSeen })
|
||||||
|
})),
|
||||||
|
addTypingUser: (userId: number, username: string) => set((state) => ({
|
||||||
|
typingUsers: new Map(state.typingUsers).set(userId, username)
|
||||||
|
})),
|
||||||
|
removeTypingUser: (userId: number) => set((state) => {
|
||||||
|
const newTypingUsers = new Map(state.typingUsers);
|
||||||
|
newTypingUsers.delete(userId);
|
||||||
|
return {
|
||||||
|
typingUsers: newTypingUsers
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
setDmTypingUser: (userId: number, isTyping: boolean) => set((state) => {
|
||||||
|
const newDmTypingUsers = new Map(state.dmTypingUsers);
|
||||||
|
if (isTyping) {
|
||||||
|
newDmTypingUsers.set(userId, true);
|
||||||
|
} else {
|
||||||
|
newDmTypingUsers.delete(userId);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
dmTypingUsers: newDmTypingUsers
|
||||||
|
};
|
||||||
|
})
|
||||||
|
}));
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
import type { ProfileDialogData } from "./types";
|
||||||
|
|
||||||
|
interface ProfileStore {
|
||||||
|
profileDialog: ProfileDialogData | null;
|
||||||
|
setProfileDialog: (data: ProfileDialogData | null) => void;
|
||||||
|
closeProfileDialog: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useProfileStore = create<ProfileStore>((set) => ({
|
||||||
|
profileDialog: null,
|
||||||
|
setProfileDialog: (data: ProfileDialogData | null) => set({ profileDialog: data }),
|
||||||
|
closeProfileDialog: () => set({ profileDialog: null })
|
||||||
|
}));
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import type { Message, User } from "@/core/types";
|
||||||
|
import { MessagePanel } from "@/pages/chat/ui/right/panels/MessagePanel";
|
||||||
|
import { PublicChatPanel } from "@/pages/chat/ui/right/panels/PublicChatPanel";
|
||||||
|
import { DMPanel } from "@/pages/chat/ui/right/panels/DMPanel";
|
||||||
|
|
||||||
|
export type ChatTabs = "chats" | "channels" | "contacts";
|
||||||
|
|
||||||
|
export type CallStatus = "calling" | "connecting" | "active" | "ended";
|
||||||
|
|
||||||
|
export interface ProfileDialogData {
|
||||||
|
userId?: number;
|
||||||
|
username?: string;
|
||||||
|
display_name?: string;
|
||||||
|
profilePicture?: string;
|
||||||
|
bio?: string;
|
||||||
|
memberSince?: string;
|
||||||
|
online?: boolean;
|
||||||
|
isOwnProfile: boolean;
|
||||||
|
verified?: boolean;
|
||||||
|
suspended?: boolean;
|
||||||
|
suspension_reason?: string | null;
|
||||||
|
deleted?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ActiveDM {
|
||||||
|
userId: number;
|
||||||
|
username: string;
|
||||||
|
publicKey: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CallState {
|
||||||
|
isActive: boolean;
|
||||||
|
status: CallStatus;
|
||||||
|
startTime: number | null;
|
||||||
|
isMuted: boolean;
|
||||||
|
remoteUserId: number | null;
|
||||||
|
remoteUsername: string | null;
|
||||||
|
isInitiator: boolean;
|
||||||
|
isMinimized: boolean;
|
||||||
|
sessionKeyHash: string | null;
|
||||||
|
encryptionEmojis: string[];
|
||||||
|
isVideoEnabled: boolean;
|
||||||
|
isRemoteVideoEnabled: boolean;
|
||||||
|
isSharingScreen: boolean;
|
||||||
|
isRemoteScreenSharing: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatState {
|
||||||
|
messages: Message[];
|
||||||
|
currentChat: string;
|
||||||
|
activeTab: ChatTabs;
|
||||||
|
dmUsers: User[];
|
||||||
|
activeDm: ActiveDM | null;
|
||||||
|
isSwitching: boolean;
|
||||||
|
setIsSwitching: (value: boolean) => void;
|
||||||
|
activePanel: MessagePanel | null;
|
||||||
|
publicChatPanel: PublicChatPanel | null;
|
||||||
|
dmPanel: DMPanel | null;
|
||||||
|
pendingPanel?: MessagePanel | null;
|
||||||
|
call: CallState;
|
||||||
|
profileDialog: ProfileDialogData | null;
|
||||||
|
onlineStatuses: Map<number, {online: boolean, lastSeen: string}>;
|
||||||
|
typingUsers: Map<number, string>; // userId -> username
|
||||||
|
dmTypingUsers: Map<number, boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserState {
|
||||||
|
currentUser: User | null;
|
||||||
|
authToken: string | null;
|
||||||
|
isSuspended: boolean;
|
||||||
|
suspensionReason: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
import type { User } from "@/core/types";
|
||||||
|
import { request } from "@/core/websocket";
|
||||||
|
import { restoreKeys } from "@/core/api/account";
|
||||||
|
import { API_BASE_URL } from "@/core/config";
|
||||||
|
import { getAuthHeaders } from "@/core/api/account";
|
||||||
|
import { initialize, subscribe, startElectronReceiver, isSupported } from "@/core/push-notifications/push-notifications";
|
||||||
|
import { isElectron } from "@/core/electron/electron";
|
||||||
|
import { onlineStatusManager } from "@/core/onlineStatusManager";
|
||||||
|
import { typingManager } from "@/core/typingManager";
|
||||||
|
import type { UserState } from "./types";
|
||||||
|
|
||||||
|
interface UserStore {
|
||||||
|
user: UserState;
|
||||||
|
setUser: (token: string, user: User) => void;
|
||||||
|
logout: () => void;
|
||||||
|
restoreFromStorage: () => Promise<void>;
|
||||||
|
setSuspended: (reason: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUserStore = create<UserStore>((set) => ({
|
||||||
|
user: {
|
||||||
|
currentUser: null,
|
||||||
|
authToken: null,
|
||||||
|
isSuspended: false,
|
||||||
|
suspensionReason: null
|
||||||
|
},
|
||||||
|
setUser: (token: string, user: User) => {
|
||||||
|
set({
|
||||||
|
user: {
|
||||||
|
currentUser: user,
|
||||||
|
authToken: token,
|
||||||
|
isSuspended: user.suspended || false,
|
||||||
|
suspensionReason: user.suspension_reason || null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onlineStatusManager.setAuthToken(token);
|
||||||
|
typingManager.setAuthToken(token);
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.setItem('authToken', token);
|
||||||
|
localStorage.setItem('currentUser', JSON.stringify(user));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to store credentials in localStorage:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
request({
|
||||||
|
type: "ping",
|
||||||
|
credentials: {
|
||||||
|
scheme: "Bearer",
|
||||||
|
credentials: token
|
||||||
|
},
|
||||||
|
data: {}
|
||||||
|
})
|
||||||
|
} catch {}
|
||||||
|
},
|
||||||
|
logout: () => {
|
||||||
|
try {
|
||||||
|
localStorage.removeItem('authToken');
|
||||||
|
localStorage.removeItem('currentUser');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to clear localStorage:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
onlineStatusManager.setAuthToken(null);
|
||||||
|
typingManager.setAuthToken(null);
|
||||||
|
onlineStatusManager.cleanup();
|
||||||
|
typingManager.cleanup();
|
||||||
|
|
||||||
|
set({
|
||||||
|
user: {
|
||||||
|
currentUser: null,
|
||||||
|
authToken: null,
|
||||||
|
isSuspended: false,
|
||||||
|
suspensionReason: null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
restoreFromStorage: async () => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('authToken');
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
const fullResponse = await fetch(`${API_BASE_URL}/user/profile`, {
|
||||||
|
headers: getAuthHeaders(token, true)
|
||||||
|
});
|
||||||
|
if (fullResponse.ok) {
|
||||||
|
const user: User = await fullResponse.json();
|
||||||
|
restoreKeys();
|
||||||
|
|
||||||
|
if (user.suspended) {
|
||||||
|
set({
|
||||||
|
user: {
|
||||||
|
currentUser: user,
|
||||||
|
authToken: token,
|
||||||
|
isSuspended: true,
|
||||||
|
suspensionReason: user.suspension_reason || null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
set({
|
||||||
|
user: {
|
||||||
|
currentUser: user,
|
||||||
|
authToken: token,
|
||||||
|
isSuspended: false,
|
||||||
|
suspensionReason: null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onlineStatusManager.setAuthToken(token);
|
||||||
|
typingManager.setAuthToken(token);
|
||||||
|
|
||||||
|
try {
|
||||||
|
request({
|
||||||
|
type: "ping",
|
||||||
|
credentials: {
|
||||||
|
scheme: "Bearer",
|
||||||
|
credentials: token
|
||||||
|
},
|
||||||
|
data: {}
|
||||||
|
})
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isSupported()) {
|
||||||
|
const initialized = await initialize();
|
||||||
|
if (initialized) {
|
||||||
|
await subscribe(token);
|
||||||
|
|
||||||
|
if (isElectron) {
|
||||||
|
await startElectronReceiver();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Notification setup failed (restored):", e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error("Unable to authenticate");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to restore user from localStorage:', error);
|
||||||
|
localStorage.removeItem('authToken');
|
||||||
|
localStorage.removeItem('currentUser');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setSuspended: (reason: string) => set((state) => ({
|
||||||
|
user: {
|
||||||
|
...state.user,
|
||||||
|
isSuspended: true,
|
||||||
|
suspensionReason: reason
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}));
|
||||||
Reference in New Issue
Block a user