Билд
This commit is contained in:
@@ -47,7 +47,7 @@ interface StoriesConfig {
|
||||
interface ChatsConfig {
|
||||
supportGroups: boolean;
|
||||
maxGroupParticipants: number;
|
||||
autoCleanChats: boolean;
|
||||
enableAutoClean: boolean;
|
||||
allowChatToGroupConversion: boolean;
|
||||
enableFolders: boolean;
|
||||
}
|
||||
@@ -73,7 +73,6 @@ interface MessagesConfig {
|
||||
|
||||
interface WebRtcConfig {
|
||||
enabled: boolean;
|
||||
enableVoiceCalls: boolean;
|
||||
enableVideoCalls: boolean;
|
||||
enableScreenSharing: boolean;
|
||||
turnHost: string;
|
||||
@@ -82,6 +81,14 @@ interface WebRtcConfig {
|
||||
turnSecret: string;
|
||||
}
|
||||
|
||||
interface TimezoneDto {
|
||||
id: string;
|
||||
displayName: string;
|
||||
standardName: string;
|
||||
offsetMinutes: number;
|
||||
offsetString: string;
|
||||
}
|
||||
|
||||
interface KlipyConfig {
|
||||
enabled: boolean;
|
||||
apiKey: string;
|
||||
@@ -124,6 +131,8 @@ interface AppUser {
|
||||
lastOnlineAt: string;
|
||||
isOnline?: boolean;
|
||||
isBanned?: boolean;
|
||||
messageCount?: number;
|
||||
storageSize?: number;
|
||||
stats?: {
|
||||
messagesCount: number;
|
||||
mediaCount: number;
|
||||
@@ -178,7 +187,9 @@ const translations = {
|
||||
mediaSent: 'Media',
|
||||
filesSent: 'Files',
|
||||
linksSent: 'Links',
|
||||
userStorageOccupied: 'Storage',
|
||||
userStorageOccupied: 'Attachments',
|
||||
sent: 'Messages',
|
||||
storage: 'Attachments',
|
||||
displayName: 'Display Name',
|
||||
cancel: 'Cancel',
|
||||
createUser: 'Create User',
|
||||
@@ -265,7 +276,7 @@ const translations = {
|
||||
maxMediaSize: 'Max file size for story media.',
|
||||
supportGroups: 'Enable group chat functionality.',
|
||||
maxGroupMembers: 'Max participants per group.',
|
||||
autoClean: 'Automatically remove old messages.',
|
||||
autoClean: 'Enable/disable auto-cleanup feature for users. Users manage their own chat timers.',
|
||||
chatToGroup: 'Allow upgrading 1-on-1 chats to groups.',
|
||||
enableFolders: 'Allow users to use chat folders.',
|
||||
dailyLimit: 'Max messages per user day (0 = unlimited).',
|
||||
@@ -273,7 +284,7 @@ const translations = {
|
||||
maxFileSize: 'Max size for message attachments.',
|
||||
noCopy: 'Prevent text copying in clients.',
|
||||
links: 'Make URLs clickable.',
|
||||
webRtc: 'Required for real-time calls.',
|
||||
webRtc: 'Enable real-time audio and video calls. This is a master switch for the WebRTC module.',
|
||||
turn: 'Required for calls behind NAT.',
|
||||
klipy: 'Integration for stickers and GIFs.',
|
||||
federation: 'Communication between different Knot instances.',
|
||||
@@ -343,7 +354,9 @@ const translations = {
|
||||
mediaSent: 'Медиа',
|
||||
filesSent: 'Файлы',
|
||||
linksSent: 'Ссылки',
|
||||
userStorageOccupied: 'Хранилище',
|
||||
userStorageOccupied: 'Вложения',
|
||||
sent: 'Сообщения',
|
||||
storage: 'Вложения',
|
||||
displayName: 'Имя',
|
||||
cancel: 'Отмена',
|
||||
createUser: 'Создать',
|
||||
@@ -430,7 +443,7 @@ const translations = {
|
||||
maxMediaSize: 'Лимит одного файла в историях.',
|
||||
supportGroups: 'Включить группы.',
|
||||
maxGroupMembers: 'Максимум людей в одной группе.',
|
||||
autoClean: 'Удаление старых сообщений.',
|
||||
autoClean: 'Разрешить пользователям использовать функцию автоочистки. Самим процессом (таймерами) управляют пользователи в своих чатах.',
|
||||
chatToGroup: 'Разрешить создавать группы из чатов.',
|
||||
enableFolders: 'Разрешить папки чатов.',
|
||||
dailyLimit: 'Лимит сообщений в сутки (0 = без лимита).',
|
||||
@@ -438,7 +451,7 @@ const translations = {
|
||||
maxFileSize: 'Лимит файлов в сообщениях.',
|
||||
noCopy: 'Мешать копированию текста.',
|
||||
links: 'Автоматические ссылки.',
|
||||
webRtc: 'Нужно для звонков.',
|
||||
webRtc: 'Включить возможность аудио и видео звонков. Глобальный переключатель для модуля WebRTC.',
|
||||
turn: 'Нужно для звонков за NAT.',
|
||||
klipy: 'Стикеры и GIF.',
|
||||
federation: 'Связь с другими серверами Knot.',
|
||||
@@ -487,9 +500,11 @@ export default function AdminPage() {
|
||||
|
||||
const formatBytes = (bytes: number) => {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024, sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const k = 1024;
|
||||
const dm = 2;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
const bytesToMb = (bytes: number) => Math.floor(bytes / (1024 * 1024));
|
||||
@@ -522,6 +537,10 @@ export default function AdminPage() {
|
||||
const [newUser, setNewUser] = useState({ username: '', displayName: '', password: '' });
|
||||
const [generatedPass, setGeneratedPass] = useState('');
|
||||
|
||||
const [timezones, setTimezones] = useState<TimezoneDto[]>([]);
|
||||
const [tzSearch, setTzSearch] = useState('');
|
||||
const [showTzDropdown, setShowTzDropdown] = useState(false);
|
||||
|
||||
const [toast, setToast] = useState<{message: string, type: 'success' | 'error'} | null>(null);
|
||||
|
||||
const showToast = (message: string, type: 'success' | 'error' = 'success') => {
|
||||
@@ -616,6 +635,13 @@ export default function AdminPage() {
|
||||
} catch {}
|
||||
};
|
||||
|
||||
const fetchTimezones = async () => {
|
||||
try {
|
||||
const res = await httpClient.request<TimezoneDto[]>('/admin/timezones');
|
||||
setTimezones(res);
|
||||
} catch {}
|
||||
};
|
||||
|
||||
const saveSettings = async () => {
|
||||
if (!config) return;
|
||||
try {
|
||||
@@ -786,6 +812,7 @@ export default function AdminPage() {
|
||||
setAuthenticated(true);
|
||||
fetchDashboard();
|
||||
fetchSettings();
|
||||
fetchTimezones();
|
||||
searchUsers('');
|
||||
} catch {
|
||||
showToast(t.errorInvalidLogin, 'error');
|
||||
@@ -931,11 +958,11 @@ export default function AdminPage() {
|
||||
<button onClick={handleCalcCleanup} className="bg-accent/10 hover:bg-accent/20 text-accent px-4 py-2 rounded-xl transition-colors">{t.analyzeJunk}</button>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="bg-black/20 p-4 rounded-xl">
|
||||
<div className="bg-black/20 p-4 rounded-xl flex flex-col justify-between min-h-[100px]">
|
||||
<div className="text-xs text-gray-500">{t.orphanedMessages}</div>
|
||||
<div className="text-xl font-bold">{cleanStats.orphanedMessagesCount}</div>
|
||||
</div>
|
||||
<div className="bg-black/20 p-4 rounded-xl">
|
||||
<div className="bg-black/20 p-4 rounded-xl flex flex-col justify-between min-h-[100px]">
|
||||
<div className="text-xs text-gray-500">{t.orphanedMedia}</div>
|
||||
<div className="text-xl font-bold">{formatBytes(cleanStats.orphanedMediaBytes)}</div>
|
||||
</div>
|
||||
@@ -987,11 +1014,56 @@ export default function AdminPage() {
|
||||
</div>
|
||||
<Hint>{t.hints.enableRegistration}</Hint>
|
||||
</div>
|
||||
<label className="flex flex-col gap-1.5 group">
|
||||
<div className="flex flex-col gap-1.5 group relative">
|
||||
<span className="text-xs font-bold text-gray-500 uppercase ml-1 group-focus-within:text-accent transition-colors">{t.timezone}</span>
|
||||
<input className="bg-black/40 border border-white/10 rounded-xl px-4 py-3.5 outline-none text-white focus:border-accent/50 focus:bg-black/60 transition-all font-mono text-sm" value={config.system.serverTimezone} onChange={e => setConfig({...config, system: {...config.system, serverTimezone: e.target.value}})} placeholder="UTC" />
|
||||
<div className="relative">
|
||||
<input
|
||||
className="w-full bg-black/40 border border-white/10 rounded-xl px-4 py-3.5 outline-none text-white focus:border-accent/50 focus:bg-black/60 transition-all font-mono text-xs pr-10"
|
||||
value={config.system.serverTimezone}
|
||||
onFocus={() => setShowTzDropdown(true)}
|
||||
onChange={e => {
|
||||
setConfig({...config, system: {...config.system, serverTimezone: e.target.value}});
|
||||
setTzSearch(e.target.value);
|
||||
}}
|
||||
placeholder="UTC"
|
||||
/>
|
||||
<Globe className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
|
||||
|
||||
<AnimatePresence>
|
||||
{showTzDropdown && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
className="absolute left-0 right-0 top-full mt-2 bg-surface border border-white/10 rounded-xl shadow-2xl z-[100] max-h-60 overflow-y-auto overflow-x-hidden p-1"
|
||||
>
|
||||
{timezones
|
||||
.filter(tz => tz.displayName.toLowerCase().includes(tzSearch.toLowerCase()) || tz.id.toLowerCase().includes(tzSearch.toLowerCase()))
|
||||
.map(tz => (
|
||||
<button
|
||||
key={tz.id}
|
||||
onClick={() => {
|
||||
setConfig({...config, system: {...config.system, serverTimezone: tz.id}});
|
||||
setTzSearch('');
|
||||
setShowTzDropdown(false);
|
||||
}}
|
||||
className="w-full text-left p-3 hover:bg-white/5 rounded-lg transition-colors flex flex-col group/tz"
|
||||
>
|
||||
<div className="flex justify-between items-center w-full">
|
||||
<span className="text-xs font-bold text-white group-hover/tz:text-accent truncate">{tz.displayName}</span>
|
||||
<span className="text-[10px] font-mono text-gray-500 group-hover/tz:text-accent/50 ml-2 shrink-0">{tz.offsetString}</span>
|
||||
</div>
|
||||
<span className="text-[10px] text-gray-600 truncate">{tz.id}</span>
|
||||
</button>
|
||||
))
|
||||
}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
{showTzDropdown && <div className="fixed inset-0 z-[90]" onClick={() => setShowTzDropdown(false)} />}
|
||||
<Hint>{t.hints.timezone}</Hint>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -1060,7 +1132,7 @@ export default function AdminPage() {
|
||||
<div className="flex flex-col gap-2 bg-white/[0.02] p-4 rounded-2xl border border-white/5">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-200 font-semibold">{t.autoClean}</span>
|
||||
<Toggle checked={config.chats.autoCleanChats} onChange={v => setConfig({...config, chats: {...config.chats, autoCleanChats: v}})} />
|
||||
<Toggle checked={config.chats.enableAutoClean} onChange={v => setConfig({...config, chats: {...config.chats, enableAutoClean: v}})} />
|
||||
</div>
|
||||
<Hint>{t.hints.autoClean}</Hint>
|
||||
</div>
|
||||
@@ -1144,16 +1216,8 @@ export default function AdminPage() {
|
||||
</div>
|
||||
<Hint>{t.hints.webRtc}</Hint>
|
||||
|
||||
{config.webRtc.enabled && (
|
||||
{config.webRtc.enabled && (<>
|
||||
<div className="flex flex-col gap-10 pt-4 border-t border-white/10">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div className="bg-white/[0.03] p-4 rounded-2xl border border-white/5 flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-bold text-gray-400 uppercase">{t.voiceCalls}</span>
|
||||
<Toggle checked={config.webRtc.enableVoiceCalls} onChange={v => setConfig({...config, webRtc: {...config.webRtc, enableVoiceCalls: v}})} />
|
||||
</div>
|
||||
<Hint>{t.hints.voiceCalls}</Hint>
|
||||
</div>
|
||||
<div className="bg-white/[0.03] p-4 rounded-2xl border border-white/5 flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-bold text-gray-400 uppercase">{t.videoCalls}</span>
|
||||
@@ -1204,8 +1268,7 @@ export default function AdminPage() {
|
||||
{t.testConnection}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1344,9 +1407,20 @@ export default function AdminPage() {
|
||||
{u.isOnline ? (
|
||||
<span className="text-[10px] bg-green-500/20 text-green-400 px-2 py-0.5 rounded-full font-bold">{t.online.toUpperCase()}</span>
|
||||
) : (
|
||||
<span className="text-xs text-gray-500">{t.offline}</span>
|
||||
<span className="text-xs text-gray-400">{t.offline}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="ml-4 flex items-center gap-4 shrink-0 text-[10px]">
|
||||
<div className="flex flex-col items-end">
|
||||
<span className="text-gray-500 uppercase font-black tracking-tighter opacity-50">{t.sent}</span>
|
||||
<span className="text-accent font-bold">{u.messageCount || 0}</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-end border-l border-white/5 pl-4">
|
||||
<span className="text-gray-500 uppercase font-black tracking-tighter opacity-50">{t.storage}</span>
|
||||
<span className="text-white font-bold">{formatBytes(u.storageSize || 0)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user