diff --git a/src/components/Chat/ChatContainer.jsx b/src/components/Chat/ChatContainer.jsx index 1252c9b..b744da9 100644 --- a/src/components/Chat/ChatContainer.jsx +++ b/src/components/Chat/ChatContainer.jsx @@ -8,6 +8,7 @@ import ChatWindow from "./ChatWindow"; import "./ChatContainer.css"; const ACTIVE_CHATS_KEY = "sociowire:activeChats"; +const MINIMIZED_CHATS_KEY = "sociowire:minimizedChats"; // Play notification sound using Web Audio API function playNotificationSound() { @@ -221,16 +222,23 @@ export default function ChatContainer() { loadConversations(); }, [authenticated, connected, fetchConversations]); - // Load active chats from localStorage + // Load active chats and minimized state from localStorage useEffect(() => { try { - const saved = localStorage.getItem(ACTIVE_CHATS_KEY); - if (saved) { - const parsed = JSON.parse(saved); + const savedChats = localStorage.getItem(ACTIVE_CHATS_KEY); + if (savedChats) { + const parsed = JSON.parse(savedChats); if (Array.isArray(parsed)) { setActiveChats(parsed); } } + const savedMinimized = localStorage.getItem(MINIMIZED_CHATS_KEY); + if (savedMinimized) { + const parsed = JSON.parse(savedMinimized); + if (typeof parsed === 'object' && parsed !== null) { + setMinimizedChats(parsed); + } + } } catch {} }, []); @@ -241,6 +249,24 @@ export default function ChatContainer() { } catch {} }, [activeChats]); + // Save minimized state to localStorage + useEffect(() => { + try { + localStorage.setItem(MINIMIZED_CHATS_KEY, JSON.stringify(minimizedChats)); + } catch {} + }, [minimizedChats]); + + // Fetch messages for restored active chats when connection is established + const hasLoadedRestoredChats = useRef(false); + useEffect(() => { + if (connected && activeChats.length > 0 && !hasLoadedRestoredChats.current) { + hasLoadedRestoredChats.current = true; + activeChats.forEach((chat) => { + fetchMessages(chat.username); + }); + } + }, [connected, activeChats, fetchMessages]); + // Refresh messages and unread counts when page becomes visible (mobile resume) useEffect(() => { const handleVisibilityChange = async () => { diff --git a/src/components/Map/MapView.jsx b/src/components/Map/MapView.jsx index e555979..0028f62 100644 --- a/src/components/Map/MapView.jsx +++ b/src/components/Map/MapView.jsx @@ -212,69 +212,153 @@ export default function MapView({ [mapRef, theme] ); - // Long-press detection for opening create post popup (1 second) + // Long-press detection for creating posts (2.5 seconds) + // Cancels on: zoom, pan, multi-touch, finger movement > 10px const longPressTimerRef = useRef(null); - const longPressStartRef = useRef(null); - const LONG_PRESS_DURATION = 1000; // 1 second + const longPressStartPos = useRef(null); + const longPressCallbackRef = useRef(null); + const LONG_PRESS_DURATION = 1000; + const LONG_PRESS_MOVE_THRESHOLD = 10; // pixels - const handleLongPressStart = useCallback((e) => { - // Only trigger on map container, not on overlays/buttons - if (e.target.closest('.map-overlay') || e.target.closest('.sw-marker') || e.target.closest('button')) { - return; - } - longPressStartRef.current = { x: e.clientX || e.touches?.[0]?.clientX, y: e.clientY || e.touches?.[0]?.clientY }; - longPressTimerRef.current = setTimeout(() => { - handleOpenCreate(); - }, LONG_PRESS_DURATION); - }, []); - - const handleLongPressEnd = useCallback(() => { + const cancelLongPress = useCallback(() => { if (longPressTimerRef.current) { clearTimeout(longPressTimerRef.current); longPressTimerRef.current = null; } - longPressStartRef.current = null; + longPressStartPos.current = null; }, []); - const handleLongPressMove = useCallback((e) => { - if (!longPressStartRef.current) return; - const x = e.clientX || e.touches?.[0]?.clientX; - const y = e.clientY || e.touches?.[0]?.clientY; - const dx = Math.abs(x - longPressStartRef.current.x); - const dy = Math.abs(y - longPressStartRef.current.y); - // If moved more than 10px, cancel the long press - if (dx > 10 || dy > 10) { - handleLongPressEnd(); - } - }, [handleLongPressEnd]); + // Cancel long press on ANY map interaction + useEffect(() => { + const map = mapRef.current; + if (!map) return; + const cancel = () => cancelLongPress(); + + map.on('zoomstart', cancel); + map.on('movestart', cancel); + map.on('pitchstart', cancel); + map.on('rotatestart', cancel); + map.on('zoom', cancel); + map.on('move', cancel); + map.on('pitch', cancel); + map.on('rotate', cancel); + map.on('dragstart', cancel); + map.on('drag', cancel); + + return () => { + map.off('zoomstart', cancel); + map.off('movestart', cancel); + map.off('pitchstart', cancel); + map.off('rotatestart', cancel); + map.off('zoom', cancel); + map.off('move', cancel); + map.off('pitch', cancel); + map.off('rotate', cancel); + map.off('dragstart', cancel); + map.off('drag', cancel); + }; + }, [mapRef.current, cancelLongPress]); + + // Touch/mouse event handlers for long-press + // Re-run when viewParams changes (indicates map is ready) useEffect(() => { const container = containerRef.current; if (!container) return; - container.addEventListener('mousedown', handleLongPressStart); - container.addEventListener('mouseup', handleLongPressEnd); - container.addEventListener('mouseleave', handleLongPressEnd); - container.addEventListener('mousemove', handleLongPressMove); - container.addEventListener('touchstart', handleLongPressStart, { passive: true }); - container.addEventListener('touchend', handleLongPressEnd); - container.addEventListener('touchcancel', handleLongPressEnd); - container.addEventListener('touchmove', handleLongPressMove, { passive: true }); + const handleTouchStart = (e) => { + // Multi-touch = cancel immediately + if (e.touches.length !== 1) { + cancelLongPress(); + return; + } - return () => { - container.removeEventListener('mousedown', handleLongPressStart); - container.removeEventListener('mouseup', handleLongPressEnd); - container.removeEventListener('mouseleave', handleLongPressEnd); - container.removeEventListener('mousemove', handleLongPressMove); - container.removeEventListener('touchstart', handleLongPressStart); - container.removeEventListener('touchend', handleLongPressEnd); - container.removeEventListener('touchcancel', handleLongPressEnd); - container.removeEventListener('touchmove', handleLongPressMove); - if (longPressTimerRef.current) { - clearTimeout(longPressTimerRef.current); + // Ignore UI elements + if (e.target.closest('.map-overlay') || e.target.closest('.sw-marker') || e.target.closest('button')) { + return; + } + + const touch = e.touches[0]; + longPressStartPos.current = { x: touch.clientX, y: touch.clientY }; + + longPressTimerRef.current = setTimeout(() => { + if (longPressStartPos.current && longPressCallbackRef.current) { + longPressCallbackRef.current(); + } + cancelLongPress(); + }, LONG_PRESS_DURATION); + }; + + const handleTouchMove = (e) => { + // Multi-touch = cancel + if (e.touches.length !== 1) { + cancelLongPress(); + return; + } + + // Check if finger moved too far from start position + if (longPressStartPos.current && e.touches[0]) { + const dx = e.touches[0].clientX - longPressStartPos.current.x; + const dy = e.touches[0].clientY - longPressStartPos.current.y; + const distance = Math.sqrt(dx * dx + dy * dy); + if (distance > LONG_PRESS_MOVE_THRESHOLD) { + cancelLongPress(); + } } }; - }, [containerRef, handleLongPressStart, handleLongPressEnd, handleLongPressMove]); + + const handleTouchEnd = () => cancelLongPress(); + + const handleMouseDown = (e) => { + if (e.target.closest('.map-overlay') || e.target.closest('.sw-marker') || e.target.closest('button')) { + return; + } + longPressStartPos.current = { x: e.clientX, y: e.clientY }; + longPressTimerRef.current = setTimeout(() => { + if (longPressStartPos.current && longPressCallbackRef.current) { + longPressCallbackRef.current(); + } + cancelLongPress(); + }, LONG_PRESS_DURATION); + }; + + const handleMouseMove = (e) => { + if (longPressStartPos.current) { + const dx = e.clientX - longPressStartPos.current.x; + const dy = e.clientY - longPressStartPos.current.y; + const distance = Math.sqrt(dx * dx + dy * dy); + if (distance > LONG_PRESS_MOVE_THRESHOLD) { + cancelLongPress(); + } + } + }; + + const handleMouseUp = () => cancelLongPress(); + const handleWheel = () => cancelLongPress(); + + container.addEventListener('touchstart', handleTouchStart, { passive: true }); + container.addEventListener('touchmove', handleTouchMove, { passive: true }); + container.addEventListener('touchend', handleTouchEnd); + container.addEventListener('touchcancel', handleTouchEnd); + container.addEventListener('mousedown', handleMouseDown); + container.addEventListener('mousemove', handleMouseMove); + container.addEventListener('mouseup', handleMouseUp); + container.addEventListener('mouseleave', handleMouseUp); + container.addEventListener('wheel', handleWheel, { passive: true }); + + return () => { + container.removeEventListener('touchstart', handleTouchStart); + container.removeEventListener('touchmove', handleTouchMove); + container.removeEventListener('touchend', handleTouchEnd); + container.removeEventListener('touchcancel', handleTouchEnd); + container.removeEventListener('mousedown', handleMouseDown); + container.removeEventListener('mousemove', handleMouseMove); + container.removeEventListener('mouseup', handleMouseUp); + container.removeEventListener('mouseleave', handleMouseUp); + container.removeEventListener('wheel', handleWheel); + cancelLongPress(); + }; + }, [viewParams, cancelLongPress]); const fitBoundsWhenReady = useCallback( (bounds, opts = {}) => { @@ -1198,6 +1282,11 @@ export default function MapView({ setModeChosen(false); }; + // Update ref for long-press callback + useEffect(() => { + longPressCallbackRef.current = handleOpenCreate; + }, [authenticated, needsEmailVerification, mainFilter]); + useEffect(() => { setUseCustomImage(contentMode !== "link"); }, [contentMode]); @@ -1735,295 +1824,181 @@ export default function MapView({ )} {modeSelectorVisible && ( -