Files
Remote-Terminal-for-MeshCore/frontend/src/hooks/useConversationRouter.ts
2026-02-16 17:28:21 -08:00

168 lines
5.2 KiB
TypeScript

import { useState, useCallback, useEffect, useRef, type MutableRefObject } from 'react';
import {
parseHashConversation,
updateUrlHash,
resolveChannelFromHashToken,
resolveContactFromHashToken,
} from '../utils/urlHash';
import { getContactDisplayName } from '../utils/pubkey';
import type { Channel, Contact, Conversation } from '../types';
const PUBLIC_CHANNEL_KEY = '8B3387E9C5CDEA6AC9E5EDBAA115CD72';
interface UseConversationRouterArgs {
channels: Channel[];
contacts: Contact[];
contactsLoaded: boolean;
setSidebarOpen: (open: boolean) => void;
pendingDeleteFallbackRef: MutableRefObject<boolean>;
hasSetDefaultConversation: MutableRefObject<boolean>;
}
export function useConversationRouter({
channels,
contacts,
contactsLoaded,
setSidebarOpen,
pendingDeleteFallbackRef,
hasSetDefaultConversation,
}: UseConversationRouterArgs) {
const [activeConversation, setActiveConversation] = useState<Conversation | null>(null);
const activeConversationRef = useRef<Conversation | null>(null);
// Phase 1: Set initial conversation from URL hash or default to Public channel
// Only needs channels (fast path) - doesn't wait for contacts
useEffect(() => {
if (hasSetDefaultConversation.current || activeConversation) return;
if (channels.length === 0) return;
const hashConv = parseHashConversation();
// Handle non-data views immediately
if (hashConv?.type === 'raw') {
setActiveConversation({ type: 'raw', id: 'raw', name: 'Raw Packet Feed' });
hasSetDefaultConversation.current = true;
return;
}
if (hashConv?.type === 'map') {
setActiveConversation({
type: 'map',
id: 'map',
name: 'Node Map',
mapFocusKey: hashConv.mapFocusKey,
});
hasSetDefaultConversation.current = true;
return;
}
if (hashConv?.type === 'visualizer') {
setActiveConversation({ type: 'visualizer', id: 'visualizer', name: 'Mesh Visualizer' });
hasSetDefaultConversation.current = true;
return;
}
// Handle channel hash (ID-first with legacy-name fallback)
if (hashConv?.type === 'channel') {
const channel = resolveChannelFromHashToken(hashConv.name, channels);
if (channel) {
setActiveConversation({ type: 'channel', id: channel.key, name: channel.name });
hasSetDefaultConversation.current = true;
return;
}
}
// Contact hash — wait for phase 2
if (hashConv?.type === 'contact') return;
// No hash or unresolvable — default to Public
const publicChannel = channels.find((c) => c.name === 'Public');
if (publicChannel) {
setActiveConversation({
type: 'channel',
id: publicChannel.key,
name: publicChannel.name,
});
hasSetDefaultConversation.current = true;
}
}, [channels, activeConversation]);
// Phase 2: Resolve contact hash (only if phase 1 didn't set a conversation)
useEffect(() => {
if (hasSetDefaultConversation.current || activeConversation) return;
const hashConv = parseHashConversation();
if (hashConv?.type === 'contact') {
if (!contactsLoaded) return;
const contact = resolveContactFromHashToken(hashConv.name, contacts);
if (contact) {
setActiveConversation({
type: 'contact',
id: contact.public_key,
name: getContactDisplayName(contact.name, contact.public_key),
});
hasSetDefaultConversation.current = true;
return;
}
// Contact hash didn't match — fall back to Public if channels loaded.
if (channels.length > 0) {
const publicChannel = channels.find((c) => c.name === 'Public');
if (publicChannel) {
setActiveConversation({
type: 'channel',
id: publicChannel.key,
name: publicChannel.name,
});
hasSetDefaultConversation.current = true;
}
}
}
}, [contacts, channels, activeConversation, contactsLoaded]);
// Keep ref in sync and update URL hash
useEffect(() => {
activeConversationRef.current = activeConversation;
if (activeConversation) {
updateUrlHash(activeConversation);
}
}, [activeConversation]);
// If a delete action left us without an active conversation, recover to Public
useEffect(() => {
if (!pendingDeleteFallbackRef.current) return;
if (activeConversation) {
pendingDeleteFallbackRef.current = false;
return;
}
const publicChannel =
channels.find((c) => c.key === PUBLIC_CHANNEL_KEY) ||
channels.find((c) => c.name === 'Public');
if (!publicChannel) return;
hasSetDefaultConversation.current = true;
pendingDeleteFallbackRef.current = false;
setActiveConversation({
type: 'channel',
id: publicChannel.key,
name: publicChannel.name,
});
}, [activeConversation, channels]);
// Handle conversation selection (closes sidebar on mobile)
const handleSelectConversation = useCallback(
(conv: Conversation) => {
setActiveConversation(conv);
setSidebarOpen(false);
},
[setSidebarOpen]
);
return {
activeConversation,
setActiveConversation,
activeConversationRef,
handleSelectConversation,
};
}