mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-23 03:25:07 +03:00
Create new Material components, redesign the chat UI, add security settings
This commit is contained in:
@@ -1,32 +1,32 @@
|
||||
import { useAppState, type ChatTabs } from "@/pages/chat/state";
|
||||
import { UnifiedChatsList } from "./UnifiedChatsList";
|
||||
import type { Tabs } from "mdui/components/tabs";
|
||||
import { MaterialTab, MaterialTabPanel, MaterialTabs } from "@/utils/material";
|
||||
|
||||
export function ChatTabs() {
|
||||
const { chat, setActiveTab } = useAppState();
|
||||
|
||||
return (
|
||||
<div className="chat-tabs">
|
||||
<mdui-tabs
|
||||
<MaterialTabs
|
||||
value={chat.activeTab}
|
||||
full-width
|
||||
onChange={(e) => setActiveTab((e.target as Tabs).value as ChatTabs)}>
|
||||
<mdui-tab value="chats">
|
||||
onChange={(e) => setActiveTab(e.target.value as ChatTabs)}>
|
||||
<MaterialTab value="chats">
|
||||
Чаты
|
||||
</mdui-tab>
|
||||
<mdui-tab value="channels">
|
||||
</MaterialTab>
|
||||
<MaterialTab value="channels">
|
||||
Каналы
|
||||
</mdui-tab>
|
||||
<mdui-tab value="contacts">
|
||||
</MaterialTab>
|
||||
<MaterialTab value="contacts">
|
||||
Контакты
|
||||
</mdui-tab>
|
||||
</MaterialTab>
|
||||
|
||||
<mdui-tab-panel slot="panel" value="chats">
|
||||
<MaterialTabPanel slot="panel" value="chats">
|
||||
<UnifiedChatsList />
|
||||
</mdui-tab-panel>
|
||||
<mdui-tab-panel slot="panel" value="channels">Скоро будет...</mdui-tab-panel>
|
||||
<mdui-tab-panel slot="panel" value="contacts">Скоро будет...</mdui-tab-panel>
|
||||
</mdui-tabs>
|
||||
</MaterialTabPanel>
|
||||
<MaterialTabPanel slot="panel" value="channels">Скоро будет...</MaterialTabPanel>
|
||||
<MaterialTabPanel slot="panel" value="contacts">Скоро будет...</MaterialTabPanel>
|
||||
</MaterialTabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,29 +4,25 @@ import { SettingsDialog } from "./settings/SettingsDialog";
|
||||
import { UsernameSearch } from "./UsernameSearch";
|
||||
import { ChatTabs } from "./ChatTabs";
|
||||
import { ChatHeader } from "./ChatHeader";
|
||||
import { MaterialBottomAppBar, MaterialFab, MaterialIconButton } from "@/utils/material";
|
||||
|
||||
function BottomAppBar() {
|
||||
const [settingsOpen, onSettingsOpenChange] = useState(false);
|
||||
const { logout } = useAppState();
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<mdui-bottom-app-bar>
|
||||
<mdui-button-icon icon="settings--filled" id="settings-open" onClick={() => onSettingsOpenChange(true)}></mdui-button-icon>
|
||||
<mdui-button-icon icon="group_add--filled"></mdui-button-icon>
|
||||
<MaterialBottomAppBar>
|
||||
<MaterialIconButton icon="settings--filled" id="settings-open" onClick={() => onSettingsOpenChange(true)} />
|
||||
<MaterialIconButton icon="group_add--filled" />
|
||||
<div style={{ flexGrow: 1 }}></div>
|
||||
<mdui-button-icon
|
||||
<MaterialIconButton
|
||||
icon="logout--filled"
|
||||
id="logout-btn"
|
||||
onClick={handleLogout}
|
||||
title="Выйти"
|
||||
></mdui-button-icon>
|
||||
<mdui-fab icon="edit--filled"></mdui-fab>
|
||||
</mdui-bottom-app-bar>
|
||||
onClick={logout}
|
||||
title="Выйти" />
|
||||
<MaterialFab icon="edit--filled" />
|
||||
</MaterialBottomAppBar>
|
||||
<SettingsDialog isOpen={settingsOpen} onOpenChange={onSettingsOpenChange} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@ import { websocket } from "@/core/websocket";
|
||||
import { onlineStatusManager } from "@/core/onlineStatusManager";
|
||||
import { OnlineIndicator } from "@/pages/chat/ui/right/OnlineIndicator";
|
||||
import defaultAvatar from "@/images/default-avatar.png";
|
||||
import { MaterialBadge, MaterialCircularProgress, MaterialList, MaterialListItem } from "@/utils/material";
|
||||
|
||||
interface PublicChat {
|
||||
id: string;
|
||||
@@ -223,17 +224,15 @@ export function UnifiedChatsList() {
|
||||
}
|
||||
|
||||
if (isLoadingUsers) {
|
||||
return (
|
||||
<mdui-circular-progress />
|
||||
);
|
||||
return <MaterialCircularProgress />;
|
||||
}
|
||||
|
||||
return (
|
||||
<mdui-list>
|
||||
<MaterialList>
|
||||
{allChats.map((chat) => {
|
||||
if (chat.type === "public") {
|
||||
return (
|
||||
<mdui-list-item
|
||||
<MaterialListItem
|
||||
key={`public-${chat.id}`}
|
||||
headline={chat.name}
|
||||
onClick={() => handlePublicChatClick(chat.name)}
|
||||
@@ -255,11 +254,11 @@ export function UnifiedChatsList() {
|
||||
objectFit: "cover"
|
||||
}}
|
||||
/>
|
||||
</mdui-list-item>
|
||||
</MaterialListItem>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<mdui-list-item
|
||||
<MaterialListItem
|
||||
key={`dm-${chat.id}`}
|
||||
headline={chat.display_name}
|
||||
onClick={() => handleDMClick(chat)}
|
||||
@@ -288,20 +287,20 @@ export function UnifiedChatsList() {
|
||||
display: "block"
|
||||
}}
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).src = defaultAvatar;
|
||||
e.target.src = defaultAvatar;
|
||||
}}
|
||||
/>
|
||||
<OnlineIndicator userId={chat.id} />
|
||||
</div>
|
||||
{chat.unreadCount > 0 && (
|
||||
<mdui-badge slot="end-icon">
|
||||
<MaterialBadge slot="end-icon">
|
||||
{chat.unreadCount}
|
||||
</mdui-badge>
|
||||
</MaterialBadge>
|
||||
)}
|
||||
</mdui-list-item>
|
||||
</MaterialListItem>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</mdui-list>
|
||||
</MaterialList>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { onlineStatusManager } from "@/core/onlineStatusManager";
|
||||
import { OnlineIndicator } from "@/pages/chat/ui/right/OnlineIndicator";
|
||||
import defaultAvatar from "@/images/default-avatar.png";
|
||||
import SearchBar from "@/core/components/SearchBar";
|
||||
import { MaterialCircularProgress, MaterialIconButton, MaterialList, MaterialListItem } from "@/utils/material";
|
||||
|
||||
interface SearchUser extends User {
|
||||
publicKey?: string | null;
|
||||
@@ -121,7 +122,7 @@ export function UsernameSearch() {
|
||||
isExpanded={isExpanded}
|
||||
onToggleExpanded={handleToggleExpanded}
|
||||
leftIcon={isExpanded ? (
|
||||
<mdui-button-icon
|
||||
<MaterialIconButton
|
||||
className="back-button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
@@ -134,7 +135,7 @@ export function UsernameSearch() {
|
||||
>
|
||||
{isSearching && (
|
||||
<div className="search-loading">
|
||||
<mdui-circular-progress value={0}></mdui-circular-progress>
|
||||
<MaterialCircularProgress />
|
||||
<span>Поиск...</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -146,9 +147,9 @@ export function UsernameSearch() {
|
||||
)}
|
||||
|
||||
{!isSearching && searchResults.length > 0 && (
|
||||
<mdui-list>
|
||||
<MaterialList>
|
||||
{searchResults.map((searchUser) => (
|
||||
<mdui-list-item
|
||||
<MaterialListItem
|
||||
key={searchUser.id}
|
||||
headline={searchUser.username}
|
||||
onClick={() => handleUserClick(searchUser)}
|
||||
@@ -174,14 +175,14 @@ export function UsernameSearch() {
|
||||
display: "block"
|
||||
}}
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).src = defaultAvatar;
|
||||
e.target.src = defaultAvatar;
|
||||
}}
|
||||
/>
|
||||
<OnlineIndicator userId={searchUser.id} />
|
||||
</div>
|
||||
</mdui-list-item>
|
||||
</MaterialListItem>
|
||||
))}
|
||||
</mdui-list>
|
||||
</MaterialList>
|
||||
)}
|
||||
|
||||
{!isSearching && searchQuery.length < 2 && (
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { MaterialButton, MaterialIconButton } from "@/utils/material";
|
||||
import "./css/cropper-dialog.scss";
|
||||
|
||||
export function CropperDialog() {
|
||||
@@ -6,14 +7,14 @@ export function CropperDialog() {
|
||||
<div className="cropper-dialog-content">
|
||||
<div className="cropper-header">
|
||||
<h3>Обрезать фото профиля</h3>
|
||||
<mdui-button-icon icon="close" id="cropper-close"></mdui-button-icon>
|
||||
<MaterialIconButton icon="close" id="cropper-close" />
|
||||
</div>
|
||||
<div className="cropper-container">
|
||||
<div id="cropper-area"></div>
|
||||
</div>
|
||||
<div className="cropper-actions">
|
||||
<mdui-button id="crop-cancel" variant="outlined">Отмена</mdui-button>
|
||||
<mdui-button id="crop-save">Сохранить</mdui-button>
|
||||
<MaterialButton id="crop-cancel" variant="outlined">Отмена</MaterialButton>
|
||||
<MaterialButton id="crop-save">Сохранить</MaterialButton>
|
||||
</div>
|
||||
</div>
|
||||
</mdui-dialog>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { Size2D, Rect } from "@/core/types";
|
||||
import { MaterialButton } from "@/utils/material";
|
||||
|
||||
interface ImageCropperProps {
|
||||
onCrop: (croppedImageData: string) => void;
|
||||
@@ -180,12 +181,12 @@ export function ImageCropper({ onCrop, onCancel, imageFile }: ImageCropperProps)
|
||||
alt="Crop source"
|
||||
/>
|
||||
<div className="cropper-actions">
|
||||
<mdui-button onClick={handleCrop} disabled={!isLoaded}>
|
||||
<MaterialButton onClick={handleCrop} disabled={!isLoaded}>
|
||||
Обрезать
|
||||
</mdui-button>
|
||||
<mdui-button variant="outlined" onClick={onCancel}>
|
||||
</MaterialButton>
|
||||
<MaterialButton variant="outlined" onClick={onCancel}>
|
||||
Отмена
|
||||
</mdui-button>
|
||||
</MaterialButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { useState } from "react";
|
||||
import { StyledDialog } from "@/core/components/StyledDialog";
|
||||
import type { DialogProps } from "@/core/types";
|
||||
import { useAppState } from "@/pages/chat/state";
|
||||
import { changePassword } from "@/core/api/securityApi";
|
||||
import { MaterialButton, MaterialIconButton, MaterialSwitch, MaterialTextField } from "@/utils/material";
|
||||
|
||||
export default function ChangePasswordDialog({ isOpen, onOpenChange }: DialogProps) {
|
||||
const { user } = useAppState();
|
||||
|
||||
const [current, setCurrent] = useState("");
|
||||
const [next, setNext] = useState("");
|
||||
const [confirm, setConfirm] = useState("");
|
||||
const [logoutAll, setLogoutAll] = useState(true);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
return (
|
||||
<StyledDialog open={isOpen} onOpenChange={onOpenChange} className="change-password-dialog">
|
||||
<div className="cpd-container">
|
||||
<div className="cpd-titlebar">
|
||||
<MaterialIconButton icon="close" onClick={() => onOpenChange(false)}></MaterialIconButton>
|
||||
<div className="cpd-title">Изменить пароль</div>
|
||||
</div>
|
||||
<div className="cpd-content">
|
||||
<form onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
if (!user.authToken || !user.currentUser?.username) return;
|
||||
if (!current || !next || next !== confirm) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
await changePassword(user.authToken, user.currentUser?.username, current, next, logoutAll);
|
||||
setCurrent("");
|
||||
setNext("");
|
||||
setConfirm("");
|
||||
onOpenChange(false);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}}>
|
||||
<MaterialTextField
|
||||
name="cpd-current-password"
|
||||
label="Текущий пароль"
|
||||
type="password"
|
||||
value={current}
|
||||
onInput={(e) => setCurrent(e.target.value)}
|
||||
variant="outlined"
|
||||
toggle-password
|
||||
required />
|
||||
<MaterialTextField
|
||||
name="cpd-new-password"
|
||||
label="Новый пароль"
|
||||
type="password"
|
||||
value={next}
|
||||
onInput={(e) => setNext(e.target.value)}
|
||||
variant="outlined"
|
||||
toggle-password
|
||||
required />
|
||||
<MaterialTextField
|
||||
name="cpd-confirm-password"
|
||||
label="Подтвердите пароль"
|
||||
type="password"
|
||||
value={confirm}
|
||||
onInput={(e) => setConfirm(e.target.value)}
|
||||
variant="outlined"
|
||||
toggle-password
|
||||
required />
|
||||
<div className="cpd-logout-all">
|
||||
<MaterialSwitch
|
||||
name="cpd-logout-all"
|
||||
checked={logoutAll}
|
||||
onInput={(e) => setLogoutAll(e.target.checked)} />
|
||||
<label htmlFor="cpd-logout-all">Выйти на всех устройствах (кроме текущего)</label>
|
||||
</div>
|
||||
<div className="cpd-actions">
|
||||
<MaterialButton type="submit" disabled={busy}>Сохранить</MaterialButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</StyledDialog>
|
||||
);
|
||||
}
|
||||
@@ -5,11 +5,11 @@ import { StyledDialog } from "@/core/components/StyledDialog";
|
||||
import { initialize, isSupported, startElectronReceiver, stopElectronReceiver, subscribe, unsubscribe } from "@/core/push-notifications/push-notifications";
|
||||
import { isElectron } from "@/core/electron/electron";
|
||||
import { useAppState } from "@/pages/chat/state";
|
||||
import type { Switch } from "mdui/components/switch";
|
||||
import { getAuthHeaders } from "@/core/api/authApi";
|
||||
import { changePassword } from "@/core/api/securityApi";
|
||||
import ChangePasswordDialog from "./ChangePasswordDialog";
|
||||
import { listDevices, revokeDevice, logoutAllOtherDevices, type DeviceInfo } from "@/core/api/devicesApi";
|
||||
import { useImmer } from "use-immer";
|
||||
import { MaterialButton, MaterialIconButton, MaterialList, MaterialListItem, MaterialSwitch } from "@/utils/material";
|
||||
|
||||
export function SettingsDialog({ isOpen, onOpenChange }: DialogProps) {
|
||||
const [activePanel, setActivePanel] = useState("notifications-settings");
|
||||
@@ -18,10 +18,7 @@ export function SettingsDialog({ isOpen, onOpenChange }: DialogProps) {
|
||||
const user = useAppState(state => state.user);
|
||||
const logout = useAppState(state => state.logout);
|
||||
const [devices, updateDevices] = useImmer<DeviceInfo[]>([]);
|
||||
const [cpCurrent, setCpCurrent] = useState("");
|
||||
const [cpNext, setCpNext] = useState("");
|
||||
const [cpConfirm, setCpConfirm] = useState("");
|
||||
const [cpLogoutAll, setCpLogoutAll] = useState(true);
|
||||
const [cpOpen, setCpOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setPushSupported(isSupported());
|
||||
@@ -79,131 +76,106 @@ export function SettingsDialog({ isOpen, onOpenChange }: DialogProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledDialog open={isOpen} onOpenChange={onOpenChange} className="settings-dialog">
|
||||
<div id="settings-dialog-inner">
|
||||
<div className="header">
|
||||
<mdui-button-icon icon="close" id="settings-close" onClick={() => onOpenChange(false)}></mdui-button-icon>
|
||||
<mdui-top-app-bar-title>Настройки</mdui-top-app-bar-title>
|
||||
</div>
|
||||
<div id="settings-menu">
|
||||
<mdui-list>
|
||||
<mdui-list-item
|
||||
icon="notifications--filled"
|
||||
rounded
|
||||
active={activePanel === "notifications-settings"}
|
||||
onClick={() => handlePanelChange("notifications-settings")}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
Уведомления
|
||||
</mdui-list-item>
|
||||
<mdui-list-item
|
||||
icon="security--filled"
|
||||
rounded
|
||||
active={activePanel === "security-settings"}
|
||||
onClick={() => handlePanelChange("security-settings")}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
Безопасность
|
||||
</mdui-list-item>
|
||||
<mdui-list-item
|
||||
icon="devices--filled"
|
||||
rounded
|
||||
active={activePanel === "devices-settings"}
|
||||
onClick={() => handlePanelChange("devices-settings")}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
Устройства
|
||||
</mdui-list-item>
|
||||
<mdui-list-item
|
||||
icon="info--filled"
|
||||
rounded
|
||||
active={activePanel === "about-settings"}
|
||||
onClick={() => handlePanelChange("about-settings")}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
О приложении
|
||||
</mdui-list-item>
|
||||
</mdui-list>
|
||||
<div className="screen">
|
||||
<div id="notifications-settings" className={`settings-panel ${activePanel === "notifications-settings" ? "active" : ""}`}>
|
||||
<h3>Уведомления</h3>
|
||||
{pushSupported && (
|
||||
<mdui-switch
|
||||
checked={pushNotificationsEnabled}
|
||||
onInput={(e) => handlePushNotificationToggle((e.target as Switch).checked)}
|
||||
>
|
||||
Push уведомления
|
||||
</mdui-switch>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
<div id="security-settings" className={`settings-panel ${activePanel === "security-settings" ? "active" : ""}`}>
|
||||
<h3>Безопасность</h3>
|
||||
<form onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
if (!user.authToken || !user.username) return;
|
||||
if (!cpCurrent || !cpNext || cpNext !== cpConfirm) return;
|
||||
try {
|
||||
await changePassword(user.authToken, user.username, cpCurrent, cpNext, cpLogoutAll);
|
||||
setCpCurrent("");
|
||||
setCpNext("");
|
||||
setCpConfirm("");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}}>
|
||||
<mdui-text-field label="Текущий пароль" type="password" value={cpCurrent} onInput={(e: any) => setCpCurrent(e.target.value)} variant="outlined" toggle-password></mdui-text-field>
|
||||
<mdui-text-field label="Новый пароль" type="password" value={cpNext} onInput={(e: any) => setCpNext(e.target.value)} variant="outlined" toggle-password></mdui-text-field>
|
||||
<mdui-text-field label="Подтвердите пароль" type="password" value={cpConfirm} onInput={(e: any) => setCpConfirm(e.target.value)} variant="outlined" toggle-password></mdui-text-field>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
||||
<mdui-switch checked={cpLogoutAll} onInput={(e: any) => setCpLogoutAll(e.target.checked)}>Выйти на всех устройствах (кроме текущего)</mdui-switch>
|
||||
<div style={{ flexGrow: 1 }}></div>
|
||||
<mdui-button type="submit" variant="tonal">Сохранить</mdui-button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="language-settings" className={`settings-panel ${activePanel === "language-settings" ? "active" : ""}`}>
|
||||
<h3>Язык</h3>
|
||||
<mdui-select label="Выберите язык" variant="outlined">
|
||||
<mdui-menu-item value="ru">Русский</mdui-menu-item>
|
||||
<mdui-menu-item value="en">English</mdui-menu-item>
|
||||
<mdui-menu-item value="es">Español</mdui-menu-item>
|
||||
</mdui-select>
|
||||
</div>
|
||||
|
||||
<div id="storage-settings" className={`settings-panel ${activePanel === "storage-settings" ? "active" : ""}`}>
|
||||
<h3>Хранилище</h3>
|
||||
<p>Использовано: 2.5 ГБ из 10 ГБ</p>
|
||||
<mdui-linear-progress value={25}></mdui-linear-progress>
|
||||
<mdui-button variant="outlined">Очистить кэш</mdui-button>
|
||||
</div>
|
||||
|
||||
<div id="devices-settings" className={`settings-panel ${activePanel === "devices-settings" ? "active" : ""}`}>
|
||||
<h3>Устройства</h3>
|
||||
<div style={{ display: "flex", gap: 12, marginBottom: 12 }}>
|
||||
<mdui-button variant="tonal" onClick={async () => { if (!user.authToken) return; await logoutAllOtherDevices(user.authToken); const list = await listDevices(user.authToken); updateDevices(() => list); }}>Выйти на всех остальных устройствах</mdui-button>
|
||||
<mdui-button variant="outlined" onClick={async () => { if (!user.authToken) return; await fetch(`${API_BASE_URL}/logout`, { headers: getAuthHeaders(user.authToken) }); logout(); }}>Выйти на этом устройстве</mdui-button>
|
||||
<>
|
||||
<StyledDialog open={isOpen} onOpenChange={onOpenChange} className="settings-dialog">
|
||||
<div id="settings-dialog-inner">
|
||||
<div className="header">
|
||||
<MaterialIconButton icon="close" id="settings-close" onClick={() => onOpenChange(false)}></MaterialIconButton>
|
||||
<div className="title">Настройки</div>
|
||||
</div>
|
||||
<div id="settings-menu">
|
||||
<MaterialList>
|
||||
<MaterialListItem
|
||||
icon="notifications--filled"
|
||||
rounded
|
||||
active={activePanel === "notifications-settings"}
|
||||
onClick={() => handlePanelChange("notifications-settings")}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
Уведомления
|
||||
</MaterialListItem>
|
||||
<MaterialListItem
|
||||
icon="security--filled"
|
||||
rounded
|
||||
active={activePanel === "security-settings"}
|
||||
onClick={() => handlePanelChange("security-settings")}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
Безопасность
|
||||
</MaterialListItem>
|
||||
<MaterialListItem
|
||||
icon="devices--filled"
|
||||
rounded
|
||||
active={activePanel === "devices-settings"}
|
||||
onClick={() => handlePanelChange("devices-settings")}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
Устройства
|
||||
</MaterialListItem>
|
||||
<MaterialListItem
|
||||
icon="info--filled"
|
||||
rounded
|
||||
active={activePanel === "about-settings"}
|
||||
onClick={() => handlePanelChange("about-settings")}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
О приложении
|
||||
</MaterialListItem>
|
||||
</MaterialList>
|
||||
<div className="screen">
|
||||
<div id="notifications-settings" className={`settings-panel ${activePanel === "notifications-settings" ? "active" : ""}`}>
|
||||
<h3>Уведомления</h3>
|
||||
{pushSupported && (
|
||||
<MaterialSwitch
|
||||
checked={pushNotificationsEnabled}
|
||||
onInput={(e) => handlePushNotificationToggle(e.target.checked)}>
|
||||
Push уведомления
|
||||
</MaterialSwitch>
|
||||
)}
|
||||
</div>
|
||||
<mdui-list>
|
||||
{devices.map((d) => (
|
||||
<mdui-list-item key={d.session_id} icon={d.current ? "devices_other--filled" : "devices--filled"} rounded end-icon={!d.current ? "logout--filled" : undefined} onEndIconClick={async () => { if (!user.authToken || d.current) return; await revokeDevice(user.authToken, d.session_id); const list = await listDevices(user.authToken); updateDevices(() => list); }}>
|
||||
<div slot="headline">{d.browser_name || "Браузер"} на {d.os_name || "OS"} {d.current ? " (это устройство)" : ""}</div>
|
||||
<div slot="description">Последняя активность: {d.last_seen || "—"}</div>
|
||||
</mdui-list-item>
|
||||
))}
|
||||
</mdui-list>
|
||||
</div>
|
||||
|
||||
<div id="about-settings" className={`settings-panel ${activePanel === "about-settings" ? "active" : ""}`}>
|
||||
<h3>О приложении</h3>
|
||||
<p>100% open source. Репозиторий на <a href="https://github.com/Toolbox-io/FromChat" target="_blank" rel="noreferrer">GitHub</a>.</p>
|
||||
<p><span className="product-name">{PRODUCT_NAME}</span></p>
|
||||
<div id="security-settings" className={`settings-panel ${activePanel === "security-settings" ? "active" : ""}`}>
|
||||
<h3>Безопасность</h3>
|
||||
<MaterialButton variant="tonal" onClick={() => setCpOpen(true)}>Изменить пароль</MaterialButton>
|
||||
</div>
|
||||
|
||||
<div id="devices-settings" className={`settings-panel ${activePanel === "devices-settings" ? "active" : ""}`}>
|
||||
<h3>Устройства</h3>
|
||||
<div style={{ display: "flex", gap: 12, marginBottom: 12 }}>
|
||||
<MaterialButton variant="tonal" onClick={async () => { if (!user.authToken) return; await logoutAllOtherDevices(user.authToken); const list = await listDevices(user.authToken); updateDevices(() => list); }}>Выйти на всех остальных устройствах</MaterialButton>
|
||||
<MaterialButton variant="outlined" onClick={async () => { if (!user.authToken) return; await fetch(`${API_BASE_URL}/logout`, { headers: getAuthHeaders(user.authToken) }); logout(); }}>Выйти на этом устройстве</MaterialButton>
|
||||
</div>
|
||||
<MaterialList>
|
||||
{devices.map((d) => (
|
||||
<MaterialListItem
|
||||
key={d.session_id}
|
||||
icon={d.current ? "devices_other--filled" : "devices--filled"}
|
||||
rounded
|
||||
end-icon={!d.current ? "logout--filled" : undefined}
|
||||
onClick={async () => {
|
||||
if (!user.authToken || d.current) return;
|
||||
await revokeDevice(user.authToken, d.session_id);
|
||||
const list = await listDevices(user.authToken);
|
||||
updateDevices(() => list);
|
||||
}}
|
||||
>
|
||||
<div slot="headline">{d.device_name || (d.browser_name || "Браузер")} на {d.os_name || "OS"} {d.current ? " (это устройство)" : ""}</div>
|
||||
<div slot="description">Последняя активность: {d.last_seen || "—"}</div>
|
||||
</MaterialListItem>
|
||||
))}
|
||||
</MaterialList>
|
||||
</div>
|
||||
|
||||
<div id="about-settings" className={`settings-panel ${activePanel === "about-settings" ? "active" : ""}`}>
|
||||
<h3>О приложении</h3>
|
||||
<p>100% open source. Репозиторий на <a href="https://github.com/Toolbox-io/FromChat" target="_blank" rel="noreferrer">GitHub</a>.</p>
|
||||
<p><span className="product-name">{PRODUCT_NAME}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</StyledDialog>
|
||||
</StyledDialog>
|
||||
<ChangePasswordDialog isOpen={cpOpen} onOpenChange={setCpOpen} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user