feat: Introduce multi-platform architecture for Electron and Web clients with shared UI components, Convex backend for messaging, and integrated search functionality.
Some checks failed
Build and Release / build-and-release (push) Has been cancelled

This commit is contained in:
Bryan1029384756
2026-02-16 13:08:39 -06:00
parent 8ff9213b34
commit ec12313996
49 changed files with 2449 additions and 3914 deletions

View File

@@ -27,6 +27,7 @@ import MessageItem, { getUserColor } from './MessageItem';
import ColoredIcon from './ColoredIcon';
import { usePlatform } from '../platform';
import { useVoice } from '../contexts/VoiceContext';
import { useSearch } from '../contexts/SearchContext';
const metadataCache = new Map();
const attachmentCache = new Map();
@@ -433,7 +434,7 @@ const EmojiButton = ({ onClick, active }) => {
const MessageContextMenu = ({ x, y, onInteract, onClose, isOwner, canDelete }) => {
const menuRef = useRef(null);
const [pos, setPos] = useState({ top: y, left: x });
useEffect(() => { const h = () => onClose(); window.addEventListener('click', h); return () => window.removeEventListener('click', h); }, [onClose]);
useEffect(() => { const h = () => onClose(); window.addEventListener('click', h); window.addEventListener('close-context-menus', h); return () => { window.removeEventListener('click', h); window.removeEventListener('close-context-menus', h); }; }, [onClose]);
React.useLayoutEffect(() => {
if (!menuRef.current) return;
const rect = menuRef.current.getBoundingClientRect();
@@ -468,7 +469,7 @@ const MessageContextMenu = ({ x, y, onInteract, onClose, isOwner, canDelete }) =
const InputContextMenu = ({ x, y, onClose, onPaste }) => {
const menuRef = useRef(null);
const [pos, setPos] = useState({ top: y, left: x });
useEffect(() => { const h = () => onClose(); window.addEventListener('click', h); return () => window.removeEventListener('click', h); }, [onClose]);
useEffect(() => { const h = () => onClose(); window.addEventListener('click', h); window.addEventListener('close-context-menus', h); return () => { window.removeEventListener('click', h); window.removeEventListener('close-context-menus', h); }; }, [onClose]);
React.useLayoutEffect(() => {
if (!menuRef.current) return;
const rect = menuRef.current.getBoundingClientRect();
@@ -492,6 +493,7 @@ const InputContextMenu = ({ x, y, onClose, onPaste }) => {
const ChatArea = ({ channelId, channelName, channelType, username, channelKey, userId: currentUserId, showMembers, onToggleMembers, onOpenDM, showPinned, onTogglePinned }) => {
const { crypto } = usePlatform();
const { isReceivingScreenShareAudio } = useVoice();
const searchCtx = useSearch();
const [decryptedMessages, setDecryptedMessages] = useState([]);
const [input, setInput] = useState('');
const [zoomedImage, setZoomedImage] = useState(null);
@@ -704,6 +706,25 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u
evictCacheIfNeeded();
// Index successfully decrypted messages for search
if (searchCtx?.isReady) {
const toIndex = needsDecryption.map(msg => {
const cached = messageDecryptionCache.get(msg.id);
if (!cached || cached.content.startsWith('[')) return null;
return {
id: msg.id,
channel_id: channelId,
sender_id: msg.sender_id,
username: msg.username,
content: cached.content,
created_at: msg.created_at,
pinned: msg.pinned,
replyToId: msg.replyToId,
};
}).filter(Boolean);
if (toIndex.length > 0) searchCtx.indexMessages(toIndex);
}
if (cancelled) return;
// Phase 3: Re-render with newly decrypted content
@@ -714,6 +735,24 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u
return () => { cancelled = true; };
}, [rawMessages, channelKey]);
// Index cached messages when search DB becomes ready (covers messages decrypted before DB init)
useEffect(() => {
if (!searchCtx?.isReady || !channelId || decryptedMessages.length === 0) return;
const toIndex = decryptedMessages
.filter(m => m.content && !m.content.startsWith('['))
.map(m => ({
id: m.id,
channel_id: channelId,
sender_id: m.sender_id,
username: m.username,
content: m.content,
created_at: m.created_at,
pinned: m.pinned,
replyToId: m.replyToId,
}));
if (toIndex.length > 0) searchCtx.indexMessages(toIndex);
}, [searchCtx?.isReady]);
useEffect(() => {
// Don't clear messageDecryptionCache — it persists across channel switches
setDecryptedMessages([]);
@@ -725,7 +764,7 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u
setMentionQuery(null);
setUnreadDividerTimestamp(null);
onTogglePinned();
}, [channelId, channelKey]);
}, [channelId]);
const myRoleNames = members?.find(m => m.username === username)?.roles?.map(r => r.name) || [];
@@ -1341,7 +1380,7 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u
username={username}
onHover={() => setHoveredMessageId(msg.id)}
onLeave={() => setHoveredMessageId(null)}
onContextMenu={(e) => { e.preventDefault(); setContextMenu({ x: e.clientX, y: e.clientY, messageId: msg.id, isOwner, canDelete }); }}
onContextMenu={(e) => { e.preventDefault(); window.dispatchEvent(new Event('close-context-menus')); setContextMenu({ x: e.clientX, y: e.clientY, messageId: msg.id, isOwner, canDelete }); }}
onAddReaction={(emoji) => { addReaction({ messageId: msg.id, userId: currentUserId, emoji: emoji || 'heart' }); }}
onEdit={() => { setEditingMessage({ id: msg.id, content: msg.content }); setEditInput(msg.content); }}
onReply={() => setReplyingTo({ messageId: msg.id, username: msg.username, content: msg.content?.substring(0, 100) })}
@@ -1440,6 +1479,7 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u
onKeyUp={saveSelection}
onContextMenu={(e) => {
e.preventDefault();
window.dispatchEvent(new Event('close-context-menus'));
setInputContextMenu({ x: e.clientX, y: e.clientY });
}}
onPaste={(e) => {