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
Some checks failed
Build and Release / build-and-release (push) Has been cancelled
This commit is contained in:
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user