v0.3.0
This commit is contained in:
@@ -4,7 +4,7 @@ import { api } from '../utils/api.js';
|
||||
import { useToast } from '../contexts/ToastContext.jsx';
|
||||
import Avatar from './Avatar.jsx';
|
||||
|
||||
export default function GroupInfoModal({ group, onClose, onUpdated }) {
|
||||
export default function GroupInfoModal({ group, onClose, onUpdated, onBack }) {
|
||||
const { user } = useAuth();
|
||||
const toast = useToast();
|
||||
const [members, setMembers] = useState([]);
|
||||
@@ -12,12 +12,12 @@ export default function GroupInfoModal({ group, onClose, onUpdated }) {
|
||||
const [newName, setNewName] = useState(group.name);
|
||||
const [addSearch, setAddSearch] = useState('');
|
||||
const [addResults, setAddResults] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const isDirect = !!group.is_direct;
|
||||
const isOwner = group.owner_id === user.id;
|
||||
const isAdmin = user.role === 'admin';
|
||||
const canManage = (group.type === 'private' && isOwner) || (group.type === 'public' && isAdmin);
|
||||
const canRename = !group.is_default && ((group.type === 'public' && isAdmin) || (group.type === 'private' && isOwner));
|
||||
const canManage = !isDirect && ((group.type === 'private' && isOwner) || (group.type === 'public' && isAdmin));
|
||||
const canRename = !isDirect && !group.is_default && ((group.type === 'public' && isAdmin) || (group.type === 'private' && isOwner));
|
||||
|
||||
useEffect(() => {
|
||||
if (group.type === 'private') {
|
||||
@@ -35,24 +35,30 @@ export default function GroupInfoModal({ group, onClose, onUpdated }) {
|
||||
if (!newName.trim() || newName === group.name) { setEditing(false); return; }
|
||||
try {
|
||||
await api.renameGroup(group.id, newName.trim());
|
||||
toast('Group renamed', 'success');
|
||||
toast('Renamed', 'success');
|
||||
onUpdated();
|
||||
setEditing(false);
|
||||
} catch (e) { toast(e.message, 'error'); }
|
||||
};
|
||||
|
||||
const handleLeave = async () => {
|
||||
if (!confirm('Leave this group?')) return;
|
||||
if (!confirm('Leave this message?')) return;
|
||||
try {
|
||||
await api.leaveGroup(group.id);
|
||||
toast('Left group', 'success');
|
||||
onUpdated();
|
||||
toast('Left message', 'success');
|
||||
onClose();
|
||||
if (isDirect) {
|
||||
// For direct messages: socket group:deleted fired by server handles
|
||||
// removing from sidebar and clearing active group — no manual refresh needed
|
||||
} else {
|
||||
onUpdated();
|
||||
if (onBack) onBack();
|
||||
}
|
||||
} catch (e) { toast(e.message, 'error'); }
|
||||
};
|
||||
|
||||
const handleTakeOwnership = async () => {
|
||||
if (!confirm('Take ownership of this private group? You will be able to see all messages.')) return;
|
||||
if (!confirm('Take ownership of this private group?')) return;
|
||||
try {
|
||||
await api.takeOwnership(group.id);
|
||||
toast('Ownership taken', 'success');
|
||||
@@ -64,7 +70,7 @@ export default function GroupInfoModal({ group, onClose, onUpdated }) {
|
||||
const handleAdd = async (u) => {
|
||||
try {
|
||||
await api.addMember(group.id, u.id);
|
||||
toast(`${u.display_name || u.name} added`, 'success');
|
||||
toast(`${u.name} added`, 'success');
|
||||
api.getMembers(group.id).then(({ members }) => setMembers(members));
|
||||
setAddSearch('');
|
||||
setAddResults([]);
|
||||
@@ -72,29 +78,34 @@ export default function GroupInfoModal({ group, onClose, onUpdated }) {
|
||||
};
|
||||
|
||||
const handleRemove = async (member) => {
|
||||
if (!confirm(`Remove ${member.display_name || member.name} from this group?`)) return;
|
||||
if (!confirm(`Remove ${member.name}?`)) return;
|
||||
try {
|
||||
await api.removeMember(group.id, member.id);
|
||||
toast(`${member.display_name || member.name} removed`, 'success');
|
||||
toast(`${member.name} removed`, 'success');
|
||||
setMembers(prev => prev.filter(m => m.id !== member.id));
|
||||
} catch (e) { toast(e.message, 'error'); }
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!confirm('Delete this group? This cannot be undone.')) return;
|
||||
if (!confirm('Delete this message? This cannot be undone.')) return;
|
||||
try {
|
||||
await api.deleteGroup(group.id);
|
||||
toast('Group deleted', 'success');
|
||||
toast('Deleted', 'success');
|
||||
onUpdated();
|
||||
onClose();
|
||||
if (onBack) onBack();
|
||||
} catch (e) { toast(e.message, 'error'); }
|
||||
};
|
||||
|
||||
// For direct messages: only show Delete button (owner = remaining user after other left)
|
||||
const canDeleteDirect = isDirect && isOwner;
|
||||
const canDeleteRegular = !isDirect && (isOwner || (isAdmin && group.type === 'public')) && !group.is_default;
|
||||
|
||||
return (
|
||||
<div className="modal-overlay" onClick={e => e.target === e.currentTarget && onClose()}>
|
||||
<div className="modal">
|
||||
<div className="flex items-center justify-between" style={{ marginBottom: 20 }}>
|
||||
<h2 className="modal-title" style={{ margin: 0 }}>Group Info</h2>
|
||||
<h2 className="modal-title" style={{ margin: 0 }}>Message Info</h2>
|
||||
<button className="btn-icon" onClick={onClose}>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||
</button>
|
||||
@@ -120,14 +131,14 @@ export default function GroupInfoModal({ group, onClose, onUpdated }) {
|
||||
)}
|
||||
<div className="flex items-center gap-6" style={{ gap: 8, marginTop: 4 }}>
|
||||
<span className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||
{group.type === 'public' ? 'Public channel' : 'Private group'}
|
||||
{isDirect ? 'Direct message' : group.type === 'public' ? 'Public message' : 'Private message'}
|
||||
</span>
|
||||
{group.is_readonly && <span className="readonly-badge" style={{ fontSize: 11, padding: '2px 8px', borderRadius: 10, background: '#fff3e0', color: '#e65100' }}>Read-only</span>}
|
||||
{!!group.is_readonly && <span className="readonly-badge" style={{ fontSize: 11, padding: '2px 8px', borderRadius: 10, background: '#fff3e0', color: '#e65100' }}>Read-only</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Members (private groups) */}
|
||||
{group.type === 'private' && (
|
||||
{/* Members — shown for private non-direct groups */}
|
||||
{group.type === 'private' && !isDirect && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div className="text-sm font-medium" style={{ color: 'var(--text-secondary)', marginBottom: 8 }}>
|
||||
Members ({members.length})
|
||||
@@ -136,18 +147,13 @@ export default function GroupInfoModal({ group, onClose, onUpdated }) {
|
||||
{members.map(m => (
|
||||
<div key={m.id} className="flex items-center" style={{ gap: 10, padding: '6px 0' }}>
|
||||
<Avatar user={m} size="sm" />
|
||||
<span className="flex-1 text-sm">{m.display_name || m.name}</span>
|
||||
<span className="flex-1 text-sm">{m.name}</span>
|
||||
{m.id === group.owner_id && <span className="text-xs" style={{ color: 'var(--text-tertiary)' }}>Owner</span>}
|
||||
{canManage && m.id !== group.owner_id && (
|
||||
<button
|
||||
onClick={() => handleRemove(m)}
|
||||
title="Remove from group"
|
||||
style={{
|
||||
background: 'none', border: 'none', cursor: 'pointer',
|
||||
color: 'var(--text-tertiary)', padding: '2px 4px', borderRadius: 4,
|
||||
lineHeight: 1, fontSize: 16,
|
||||
transition: 'color var(--transition)',
|
||||
}}
|
||||
title="Remove"
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-tertiary)', padding: '2px 4px', borderRadius: 4, lineHeight: 1, transition: 'color var(--transition)' }}
|
||||
onMouseEnter={e => e.currentTarget.style.color = 'var(--error)'}
|
||||
onMouseLeave={e => e.currentTarget.style.color = 'var(--text-tertiary)'}
|
||||
>
|
||||
@@ -159,16 +165,15 @@ export default function GroupInfoModal({ group, onClose, onUpdated }) {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{canManage && (
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<input className="input" placeholder="Search to add member..." value={addSearch} onChange={e => setAddSearch(e.target.value)} />
|
||||
{addResults.length > 0 && addSearch && (
|
||||
<div style={{ border: '1px solid var(--border)', borderRadius: 'var(--radius)', marginTop: 4, maxHeight: 150, overflowY: 'auto' }}>
|
||||
<div style={{ border: '1px solid var(--border)', borderRadius: 'var(--radius)', marginTop: 4, maxHeight: 150, overflowY: 'auto', background: 'var(--surface)' }}>
|
||||
{addResults.filter(u => !members.find(m => m.id === u.id)).map(u => (
|
||||
<button key={u.id} className="flex items-center gap-2 w-full" style={{ gap: 10, padding: '8px 12px', textAlign: 'left', transition: 'background var(--transition)' }} onClick={() => handleAdd(u)} onMouseEnter={e => e.currentTarget.style.background = 'var(--background)'} onMouseLeave={e => e.currentTarget.style.background = ''}>
|
||||
<button key={u.id} className="flex items-center gap-2 w-full" style={{ gap: 10, padding: '8px 12px', textAlign: 'left', transition: 'background var(--transition)', color: 'var(--text-primary)' }} onClick={() => handleAdd(u)} onMouseEnter={e => e.currentTarget.style.background = 'var(--background)'} onMouseLeave={e => e.currentTarget.style.background = ''}>
|
||||
<Avatar user={u} size="sm" />
|
||||
<span className="text-sm flex-1">{u.display_name || u.name}</span>
|
||||
<span className="text-sm flex-1">{u.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -180,16 +185,21 @@ export default function GroupInfoModal({ group, onClose, onUpdated }) {
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex-col gap-2">
|
||||
{group.type === 'private' && group.owner_id !== user.id && (
|
||||
{/* Direct message: leave (if not already owner/last person) */}
|
||||
{isDirect && !isOwner && (
|
||||
<button className="btn btn-secondary w-full" onClick={handleLeave}>Leave Conversation</button>
|
||||
)}
|
||||
{/* Regular private: leave if not owner */}
|
||||
{!isDirect && group.type === 'private' && !isOwner && (
|
||||
<button className="btn btn-secondary w-full" onClick={handleLeave}>Leave Group</button>
|
||||
)}
|
||||
{isAdmin && group.type === 'private' && group.owner_id !== user.id && (
|
||||
<button className="btn btn-secondary w-full" onClick={handleTakeOwnership}>
|
||||
Take Ownership (Admin)
|
||||
</button>
|
||||
{/* Admin take ownership (non-direct only) */}
|
||||
{!isDirect && isAdmin && group.type === 'private' && !isOwner && (
|
||||
<button className="btn btn-secondary w-full" onClick={handleTakeOwnership}>Take Ownership (Admin)</button>
|
||||
)}
|
||||
{(isOwner || (isAdmin && group.type === 'public')) && !group.is_default && (
|
||||
<button className="btn btn-danger w-full" onClick={handleDelete}>Delete Group</button>
|
||||
{/* Delete */}
|
||||
{(canDeleteDirect || canDeleteRegular) && (
|
||||
<button className="btn btn-danger w-full" onClick={handleDelete}>Delete</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user