v0.10.9 update ui settings
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jama-frontend",
|
||||
"version": "0.10.8",
|
||||
"version": "0.10.9",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -353,6 +353,7 @@ export default function Chat() {
|
||||
/>
|
||||
{modal === 'profile' && <ProfileModal onClose={() => setModal(null)} />}
|
||||
{modal === 'settings' && <SettingsModal onClose={() => setModal(null)} onFeaturesChanged={setFeatures} />}
|
||||
{modal === 'branding' && <BrandingModal onClose={() => setModal(null)} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -377,6 +378,7 @@ export default function Chat() {
|
||||
/>
|
||||
{modal === 'profile' && <ProfileModal onClose={() => setModal(null)} />}
|
||||
{modal === 'settings' && <SettingsModal onClose={() => setModal(null)} onFeaturesChanged={setFeatures} />}
|
||||
{modal === 'branding' && <BrandingModal onClose={() => setModal(null)} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import UserFooter from '../components/UserFooter.jsx';
|
||||
|
||||
// ── useKeyboardOpen — true when software keyboard is visible ─────────────────
|
||||
function useKeyboardOpen() {
|
||||
const [open, setOpen] = useState(false);
|
||||
useEffect(() => {
|
||||
const vv = window.visualViewport;
|
||||
if (!vv) return;
|
||||
const handler = () => setOpen(vv.height < window.innerHeight * 0.75);
|
||||
vv.addEventListener('resize', handler);
|
||||
return () => vv.removeEventListener('resize', handler);
|
||||
}, []);
|
||||
return open;
|
||||
}
|
||||
import { api } from '../utils/api.js';
|
||||
import { useToast } from '../contexts/ToastContext.jsx';
|
||||
import Avatar from '../components/Avatar.jsx';
|
||||
@@ -502,6 +515,7 @@ export default function GroupManagerPage({ isMobile = false, onProfile, onHelp,
|
||||
const [allUsers, setAllUsers] = useState([]);
|
||||
const [allUserGroups, setAllUserGroups] = useState([]);
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
const keyboardOpen = useKeyboardOpen();
|
||||
const onRefresh = () => setRefreshKey(k => k+1);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -509,12 +523,36 @@ export default function GroupManagerPage({ isMobile = false, onProfile, onHelp,
|
||||
api.getUserGroups().then(({ groups }) => setAllUserGroups(groups)).catch(() => {});
|
||||
}, [refreshKey]);
|
||||
|
||||
// Nav item helper — matches Schedule page style
|
||||
const navItem = (label, key) => (
|
||||
<button key={key} onClick={() => setTab(key)}
|
||||
style={{ display:'block', width:'100%', textAlign:'left', padding:'8px 10px',
|
||||
borderRadius:'var(--radius)', border:'none',
|
||||
background: tab===key ? 'var(--primary-light)' : 'transparent',
|
||||
color: tab===key ? 'var(--primary)' : 'var(--text-primary)',
|
||||
cursor:'pointer', fontWeight: tab===key ? 600 : 400, fontSize:14, marginBottom:2 }}>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ display:'flex', flex:1, overflow:'hidden', minHeight:0 }}>
|
||||
|
||||
{/* ── Left panel (desktop only) ── */}
|
||||
{!isMobile && (
|
||||
<div style={{ width:SIDEBAR_W, flexShrink:0, borderRight:'1px solid var(--border)', display:'flex', flexDirection:'column', background:'var(--surface)', overflow:'hidden' }}>
|
||||
<div style={{ padding:'16px 16px 0' }}>
|
||||
{/* Title — matches Schedule page */}
|
||||
<div style={{ display:'flex', alignItems:'center', gap:8, marginBottom:16 }}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--primary)" strokeWidth="2"><rect x="2" y="7" width="20" height="14" rx="2"/><path d="M16 7V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2"/></svg>
|
||||
<span style={{ fontSize:16, fontWeight:700, color:'var(--text-primary)' }}>Group Manager</span>
|
||||
</div>
|
||||
{/* Tab navigation */}
|
||||
<div className="section-label" style={{ marginBottom:6 }}>View</div>
|
||||
{navItem('User Groups', 'all')}
|
||||
{navItem('Multi-Group DMs', 'dm')}
|
||||
{navItem('U2U Restrictions', 'u2u')}
|
||||
</div>
|
||||
<div style={{ flex:1 }} />
|
||||
<UserFooter onProfile={onProfile} onHelp={onHelp} onAbout={onAbout} />
|
||||
</div>
|
||||
@@ -523,20 +561,15 @@ export default function GroupManagerPage({ isMobile = false, onProfile, onHelp,
|
||||
{/* ── Right panel ── */}
|
||||
<div style={{ flex:1, display:'flex', flexDirection:'column', overflow:'hidden', minWidth:0, background:'var(--background)' }}>
|
||||
|
||||
{/* Header */}
|
||||
<div style={{ background:'var(--surface)', borderBottom:'1px solid var(--border)', padding:'0 16px', flexShrink:0 }}>
|
||||
<div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', height:52, gap:8 }}>
|
||||
<div style={{ display:'flex', alignItems:'center', gap:8 }}>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--primary)" strokeWidth="2"><rect x="2" y="7" width="20" height="14" rx="2"/><path d="M16 7V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2"/></svg>
|
||||
<span style={{ fontWeight:700, fontSize:15 }}>Group Manager</span>
|
||||
</div>
|
||||
<div style={{ display:'flex', gap:6, flexShrink:0 }}>
|
||||
<button className={`btn btn-sm ${tab==='all'?'btn-primary':'btn-secondary'}`} onClick={() => setTab('all')}>{isMobile ? 'Groups' : 'User Groups'}</button>
|
||||
<button className={`btn btn-sm ${tab==='dm'?'btn-primary':'btn-secondary'}`} onClick={() => setTab('dm')}>{isMobile ? 'Multi-DMs' : 'Multi-Group DMs'}</button>
|
||||
<button className={`btn btn-sm ${tab==='u2u'?'btn-primary':'btn-secondary'}`} onClick={() => setTab('u2u')}>U2U</button>
|
||||
</div>
|
||||
{/* Mobile tab bar — only shown on mobile */}
|
||||
{isMobile && (
|
||||
<div style={{ background:'var(--surface)', borderBottom:'1px solid var(--border)', padding:'0 12px', display:'flex', gap:6, height:48, alignItems:'center', flexShrink:0 }}>
|
||||
<span style={{ fontWeight:700, fontSize:14, marginRight:4, color:'var(--text-primary)' }}>Groups</span>
|
||||
<button className={`btn btn-sm ${tab==='all'?'btn-primary':'btn-secondary'}`} onClick={() => setTab('all')}>Groups</button>
|
||||
<button className={`btn btn-sm ${tab==='dm'?'btn-primary':'btn-secondary'}`} onClick={() => setTab('dm')}>Multi-DMs</button>
|
||||
<button className={`btn btn-sm ${tab==='u2u'?'btn-primary':'btn-secondary'}`} onClick={() => setTab('u2u')}>U2U</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<div style={{ flex:1, display:'flex', overflow: isMobile ? 'auto' : 'hidden', paddingBottom: isMobile ? 70 : 0 }}>
|
||||
@@ -545,8 +578,8 @@ export default function GroupManagerPage({ isMobile = false, onProfile, onHelp,
|
||||
{tab==='u2u' && <U2URestrictionsTab allUserGroups={allUserGroups} isMobile={isMobile} />}
|
||||
</div>
|
||||
|
||||
{/* Mobile footer — fixed at bottom, always visible */}
|
||||
{isMobile && (
|
||||
{/* Mobile footer — hidden when keyboard is open */}
|
||||
{isMobile && !keyboardOpen && (
|
||||
<div style={{ position:'fixed', bottom:0, left:0, right:0, zIndex:20, background:'var(--surface)', borderTop:'1px solid var(--border)' }}>
|
||||
<UserFooter onProfile={onProfile} onHelp={onHelp} onAbout={onAbout} />
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { useToast } from '../contexts/ToastContext.jsx';
|
||||
import { api } from '../utils/api.js';
|
||||
import Avatar from '../components/Avatar.jsx';
|
||||
@@ -24,7 +24,6 @@ function parseCSV(text) {
|
||||
return { rows, invalid };
|
||||
}
|
||||
|
||||
// ── User row (accordion) ──────────────────────────────────────────────────────
|
||||
function UserRow({ u, onUpdated }) {
|
||||
const toast = useToast();
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -160,27 +159,20 @@ function UserRow({ u, onUpdated }) {
|
||||
);
|
||||
}
|
||||
|
||||
// ── Create user form ──────────────────────────────────────────────────────────
|
||||
function CreateUserForm({ userPass, onCreated, isMobile }) {
|
||||
const toast = useToast();
|
||||
const [form, setForm] = useState({ name:'', email:'', password:'', role:'member' });
|
||||
const [saving, setSaving] = useState(false);
|
||||
const set = k => v => setForm(f => ({ ...f, [k]: v }));
|
||||
|
||||
const handle = async () => {
|
||||
if (!form.name.trim() || !form.email.trim()) return toast('Name and email are required', 'error');
|
||||
if (!isValidEmail(form.email)) return toast('Invalid email address', 'error');
|
||||
if (!/\S+\s+\S+/.test(form.name.trim())) return toast('Name must be two words (First Last)', 'error');
|
||||
setSaving(true);
|
||||
try {
|
||||
await api.createUser(form);
|
||||
toast('User created', 'success');
|
||||
setForm({ name:'', email:'', password:'', role:'member' });
|
||||
onCreated();
|
||||
} catch(e) { toast(e.message, 'error'); }
|
||||
try { await api.createUser(form); toast('User created', 'success'); setForm({ name:'', email:'', password:'', role:'member' }); onCreated(); }
|
||||
catch(e) { toast(e.message, 'error'); }
|
||||
finally { setSaving(false); }
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth:560 }}>
|
||||
<div style={{ display:'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap:12, marginBottom:12 }}>
|
||||
@@ -210,7 +202,6 @@ function CreateUserForm({ userPass, onCreated, isMobile }) {
|
||||
);
|
||||
}
|
||||
|
||||
// ── Bulk import form ──────────────────────────────────────────────────────────
|
||||
function BulkImportForm({ userPass, onCreated }) {
|
||||
const toast = useToast();
|
||||
const fileRef = useRef(null);
|
||||
@@ -219,7 +210,6 @@ function BulkImportForm({ userPass, onCreated }) {
|
||||
const [csvInvalid, setCsvInvalid] = useState([]);
|
||||
const [bulkResult, setBulkResult] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleFile = e => {
|
||||
const file = e.target.files?.[0]; if (!file) return;
|
||||
setCsvFile(file); setBulkResult(null);
|
||||
@@ -227,7 +217,6 @@ function BulkImportForm({ userPass, onCreated }) {
|
||||
reader.onload = ev => { const { rows, invalid } = parseCSV(ev.target.result); setCsvRows(rows); setCsvInvalid(invalid); };
|
||||
reader.readAsText(file);
|
||||
};
|
||||
|
||||
const handleImport = async () => {
|
||||
if (!csvRows.length) return;
|
||||
setLoading(true);
|
||||
@@ -239,20 +228,16 @@ function BulkImportForm({ userPass, onCreated }) {
|
||||
} catch(e) { toast(e.message, 'error'); }
|
||||
finally { setLoading(false); }
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth:560 }} className="flex-col gap-4">
|
||||
<div className="card" style={{ background:'var(--background)', border:'1px dashed var(--border)' }}>
|
||||
<p className="text-sm font-medium" style={{ marginBottom:6 }}>CSV Format</p>
|
||||
<code style={{ fontSize:12, color:'var(--text-secondary)', display:'block', background:'var(--surface)', padding:8, borderRadius:4, border:'1px solid var(--border)', whiteSpace:'pre' }}>{"name,email,password,role\nJane Smith,jane@company.com,,member\nBob Jones,bob@company.com,TempPass1,admin"}</code>
|
||||
<p className="text-xs" style={{ color:'var(--text-tertiary)', marginTop:8 }}>
|
||||
Name and email required. Blank password defaults to <strong>{userPass}</strong>, blank role defaults to member.
|
||||
</p>
|
||||
<p className="text-xs" style={{ color:'var(--text-tertiary)', marginTop:8 }}>Name and email required. Blank password defaults to <strong>{userPass}</strong>, blank role defaults to member.</p>
|
||||
</div>
|
||||
<div style={{ display:'flex', alignItems:'center', gap:10, flexWrap:'wrap' }}>
|
||||
<label className="btn btn-secondary" style={{ cursor:'pointer', margin:0 }}>
|
||||
Select CSV File
|
||||
<input ref={fileRef} type="file" accept=".csv,.txt" style={{ display:'none' }} onChange={handleFile} />
|
||||
Select CSV File<input ref={fileRef} type="file" accept=".csv,.txt" style={{ display:'none' }} onChange={handleFile} />
|
||||
</label>
|
||||
{csvFile && <span className="text-sm" style={{ color:'var(--text-secondary)' }}>{csvFile.name}{csvRows.length > 0 && <span style={{ color:'var(--text-tertiary)', marginLeft:6 }}>({csvRows.length} valid)</span>}</span>}
|
||||
{csvRows.length > 0 && <button className="btn btn-primary" onClick={handleImport} disabled={loading}>{loading ? 'Creating…' : `Create ${csvRows.length} User${csvRows.length!==1?'s':''}`}</button>}
|
||||
@@ -287,6 +272,19 @@ function BulkImportForm({ userPass, onCreated }) {
|
||||
);
|
||||
}
|
||||
|
||||
// ── useKeyboardOpen — true when software keyboard is visible ─────────────────
|
||||
function useKeyboardOpen() {
|
||||
const [open, setOpen] = useState(false);
|
||||
useEffect(() => {
|
||||
const vv = window.visualViewport;
|
||||
if (!vv) return;
|
||||
const handler = () => setOpen(vv.height < window.innerHeight * 0.75);
|
||||
vv.addEventListener('resize', handler);
|
||||
return () => vv.removeEventListener('resize', handler);
|
||||
}, []);
|
||||
return open;
|
||||
}
|
||||
|
||||
// ── Main page ─────────────────────────────────────────────────────────────────
|
||||
export default function UserManagerPage({ isMobile = false, onProfile, onHelp, onAbout }) {
|
||||
const [users, setUsers] = useState([]);
|
||||
@@ -295,21 +293,19 @@ export default function UserManagerPage({ isMobile = false, onProfile, onHelp, o
|
||||
const [search, setSearch] = useState('');
|
||||
const [tab, setTab] = useState('users');
|
||||
const [userPass, setUserPass] = useState('user@1234');
|
||||
const [inputFocused, setInputFocused] = useState(false);
|
||||
const keyboardOpen = useKeyboardOpen();
|
||||
|
||||
const load = async () => {
|
||||
const load = useCallback(async () => {
|
||||
setLoadError(''); setLoading(true);
|
||||
try {
|
||||
const { users } = await api.getUsers();
|
||||
setUsers(users || []);
|
||||
} catch(e) { setLoadError(e.message || 'Failed to load users'); }
|
||||
try { const { users } = await api.getUsers(); setUsers(users || []); }
|
||||
catch(e) { setLoadError(e.message || 'Failed to load users'); }
|
||||
finally { setLoading(false); }
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
api.getSettings().then(({ settings }) => { if (settings.user_pass) setUserPass(settings.user_pass); }).catch(() => {});
|
||||
}, []);
|
||||
}, [load]);
|
||||
|
||||
const filtered = users.filter(u =>
|
||||
!search || u.name?.toLowerCase().includes(search.toLowerCase()) ||
|
||||
@@ -317,12 +313,36 @@ export default function UserManagerPage({ isMobile = false, onProfile, onHelp, o
|
||||
u.email?.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
|
||||
// ── Nav item helper (matches Schedule page style) ─────────────────────────
|
||||
const navItem = (label, key) => (
|
||||
<button key={key} onClick={() => setTab(key)}
|
||||
style={{ display:'block', width:'100%', textAlign:'left', padding:'8px 10px',
|
||||
borderRadius:'var(--radius)', border:'none',
|
||||
background: tab===key ? 'var(--primary-light)' : 'transparent',
|
||||
color: tab===key ? 'var(--primary)' : 'var(--text-primary)',
|
||||
cursor:'pointer', fontWeight: tab===key ? 600 : 400, fontSize:14, marginBottom:2 }}>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ display:'flex', flex:1, overflow:'hidden', minHeight:0 }}>
|
||||
|
||||
{/* ── Left panel (desktop only) — blank, reserved for future use ── */}
|
||||
{/* ── Left panel ── */}
|
||||
{!isMobile && (
|
||||
<div style={{ width:SIDEBAR_W, flexShrink:0, borderRight:'1px solid var(--border)', display:'flex', flexDirection:'column', background:'var(--surface)', overflow:'hidden' }}>
|
||||
<div style={{ padding:'16px 16px 0' }}>
|
||||
{/* Title — matches Schedule page */}
|
||||
<div style={{ display:'flex', alignItems:'center', gap:8, marginBottom:16 }}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--primary)" strokeWidth="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
||||
<span style={{ fontSize:16, fontWeight:700, color:'var(--text-primary)' }}>User Manager</span>
|
||||
</div>
|
||||
{/* Tab navigation */}
|
||||
<div className="section-label" style={{ marginBottom:6 }}>View</div>
|
||||
{navItem(`All Users${!loading ? ` (${users.length})` : ''}`, 'users')}
|
||||
{navItem('+ Create User', 'create')}
|
||||
{navItem('Bulk Import CSV', 'bulk')}
|
||||
</div>
|
||||
<div style={{ flex:1 }} />
|
||||
<UserFooter onProfile={onProfile} onHelp={onHelp} onAbout={onAbout} />
|
||||
</div>
|
||||
@@ -331,28 +351,20 @@ export default function UserManagerPage({ isMobile = false, onProfile, onHelp, o
|
||||
{/* ── Right panel ── */}
|
||||
<div style={{ flex:1, display:'flex', flexDirection:'column', overflow:'hidden', minWidth:0, background:'var(--background)' }}>
|
||||
|
||||
{/* Header */}
|
||||
<div style={{ background:'var(--surface)', borderBottom:'1px solid var(--border)', padding:'0 16px', flexShrink:0 }}>
|
||||
<div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', height:52, gap:8 }}>
|
||||
<div style={{ display:'flex', alignItems:'center', gap:8, minWidth:0 }}>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--primary)" strokeWidth="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
||||
<span style={{ fontWeight:700, fontSize:15 }}>User Manager</span>
|
||||
{!loading && <span style={{ fontSize:12, color:'var(--text-tertiary)', flexShrink:0 }}>{users.length} user{users.length!==1?'s':''}</span>}
|
||||
</div>
|
||||
<div style={{ display:'flex', gap:6, flexShrink:0 }}>
|
||||
<button className={`btn btn-sm ${tab==='users'?'btn-primary':'btn-secondary'}`} onClick={() => setTab('users')}>Users</button>
|
||||
<button className={`btn btn-sm ${tab==='create'?'btn-primary':'btn-secondary'}`} onClick={() => setTab('create')}>+ Create</button>
|
||||
{!isMobile && <button className={`btn btn-sm ${tab==='bulk'?'btn-primary':'btn-secondary'}`} onClick={() => setTab('bulk')}>Bulk</button>}
|
||||
</div>
|
||||
{/* Mobile tab bar — only on mobile since desktop uses left panel */}
|
||||
{isMobile && (
|
||||
<div style={{ background:'var(--surface)', borderBottom:'1px solid var(--border)', padding:'0 12px', display:'flex', gap:6, height:48, alignItems:'center', flexShrink:0 }}>
|
||||
<span style={{ fontWeight:700, fontSize:14, marginRight:4, color:'var(--text-primary)' }}>Users</span>
|
||||
<button className={`btn btn-sm ${tab==='users'?'btn-primary':'btn-secondary'}`} onClick={() => setTab('users')}>All</button>
|
||||
<button className={`btn btn-sm ${tab==='create'?'btn-primary':'btn-secondary'}`} onClick={() => setTab('create')}>+ Create</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<div style={{ flex:1, overflowY:'auto', padding:16, paddingBottom: isMobile ? 86 : 16 }}>
|
||||
<div style={{ flex:1, overflowY:'auto', padding:16, paddingBottom: isMobile ? 80 : 16 }}>
|
||||
{tab === 'users' && (
|
||||
<>
|
||||
<input className="input" placeholder="Search users…" value={search} onChange={e => setSearch(e.target.value)}
|
||||
onFocus={() => setInputFocused(true)} onBlur={() => setInputFocused(false)}
|
||||
autoComplete="new-password" autoCorrect="off" spellCheck={false}
|
||||
style={{ marginBottom:16, width:'100%', maxWidth: isMobile ? '100%' : 400 }} />
|
||||
<div style={{ background:'var(--surface)', borderRadius:'var(--radius)', boxShadow:'var(--shadow-sm)', overflow:'hidden' }}>
|
||||
@@ -377,8 +389,8 @@ export default function UserManagerPage({ isMobile = false, onProfile, onHelp, o
|
||||
{tab === 'bulk' && <BulkImportForm userPass={userPass} onCreated={load} />}
|
||||
</div>
|
||||
|
||||
{/* Mobile footer — fixed at bottom, hidden when keyboard open */}
|
||||
{isMobile && !inputFocused && (
|
||||
{/* Mobile footer — hidden when keyboard is open */}
|
||||
{isMobile && !keyboardOpen && (
|
||||
<div style={{ position:'fixed', bottom:0, left:0, right:0, zIndex:20, background:'var(--surface)', borderTop:'1px solid var(--border)' }}>
|
||||
<UserFooter onProfile={onProfile} onHelp={onHelp} onAbout={onAbout} />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user