diff --git a/src/components/Chat/ChatContainer.jsx b/src/components/Chat/ChatContainer.jsx index b744da9..c8cd1df 100644 --- a/src/components/Chat/ChatContainer.jsx +++ b/src/components/Chat/ChatContainer.jsx @@ -41,6 +41,7 @@ export default function ChatContainer() { const [activeChats, setActiveChats] = useState([]); // [{ username, avatar }] const [minimizedChats, setMinimizedChats] = useState({}); // { username: boolean } const [unreadCounts, setUnreadCounts] = useState({}); // { username: number } + const [showSidebar, setShowSidebar] = useState(false); // sidebar with friends list const notificationPermissionRef = useRef("default"); const minimizedChatsRef = useRef(minimizedChats); @@ -573,7 +574,7 @@ export default function ChatContainer() {
closeChat(chat.username)} onMinimize={() => toggleMinimize(chat.username)} + showSidebar={showSidebar} + friends={friends} + presence={presence} + unreadCounts={unreadCounts} + onSelectFriend={openChat} + onToggleSidebar={() => setShowSidebar(!showSidebar)} />
))} diff --git a/src/components/Chat/ChatWindow.css b/src/components/Chat/ChatWindow.css index 866dc9b..968783c 100644 --- a/src/components/Chat/ChatWindow.css +++ b/src/components/Chat/ChatWindow.css @@ -1,3 +1,14 @@ +/* Outer wrapper for chat + sidebar */ +.chat-window-outer { + display: flex; + flex-direction: row-reverse; + align-items: flex-end; +} + +.chat-window-outer.with-sidebar { + gap: 0; +} + .chat-window { width: 320px; height: 420px; @@ -11,6 +22,134 @@ backdrop-filter: blur(12px); } +.chat-window-outer.with-sidebar .chat-window { + border-radius: 0 12px 0 0; +} + +/* Sidebar toggle button */ +.chat-sidebar-toggle { + width: 26px; + height: 26px; + border: none; + border-radius: 50%; + background: rgba(255, 255, 255, 0.1); + color: #e2e8f0; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 11px; + transition: background 0.2s, color 0.2s; +} + +.chat-sidebar-toggle:hover { + background: rgba(255, 255, 255, 0.2); +} + +.chat-sidebar-toggle.active { + background: rgba(59, 130, 246, 0.4); + color: #60a5fa; +} + +/* Friends Sidebar */ +.chat-sidebar { + width: 56px; + height: 420px; + background: rgba(15, 23, 42, 0.95); + border: 1px solid rgba(148, 163, 184, 0.3); + border-right: none; + border-radius: 12px 0 0 0; + backdrop-filter: blur(12px); + display: flex; + flex-direction: column; +} + +.chat-sidebar-list { + flex: 1; + overflow-y: auto; + padding: 8px 6px; + display: flex; + flex-direction: column; + gap: 6px; +} + +.chat-sidebar-item { + width: 42px; + height: 42px; + border: none; + border-radius: 10px; + background: transparent; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + position: relative; + transition: background 0.2s; + padding: 0; +} + +.chat-sidebar-item:hover { + background: rgba(59, 130, 246, 0.15); +} + +.chat-sidebar-item.active { + background: rgba(59, 130, 246, 0.25); +} + +.chat-sidebar-item.has-unread { + background: rgba(239, 68, 68, 0.15); +} + +.chat-sidebar-avatar { + width: 36px; + height: 36px; + border-radius: 50%; + background: linear-gradient(135deg, #3b82f6, #8b5cf6); + display: flex; + align-items: center; + justify-content: center; + color: white; + font-weight: 600; + font-size: 14px; + position: relative; + overflow: hidden; +} + +.chat-sidebar-avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.chat-sidebar-avatar.online::after { + content: ""; + position: absolute; + bottom: 0; + right: 0; + width: 10px; + height: 10px; + background: #22c55e; + border: 2px solid rgba(15, 23, 42, 0.95); + border-radius: 50%; +} + +.chat-sidebar-unread { + position: absolute; + top: 0; + right: 0; + min-width: 16px; + height: 16px; + padding: 0 4px; + background: #ef4444; + color: white; + font-size: 10px; + font-weight: 700; + border-radius: 999px; + display: flex; + align-items: center; + justify-content: center; +} + .chat-window--minimized { height: auto; cursor: pointer; @@ -306,8 +445,30 @@ body[data-theme="light"] .chat-input { border-color: rgba(148, 163, 184, 0.4); } +/* Light theme sidebar */ +body[data-theme="light"] .chat-sidebar { + background: rgba(255, 255, 255, 0.98); + border-color: rgba(148, 163, 184, 0.4); +} + +body[data-theme="light"] .chat-sidebar-item:hover { + background: rgba(59, 130, 246, 0.1); +} + +body[data-theme="light"] .chat-sidebar-item.active { + background: rgba(59, 130, 246, 0.15); +} + +body[data-theme="light"] .chat-sidebar-avatar.online::after { + border-color: rgba(255, 255, 255, 0.98); +} + /* Responsive */ @media (max-width: 640px) { + .chat-window-outer { + width: 100%; + } + .chat-window { width: 100%; max-width: 100vw; @@ -318,6 +479,19 @@ body[data-theme="light"] .chat-input { border-radius: 16px 16px 0 0; } + .chat-window-outer.with-sidebar .chat-window { + border-radius: 16px 16px 0 0; + } + + /* Hide sidebar on mobile */ + .chat-sidebar { + display: none; + } + + .chat-sidebar-toggle { + display: none; + } + .chat-input-area { padding-bottom: max(10px, env(safe-area-inset-bottom, 10px)); } diff --git a/src/components/Chat/ChatWindow.jsx b/src/components/Chat/ChatWindow.jsx index c69b672..e9f37fa 100644 --- a/src/components/Chat/ChatWindow.jsx +++ b/src/components/Chat/ChatWindow.jsx @@ -15,6 +15,13 @@ export default function ChatWindow({ onClose, onMinimize, minimized = false, + // Sidebar props + showSidebar = false, + friends = [], + presence = {}, + unreadCounts = {}, + onSelectFriend, + onToggleSidebar, }) { const { t } = useTranslation(); const [input, setInput] = useState(""); @@ -106,113 +113,158 @@ export default function ChatWindow({ } return ( -
- {/* Header */} -
-
-
- {recipientAvatar ? ( - {recipientName} - ) : ( - {(recipientName || "?")[0].toUpperCase()} - )} +
+
+ {/* Header */} +
+
+
+ {recipientAvatar ? ( + {recipientName} + ) : ( + {(recipientName || "?")[0].toUpperCase()} + )} +
+
+ {recipientName} + + {online ? t("common.online") : ""} + +
-
- {recipientName} - - {online ? t("common.online") : ""} - -
-
-
- - -
-
- - {/* Messages */} -
- {messages.length === 0 ? ( -
- {t("chat.startConversation", "Start a conversation")} -
- ) : ( - messages.map((msg, idx) => { - // Compare by username (sender_name), case-insensitive - const isMine = msg.sender_name?.toLowerCase() !== recipientName?.toLowerCase(); - // Determine message status for sent messages - const isSending = msg.id?.toString().startsWith("temp-"); - const isRead = msg.read_at != null; - const isSent = !isSending && msg.id; - - return ( -
+ {onToggleSidebar && ( +
- ); - }) - )} - {typing && ( -
- - - + + + )} + +
- )} -
+
+ + {/* Messages */} +
+ {messages.length === 0 ? ( +
+ {t("chat.startConversation", "Start a conversation")} +
+ ) : ( + messages.map((msg, idx) => { + // Compare by username (sender_name), case-insensitive + const isMine = msg.sender_name?.toLowerCase() !== recipientName?.toLowerCase(); + // Determine message status for sent messages + const isSending = msg.id?.toString().startsWith("temp-"); + const isRead = msg.read_at != null; + const isSent = !isSending && msg.id; + + return ( +
+
{msg.content}
+
+ {formatTime(msg.created_at)} + {isMine && ( + + {isSending ? ( + + ) : isRead ? ( + + ) : isSent ? ( + + ) : null} + + )} +
+
+ ); + }) + )} + {typing && ( +
+ + + +
+ )} +
+
+ + {/* Input */} +
+ + +
- {/* Input */} -
- - -
+ {/* Friends Sidebar */} + {showSidebar && ( +
+
+ {friends.map((friend) => { + const friendUsername = friend.username || friend.name; + const avatar = friend.avatar_url || friend.avatar; + const isOnline = presence[friendUsername] || false; + const unread = unreadCounts[friendUsername] || 0; + const isActive = friendUsername === recipientName; + + return ( + + ); + })} +
+
+ )}
); } diff --git a/src/components/Map/RadialMenu.jsx b/src/components/Map/RadialMenu.jsx index 1ccd7ad..3cb9eae 100644 --- a/src/components/Map/RadialMenu.jsx +++ b/src/components/Map/RadialMenu.jsx @@ -82,6 +82,7 @@ export default function RadialMenu({ const handleTimeClick = (hours) => { onTime?.(hours); + setRightOpen(false); // Close menu after selection }; const handleWeatherClick = () => { @@ -206,7 +207,7 @@ export default function RadialMenu({
{/* RIGHT FAB - Time & Weather */} -
+
{/* Time options */} {TIME_OPTIONS.map((t, i) => { diff --git a/src/components/Map/markerManager.js b/src/components/Map/markerManager.js index 6c68cc7..471bffd 100644 --- a/src/components/Map/markerManager.js +++ b/src/components/Map/markerManager.js @@ -567,7 +567,7 @@ function openImageLightbox(src) { overlay.style.position = "fixed"; overlay.style.inset = "0"; overlay.style.background = "rgba(8,10,16,0.86)"; - overlay.style.zIndex = "10000000"; + overlay.style.zIndex = "6000"; /* Image lightbox - above post modal (5000), below auth (10000) */ overlay.style.display = "flex"; overlay.style.alignItems = "center"; overlay.style.justifyContent = "center"; @@ -997,7 +997,7 @@ function openCameraOverlay({ map, post, theme, fullScreen, interactionState }) { backdrop.style.cssText = ` position: fixed; inset: 0; - z-index: 9999999; + z-index: 5000; background: rgba(0,0,0,0.85); display: flex; align-items: center; @@ -1218,7 +1218,7 @@ export function openCenteredOverlay({ map, post, theme, fullScreen = false }) { if (fullScreen) backdrop.classList.add("sw-modal-fullscreen"); backdrop.style.position = "fixed"; backdrop.style.inset = "0"; - backdrop.style.zIndex = "9999999"; + backdrop.style.zIndex = "5000"; backdrop.style.background = "rgba(0,0,0,0.08)"; backdrop.style.backdropFilter = "blur(1px)"; backdrop.style.display = "flex"; diff --git a/src/components/Map/markerMedia.js b/src/components/Map/markerMedia.js index 8d2afd4..2d4c7b5 100644 --- a/src/components/Map/markerMedia.js +++ b/src/components/Map/markerMedia.js @@ -194,7 +194,7 @@ export function openImageLightbox(src) { overlay.style.position = "fixed"; overlay.style.inset = "0"; overlay.style.background = "rgba(8,10,16,0.86)"; - overlay.style.zIndex = "10000000"; + overlay.style.zIndex = "6000"; /* Image lightbox - above post modal (5000), below auth (10000) */ overlay.style.display = "flex"; overlay.style.alignItems = "center"; overlay.style.justifyContent = "center"; diff --git a/src/components/Map/usePostsEngine.js b/src/components/Map/usePostsEngine.js index 4a41cd0..20b2db1 100644 --- a/src/components/Map/usePostsEngine.js +++ b/src/components/Map/usePostsEngine.js @@ -1,6 +1,6 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { fetchPosts, fetchSmartFeed, fetchUnifiedSearch, resolveAssetRef } from "../../api/client"; -import { categoryCode, matchesSubFilter } from "./mapFilter"; +import { categoryCode, matchesSubFilter, matchesTimeFilter, effectivePostTime } from "./mapFilter"; import { getCoords, getViewFromMap, haversineKm } from "./mapGeo"; import { createMarkerForPost, createSimpleMarkerForPost } from "./markerManager"; import { getTemplateKeyForPost } from "./templateSpecs"; @@ -875,6 +875,12 @@ export function usePostsEngine({ // Apply content filter (links, video, image, live, event) if (!matchesContentFilter(p, contentFilter)) return false; + // Apply time filter + if (tf && tf > 0) { + const postTime = effectivePostTime(p); + if (!matchesTimeFilter(postTime, tf)) return false; + } + if (isClusterFocus) { const id = Number(getId(p)); const clusterPostId = Number(p?.cluster_post_id); @@ -1521,12 +1527,14 @@ export function usePostsEngine({ const handlePostDelete = useCallback( (payload) => { - const postId = payload?.post_id || payload?.postId || payload?.id; - if (!postId) return; + const rawId = payload?.post_id || payload?.postId || payload?.id; + if (!rawId) return; + const postId = Number(rawId); // Ensure numeric comparison + console.log("[PostsEngine] handlePostDelete postId=", postId); const map = mapRef.current; const overlay = map?.__swCenteredOverlay; - if (overlay && overlay.postId === postId) { + if (overlay && Number(overlay.postId) === postId) { // Remove event listeners try { if (overlay.onKey) window.removeEventListener("keydown", overlay.onKey); } catch {} try { if (overlay.onAuth) window.removeEventListener("sociowire:auth-changed", overlay.onAuth); } catch {} @@ -1544,13 +1552,13 @@ export function usePostsEngine({ map.__swCenteredOverlay = null; } - allPostsRef.current = (allPostsRef.current || []).filter((p) => (p?.id || p?.ID) !== postId); - setVisiblePosts((current) => current.filter((p) => (p?.id || p?.ID) !== postId)); + allPostsRef.current = (allPostsRef.current || []).filter((p) => Number(p?.id || p?.ID) !== postId); + setVisiblePosts((current) => current.filter((p) => Number(p?.id || p?.ID) !== postId)); const markers = markersRef.current || []; const nextMarkers = []; for (const item of markers) { - if (!item || item.id !== postId) { + if (!item || Number(item.id) !== postId) { nextMarkers.push(item); continue; } diff --git a/src/components/Posts/PostCard.jsx b/src/components/Posts/PostCard.jsx index b8eeae6..82c2908 100644 --- a/src/components/Posts/PostCard.jsx +++ b/src/components/Posts/PostCard.jsx @@ -157,7 +157,7 @@ export default function PostCard({ post, selected, onSelect, onPostDeleted, onPo comment: post?.comment_count ?? 0, share: post?.share_count ?? 0, })); - const { username, token } = useAuth(); + const { username, token, userId } = useAuth(); // Edit/Delete state const [showEditDelete, setShowEditDelete] = useState(false); @@ -232,8 +232,11 @@ export default function PostCard({ post, selected, onSelect, onPostDeleted, onPo const title = post?.title || "Untitled"; const snippet = post?.snippet || ""; const author = (post?.author || post?.Author || "anon").toString(); + const authorId = post?.author_id || post?.authorId || null; + // Check if current user is the author (by username OR by user ID) const isAuthor = - username && author && author.toLowerCase() === username.toLowerCase(); + (username && author && author.toLowerCase() === username.toLowerCase()) || + (userId && authorId && Number(userId) === Number(authorId)); const [authorAvatar, setAuthorAvatar] = useState(""); const sub = (post?.sub_category || post?.subCategory || "").toString(); const contentType = (post?.content_type || "").toString().toLowerCase(); diff --git a/src/styles/auth-modal.css b/src/styles/auth-modal.css index d9a598e..68ca6b8 100644 --- a/src/styles/auth-modal.css +++ b/src/styles/auth-modal.css @@ -1,12 +1,13 @@ .auth-modal-backdrop { position: fixed; inset: 0; - z-index: 10000; /* Auth modal - highest priority */ + z-index: 10000; /* Auth modal - above post modals (5000) and chat (8000) */ display: flex; align-items: center; justify-content: center; background: rgba(2, 6, 23, 0.65); backdrop-filter: blur(8px); + isolation: isolate; /* Create new stacking context */ } .auth-modal {