mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-03-28 17:43:05 +01:00
254 lines
8.3 KiB
TypeScript
254 lines
8.3 KiB
TypeScript
import { useState, useCallback, useEffect, useRef, type MutableRefObject } from 'react';
|
|
import {
|
|
parseHashConversation,
|
|
updateUrlHash,
|
|
resolveChannelFromHashToken,
|
|
resolveContactFromHashToken,
|
|
} from '../utils/urlHash';
|
|
import {
|
|
getLastViewedConversation,
|
|
getReopenLastConversationEnabled,
|
|
saveLastViewedConversation,
|
|
} from '../utils/lastViewedConversation';
|
|
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, setActiveConversationState] = useState<Conversation | null>(null);
|
|
const activeConversationRef = useRef<Conversation | null>(null);
|
|
const hashSyncEnabledRef = useRef(
|
|
typeof window !== 'undefined' ? window.location.hash.length > 0 : false
|
|
);
|
|
|
|
const setActiveConversation = useCallback((conv: Conversation | null) => {
|
|
hashSyncEnabledRef.current = true;
|
|
setActiveConversationState(conv);
|
|
}, []);
|
|
|
|
const getPublicChannelConversation = useCallback((): Conversation | null => {
|
|
const publicChannel = channels.find((c) => c.name === 'Public');
|
|
if (!publicChannel) return null;
|
|
return {
|
|
type: 'channel',
|
|
id: publicChannel.key,
|
|
name: publicChannel.name,
|
|
};
|
|
}, [channels]);
|
|
|
|
// 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') {
|
|
setActiveConversationState({ type: 'raw', id: 'raw', name: 'Raw Packet Feed' });
|
|
hasSetDefaultConversation.current = true;
|
|
return;
|
|
}
|
|
if (hashConv?.type === 'map') {
|
|
setActiveConversationState({
|
|
type: 'map',
|
|
id: 'map',
|
|
name: 'Node Map',
|
|
mapFocusKey: hashConv.mapFocusKey,
|
|
});
|
|
hasSetDefaultConversation.current = true;
|
|
return;
|
|
}
|
|
if (hashConv?.type === 'visualizer') {
|
|
setActiveConversationState({ type: 'visualizer', id: 'visualizer', name: 'Mesh Visualizer' });
|
|
hasSetDefaultConversation.current = true;
|
|
return;
|
|
}
|
|
if (hashConv?.type === 'search') {
|
|
setActiveConversationState({ type: 'search', id: 'search', name: 'Message Search' });
|
|
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) {
|
|
setActiveConversationState({ 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: optionally restore last-viewed conversation if enabled on this device.
|
|
if (!hashConv && getReopenLastConversationEnabled()) {
|
|
const lastViewed = getLastViewedConversation();
|
|
if (
|
|
lastViewed &&
|
|
(lastViewed.type === 'raw' || lastViewed.type === 'map' || lastViewed.type === 'visualizer')
|
|
) {
|
|
setActiveConversationState(lastViewed);
|
|
hasSetDefaultConversation.current = true;
|
|
return;
|
|
}
|
|
if (lastViewed?.type === 'channel') {
|
|
const channel =
|
|
channels.find((c) => c.key.toLowerCase() === lastViewed.id.toLowerCase()) ||
|
|
resolveChannelFromHashToken(lastViewed.id, channels);
|
|
if (channel) {
|
|
setActiveConversationState({
|
|
type: 'channel',
|
|
id: channel.key,
|
|
name: channel.name,
|
|
});
|
|
hasSetDefaultConversation.current = true;
|
|
return;
|
|
}
|
|
}
|
|
// Last-viewed contact resolution waits for contacts in phase 2.
|
|
if (lastViewed?.type === 'contact') return;
|
|
}
|
|
|
|
// No hash or unresolvable — default to Public
|
|
const publicConversation = getPublicChannelConversation();
|
|
if (publicConversation) {
|
|
setActiveConversationState(publicConversation);
|
|
hasSetDefaultConversation.current = true;
|
|
}
|
|
}, [channels, activeConversation, getPublicChannelConversation, hasSetDefaultConversation]);
|
|
|
|
// 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) {
|
|
setActiveConversationState({
|
|
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.
|
|
const publicConversation = getPublicChannelConversation();
|
|
if (publicConversation) {
|
|
setActiveConversationState(publicConversation);
|
|
hasSetDefaultConversation.current = true;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// No hash: optionally restore a last-viewed contact once contacts are loaded.
|
|
if (!hashConv && getReopenLastConversationEnabled()) {
|
|
const lastViewed = getLastViewedConversation();
|
|
if (lastViewed?.type !== 'contact') return;
|
|
if (!contactsLoaded) return;
|
|
|
|
const contact =
|
|
contacts.find((item) => item.public_key.toLowerCase() === lastViewed.id.toLowerCase()) ||
|
|
resolveContactFromHashToken(lastViewed.id, contacts);
|
|
if (contact) {
|
|
setActiveConversationState({
|
|
type: 'contact',
|
|
id: contact.public_key,
|
|
name: getContactDisplayName(contact.name, contact.public_key),
|
|
});
|
|
hasSetDefaultConversation.current = true;
|
|
return;
|
|
}
|
|
|
|
const publicConversation = getPublicChannelConversation();
|
|
if (publicConversation) {
|
|
setActiveConversationState(publicConversation);
|
|
hasSetDefaultConversation.current = true;
|
|
}
|
|
}
|
|
}, [
|
|
contacts,
|
|
channels,
|
|
activeConversation,
|
|
contactsLoaded,
|
|
getPublicChannelConversation,
|
|
hasSetDefaultConversation,
|
|
]);
|
|
|
|
// Keep ref in sync and update URL hash
|
|
useEffect(() => {
|
|
activeConversationRef.current = activeConversation;
|
|
if (activeConversation) {
|
|
if (hashSyncEnabledRef.current) {
|
|
updateUrlHash(activeConversation);
|
|
}
|
|
if (getReopenLastConversationEnabled() && activeConversation.type !== 'search') {
|
|
saveLastViewedConversation(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;
|
|
setActiveConversationState({
|
|
type: 'channel',
|
|
id: publicChannel.key,
|
|
name: publicChannel.name,
|
|
});
|
|
}, [activeConversation, channels, hasSetDefaultConversation, pendingDeleteFallbackRef]);
|
|
|
|
// Handle conversation selection (closes sidebar on mobile)
|
|
const handleSelectConversation = useCallback(
|
|
(conv: Conversation) => {
|
|
setActiveConversation(conv);
|
|
setSidebarOpen(false);
|
|
},
|
|
[setActiveConversation, setSidebarOpen]
|
|
);
|
|
|
|
return {
|
|
activeConversation,
|
|
setActiveConversation,
|
|
activeConversationRef,
|
|
handleSelectConversation,
|
|
};
|
|
}
|