v0.5.0 UI and new message rules
This commit is contained in:
@@ -12,6 +12,8 @@ export default function GroupInfoModal({ group, onClose, onUpdated, onBack }) {
|
||||
const [newName, setNewName] = useState(group.name);
|
||||
const [addSearch, setAddSearch] = useState('');
|
||||
const [addResults, setAddResults] = useState([]);
|
||||
const [customName, setCustomName] = useState(group.owner_name_original ? group.name : '');
|
||||
const [savingCustom, setSavingCustom] = useState(false);
|
||||
|
||||
const isDirect = !!group.is_direct;
|
||||
const isOwner = group.owner_id === user.id;
|
||||
@@ -25,6 +27,19 @@ export default function GroupInfoModal({ group, onClose, onUpdated, onBack }) {
|
||||
}
|
||||
}, [group.id]);
|
||||
|
||||
const handleCustomName = async () => {
|
||||
setSavingCustom(true);
|
||||
try {
|
||||
await api.setCustomGroupName(group.id, customName.trim());
|
||||
toast(customName.trim() ? 'Custom name saved' : 'Custom name removed', 'success');
|
||||
onUpdated();
|
||||
} catch (e) {
|
||||
toast(e.message, 'error');
|
||||
} finally {
|
||||
setSavingCustom(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (addSearch) {
|
||||
api.searchUsers(addSearch).then(({ users }) => setAddResults(users)).catch(() => {});
|
||||
@@ -115,7 +130,7 @@ export default function GroupInfoModal({ group, onClose, onUpdated, onBack }) {
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
{editing ? (
|
||||
<div className="flex gap-2">
|
||||
<input className="input flex-1" value={newName} onChange={e => setNewName(e.target.value)} autoFocus onKeyDown={e => e.key === 'Enter' && handleRename()} />
|
||||
<input className="input flex-1" value={newName} onChange={e => setNewName(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleRename()} />
|
||||
<button className="btn btn-primary btn-sm" onClick={handleRename}>Save</button>
|
||||
<button className="btn btn-secondary btn-sm" onClick={() => setEditing(false)}>Cancel</button>
|
||||
</div>
|
||||
@@ -137,6 +152,31 @@ export default function GroupInfoModal({ group, onClose, onUpdated, onBack }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Custom name — any user can set their own display name for this group */}
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<label className="text-sm font-medium" style={{ color: 'var(--text-secondary)', display: 'block', marginBottom: 6 }}>
|
||||
Your custom name <span style={{ fontWeight: 400, color: 'var(--text-tertiary)' }}>(only visible to you)</span>
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
className="input flex-1"
|
||||
value={customName}
|
||||
onChange={e => setCustomName(e.target.value)}
|
||||
placeholder={group.owner_name_original || group.name}
|
||||
onKeyDown={e => e.key === 'Enter' && handleCustomName()}
|
||||
/>
|
||||
<button className="btn btn-primary btn-sm" onClick={handleCustomName} disabled={savingCustom}>
|
||||
{customName.trim() ? 'Save' : 'Remove'}
|
||||
</button>
|
||||
</div>
|
||||
{group.owner_name_original && (
|
||||
<p className="text-xs" style={{ color: 'var(--text-tertiary)', marginTop: 4 }}>
|
||||
Showing as: <strong>{customName.trim() || group.owner_name_original}</strong>
|
||||
{customName.trim() && <span> ({group.owner_name_original})</span>}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Members — shown for private non-direct groups */}
|
||||
{group.type === 'private' && !isDirect && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
|
||||
@@ -57,8 +57,12 @@ export default function NewChatModal({ onClose, onCreated }) {
|
||||
};
|
||||
}
|
||||
|
||||
const { group } = await api.createGroup(payload);
|
||||
toast(isDirect ? 'Direct message started!' : `${tab === 'public' ? 'Public message' : 'Group message'} created!`, 'success');
|
||||
const { group, duplicate } = await api.createGroup(payload);
|
||||
if (duplicate) {
|
||||
toast('A group with these members already exists — opening it now.', 'info');
|
||||
} else {
|
||||
toast(isDirect ? 'Direct message started!' : `${tab === 'public' ? 'Public message' : 'Group message'} created!`, 'success');
|
||||
}
|
||||
onCreated(group);
|
||||
} catch (e) {
|
||||
toast(e.message, 'error');
|
||||
@@ -89,8 +93,8 @@ export default function NewChatModal({ onClose, onCreated }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Message Name — hidden for direct (1-user) messages */}
|
||||
{!isDirect && (
|
||||
{/* Message Name — only shown when needed: public always, private only when 2+ members selected */}
|
||||
{(tab === 'public' || (tab === 'private' && selected.length > 1)) && (
|
||||
<div className="flex-col gap-2" style={{ marginBottom: 16 }}>
|
||||
<label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>Message Name</label>
|
||||
<input
|
||||
@@ -98,7 +102,6 @@ export default function NewChatModal({ onClose, onCreated }) {
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
placeholder={namePlaceholder}
|
||||
autoFocus={tab === 'public'}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -118,7 +121,7 @@ export default function NewChatModal({ onClose, onCreated }) {
|
||||
<label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>
|
||||
{isDirect ? 'Direct Message with' : 'Add Members'}
|
||||
</label>
|
||||
<input className="input" placeholder="Search users..." value={search} onChange={e => setSearch(e.target.value)} autoFocus />
|
||||
<input className="input" placeholder="Search users..." value={search} onChange={e => setSearch(e.target.value)} />
|
||||
</div>
|
||||
|
||||
{selected.length > 0 && (
|
||||
|
||||
@@ -101,7 +101,7 @@ export default function SupportModal({ onClose }) {
|
||||
placeholder="Jane Smith"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
autoFocus
|
||||
|
||||
maxLength={100}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -115,7 +115,7 @@ function UserRow({ u, onUpdated }) {
|
||||
value={nameVal}
|
||||
onChange={e => setNameVal(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === 'Enter') handleSaveName(); if (e.key === 'Escape') { setEditName(false); setNameVal(u.name); } }}
|
||||
autoFocus
|
||||
|
||||
/>
|
||||
<button className="btn btn-primary btn-sm" onClick={handleSaveName}>Save</button>
|
||||
<button className="btn btn-secondary btn-sm" onClick={() => { setEditName(false); setNameVal(u.name); }}>✕</button>
|
||||
@@ -157,7 +157,7 @@ function UserRow({ u, onUpdated }) {
|
||||
value={resetPw}
|
||||
onChange={e => setResetPw(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === 'Enter') handleResetPw(); if (e.key === 'Escape') { setShowReset(false); setResetPw(''); } }}
|
||||
autoFocus
|
||||
|
||||
/>
|
||||
<button className="btn btn-primary btn-sm" onClick={handleResetPw}>Set</button>
|
||||
<button className="btn btn-secondary btn-sm" onClick={() => { setShowReset(false); setResetPw(''); }}>✕</button>
|
||||
|
||||
Reference in New Issue
Block a user