import React, { useState, useRef, useEffect, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import { Modal } from '../ui/Modal'; import { Avatar } from '../ui/Avatar'; import { useUIStore } from '../../stores/uiStore'; import { useSpaceStore } from '../../stores/spaceStore'; import { useAuthStore } from '../../stores/authStore'; import { useSocialStore, type TaggedFriend } from '../../stores/socialStore'; import { api } from '../../api/client'; import { isSelf, parseFederatedUsername } from '../../utils/identity'; import { useCanonicalUserView } from '../../utils/userViewLookup'; import type { User } from '@backspace/shared'; function AddDmFriendRow({ friend, isInDm, isSelected, atCapacity, isAdding, onToggle, }: { friend: TaggedFriend; isInDm: boolean; isSelected: boolean; atCapacity: boolean; isAdding: boolean; onToggle: (id: string) => void; }) { const canonical = useCanonicalUserView(friend as unknown as User); const { baseName } = parseFederatedUsername(canonical.username); const friendDisplayName = canonical.displayName ?? baseName; return ( onToggle(friend.id)} disabled={isInDm || isAdding || atCapacity} className={`w-full flex items-center gap-3 px-3 py-2 rounded-[4px] transition-colors text-left ${ isInDm ? 'opacity-40 cursor-not-allowed' : isSelected ? 'bg-accent-mint/[0.08]' : 'hover:bg-interactive-hover' } ${atCapacity && !isInDm ? 'opacity-50 cursor-not-allowed' : ''}`} > {friendDisplayName} {isInDm ? 'Already in this DM' : `@${canonical.username}`} {!isInDm && ( {isSelected && ( )} )} ); } export function AddDmMemberModal() { const [query, setQuery] = useState(''); const [selected, setSelected] = useState>(new Set()); const [error, setError] = useState(''); const [isAdding, setIsAdding] = useState(false); const activeModal = useUIStore((s) => s.activeModal); const modalData = useUIStore((s) => s.modalData); const closeModal = useUIStore((s) => s.closeModal); const dmChannels = useSpaceStore((s) => s.dmChannels); const addDmChannel = useSpaceStore((s) => s.addDmChannel); const friends = useSocialStore((s) => s.friends); const navigate = useNavigate(); const myUser = useAuthStore((s) => s.user); const inputRef = useRef(null); const isOpen = activeModal === 'addDmMember'; const dmChannelId = modalData.dmChannelId as string | undefined; const dmChannel = dmChannels.find(dm => dm.id === dmChannelId); const currentMemberIds = useMemo( () => new Set(dmChannel?.members.map(m => m.id) ?? []), [dmChannel?.members], ); const memberCount = dmChannel?.members.length ?? 0; const maxMembers = 10; const remainingSlots = maxMembers - memberCount; // Filter friends: client-side search, exclude self const filteredFriends = useMemo(() => { const q = query.trim().toLowerCase(); return friends.filter((f) => { if (isSelf(f, myUser)) return false; if (!q) return true; const displayName = (f.displayName ?? '').toLowerCase(); const username = f.username.toLowerCase(); return displayName.includes(q) || username.includes(q); }); }, [friends, query, myUser]); // Reset state when modal opens useEffect(() => { if (isOpen) { setQuery(''); setSelected(new Set()); setError(''); setIsAdding(false); setTimeout(() => inputRef.current?.focus(), 100); } }, [isOpen]); const toggleFriend = (friendId: string) => { if (currentMemberIds.has(friendId)) return; setSelected((prev) => { const next = new Set(prev); if (next.has(friendId)) { next.delete(friendId); } else { // Enforce remaining capacity if (next.size >= remainingSlots) return prev; next.add(friendId); } return next; }); }; const removeFriend = (friendId: string) => { setSelected((prev) => { const next = new Set(prev); next.delete(friendId); return next; }); }; const selectedFriends = useMemo( () => friends.filter((f) => selected.has(f.id)), [friends, selected], ); const handleSubmit = async () => { if (!dmChannelId || !dmChannel || isAdding || selectedFriends.length === 0) return; setError(''); setIsAdding(true); try { if (!dmChannel.ownerId) { // 1-on-1 DM → create a new group DM with all selected + existing other member const otherMember = dmChannel.members.find(m => !isSelf(m, myUser)); if (!otherMember) { setError('Could not determine the other member of this conversation.'); setIsAdding(false); return; } const users = [ { id: otherMember.id, homeUserId: otherMember.homeUserId, homeInstance: otherMember.homeInstance }, ...selectedFriends.map((f) => ({ id: f.id, homeUserId: f.homeUserId, homeInstance: f.homeInstance, })), ]; const newChannel = await api.dm.createGroup({ users, fromDmChannelId: dmChannelId }); addDmChannel(newChannel); closeModal(); navigate(`/channels/@me/${newChannel.id}`); } else { // Existing group DM → add each friend sequentially for (const friend of selectedFriends) { await api.dm.addMember(dmChannelId, { userId: friend.homeInstance ? undefined : friend.id, homeUserId: friend.homeUserId ?? undefined, homeInstance: friend.homeInstance ?? undefined, }); } closeModal(); } } catch (err) { setError((err as Error).message || 'Failed to add members'); } finally { setIsAdding(false); } }; const buttonText = selectedFriends.length === 0 ? 'Select Friends' : `Add ${selectedFriends.length} Friend${selectedFriends.length > 1 ? 's' : ''}`; return ( {/* Header with member count */} Select friends to add to this conversation. {memberCount}/{maxMembers} {/* Selected chips */} {selectedFriends.length > 0 && ( {selectedFriends.map((f) => ( {f.displayName ?? parseFederatedUsername(f.username).baseName} removeFriend(f.id)} className="opacity-60 hover:opacity-100 transition-opacity text-[14px] leading-none" > × ))} )} {/* Search input */} setQuery(e.target.value)} placeholder="Search friends..." className="input-search w-full py-2 text-[14px]" disabled={remainingSlots <= 0} /> {remainingSlots <= 0 && ( This group DM has reached the 10-member limit. )} {error && ( {error} )} {/* Friend list */} {filteredFriends.length === 0 && ( {query.trim() ? 'No friends match your search' : 'No friends yet'} )} {filteredFriends.map((friend) => { const isInDm = currentMemberIds.has(friend.id); const isSelected = selected.has(friend.id); const atCapacity = !isSelected && selected.size >= remainingSlots; return ( ); })} {/* Submit button */} {isAdding ? 'Adding...' : buttonText} ); }
Select friends to add to this conversation.
This group DM has reached the 10-member limit.
{error}