Repair: copy legacy joinDate into new user's created_at when creating users from legacy wallz
This commit is contained in:
@@ -61,10 +61,12 @@ function buildSearchPreview(item) {
|
||||
|
||||
function MessagesPage({ userId, username, activeConversationId: initialId }) {
|
||||
const [conversations, setConversations] = useState([])
|
||||
const [unreadTotal, setUnreadTotal] = useState(null)
|
||||
const [loadingConvs, setLoadingConvs] = useState(true)
|
||||
const [activeId, setActiveId] = useState(initialId ?? null)
|
||||
const [realtimeEnabled, setRealtimeEnabled] = useState(false)
|
||||
const [realtimeStatus, setRealtimeStatus] = useState('offline')
|
||||
const [onlineUserIds, setOnlineUserIds] = useState([])
|
||||
const [typingByConversation, setTypingByConversation] = useState({})
|
||||
const [showNewModal, setShowNewModal] = useState(false)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
@@ -75,6 +77,7 @@ function MessagesPage({ userId, username, activeConversationId: initialId }) {
|
||||
try {
|
||||
const data = await apiFetch('/api/messages/conversations')
|
||||
setConversations(data.data ?? [])
|
||||
setUnreadTotal(Number.isFinite(Number(data?.summary?.unread_total)) ? Number(data.summary.unread_total) : null)
|
||||
} catch (e) {
|
||||
console.error('Failed to load conversations', e)
|
||||
} finally {
|
||||
@@ -173,6 +176,11 @@ function MessagesPage({ userId, username, activeConversationId: initialId }) {
|
||||
}
|
||||
|
||||
setConversations((prev) => mergeConversationSummary(prev, nextConversation))
|
||||
|
||||
const nextUnreadTotal = Number(payload?.summary?.unread_total)
|
||||
if (Number.isFinite(nextUnreadTotal)) {
|
||||
setUnreadTotal(nextUnreadTotal)
|
||||
}
|
||||
}
|
||||
|
||||
channel.listen('.conversation.updated', handleConversationUpdated)
|
||||
@@ -192,6 +200,79 @@ function MessagesPage({ userId, username, activeConversationId: initialId }) {
|
||||
}
|
||||
}, [realtimeEnabled, userId])
|
||||
|
||||
useEffect(() => {
|
||||
if (!realtimeEnabled || !userId) {
|
||||
setOnlineUserIds([])
|
||||
return undefined
|
||||
}
|
||||
|
||||
const echo = getEcho()
|
||||
if (!echo) {
|
||||
setOnlineUserIds([])
|
||||
return undefined
|
||||
}
|
||||
|
||||
const setMembers = (users) => {
|
||||
const nextIds = (users ?? [])
|
||||
.map((user) => Number(user?.id))
|
||||
.filter((id) => Number.isFinite(id) && id !== Number(userId))
|
||||
|
||||
setOnlineUserIds(Array.from(new Set(nextIds)))
|
||||
}
|
||||
|
||||
const channel = echo.join('messaging')
|
||||
channel
|
||||
.here(setMembers)
|
||||
.joining((user) => setOnlineUserIds((prev) => (
|
||||
prev.includes(Number(user?.id)) || Number(user?.id) === Number(userId)
|
||||
? prev
|
||||
: [...prev, Number(user.id)]
|
||||
)))
|
||||
.leaving((user) => setOnlineUserIds((prev) => prev.filter((id) => id !== Number(user?.id))))
|
||||
|
||||
return () => {
|
||||
echo.leave('messaging')
|
||||
}
|
||||
}, [realtimeEnabled, userId])
|
||||
|
||||
useEffect(() => {
|
||||
if (!userId) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
let intervalId = null
|
||||
|
||||
const sendHeartbeat = () => {
|
||||
if (document.visibilityState === 'hidden') {
|
||||
return
|
||||
}
|
||||
|
||||
apiFetch('/api/messages/presence/heartbeat', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(activeId ? { conversation_id: activeId } : {}),
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
const handleVisibilitySync = () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
sendHeartbeat()
|
||||
}
|
||||
}
|
||||
|
||||
sendHeartbeat()
|
||||
intervalId = window.setInterval(sendHeartbeat, 25000)
|
||||
window.addEventListener('focus', sendHeartbeat)
|
||||
document.addEventListener('visibilitychange', handleVisibilitySync)
|
||||
|
||||
return () => {
|
||||
if (intervalId) {
|
||||
window.clearInterval(intervalId)
|
||||
}
|
||||
window.removeEventListener('focus', sendHeartbeat)
|
||||
document.removeEventListener('visibilitychange', handleVisibilitySync)
|
||||
}
|
||||
}, [activeId, userId])
|
||||
|
||||
useEffect(() => {
|
||||
if (!realtimeEnabled) {
|
||||
setTypingByConversation({})
|
||||
@@ -310,12 +391,16 @@ function MessagesPage({ userId, username, activeConversationId: initialId }) {
|
||||
history.replaceState(null, '', `/messages/${conv.id}`)
|
||||
}, [loadConversations])
|
||||
|
||||
const handleMarkRead = useCallback((conversationId) => {
|
||||
const handleMarkRead = useCallback((conversationId, nextUnreadTotal = null) => {
|
||||
setConversations((prev) => prev.map((conversation) => (
|
||||
conversation.id === conversationId
|
||||
? { ...conversation, unread_count: 0 }
|
||||
: conversation
|
||||
)))
|
||||
|
||||
if (Number.isFinite(Number(nextUnreadTotal))) {
|
||||
setUnreadTotal(Number(nextUnreadTotal))
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleConversationPatched = useCallback((patch) => {
|
||||
@@ -369,7 +454,9 @@ function MessagesPage({ userId, username, activeConversationId: initialId }) {
|
||||
}, [])
|
||||
|
||||
const activeConversation = conversations.find((conversation) => conversation.id === activeId) ?? null
|
||||
const unreadCount = conversations.reduce((sum, conversation) => sum + Number(conversation.unread_count || 0), 0)
|
||||
const unreadCount = Number.isFinite(Number(unreadTotal))
|
||||
? Number(unreadTotal)
|
||||
: conversations.reduce((sum, conversation) => sum + Number(conversation.unread_count || 0), 0)
|
||||
const pinnedCount = conversations.reduce((sum, conversation) => {
|
||||
const me = conversation.my_participant ?? conversation.all_participants?.find((participant) => participant.user_id === userId)
|
||||
return sum + (me?.is_pinned ? 1 : 0)
|
||||
@@ -475,6 +562,7 @@ function MessagesPage({ userId, username, activeConversationId: initialId }) {
|
||||
loading={loadingConvs}
|
||||
activeId={activeId}
|
||||
currentUserId={userId}
|
||||
onlineUserIds={onlineUserIds}
|
||||
typingByConversation={typingByConversation}
|
||||
onSelect={handleSelectConversation}
|
||||
/>
|
||||
@@ -490,6 +578,7 @@ function MessagesPage({ userId, username, activeConversationId: initialId }) {
|
||||
realtimeStatus={realtimeStatus}
|
||||
currentUserId={userId}
|
||||
currentUsername={username}
|
||||
onlineUserIds={onlineUserIds}
|
||||
apiFetch={apiFetch}
|
||||
onBack={() => {
|
||||
setActiveId(null)
|
||||
|
||||
@@ -66,10 +66,9 @@ export default function ProfileGallery() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto w-full max-w-6xl px-4 pt-6 md:px-6">
|
||||
<div className="w-full pt-6">
|
||||
<ProfileGalleryPanel
|
||||
artworks={artworks}
|
||||
featuredArtworks={featuredArtworks}
|
||||
username={username}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { usePage } from '@inertiajs/react'
|
||||
import ProfileHero from '../../components/profile/ProfileHero'
|
||||
import ProfileStatsRow from '../../components/profile/ProfileStatsRow'
|
||||
import ProfileTabs from '../../components/profile/ProfileTabs'
|
||||
import TabArtworks from '../../components/profile/tabs/TabArtworks'
|
||||
import TabAchievements from '../../components/profile/tabs/TabAchievements'
|
||||
@@ -13,16 +12,26 @@ import TabActivity from '../../components/profile/tabs/TabActivity'
|
||||
import TabPosts from '../../components/profile/tabs/TabPosts'
|
||||
import TabStories from '../../components/profile/tabs/TabStories'
|
||||
|
||||
const VALID_TABS = ['artworks', 'stories', 'achievements', 'posts', 'collections', 'about', 'stats', 'favourites', 'activity']
|
||||
const VALID_TABS = ['posts', 'artworks', 'stories', 'achievements', 'collections', 'about', 'stats', 'favourites', 'activity']
|
||||
|
||||
function getInitialTab() {
|
||||
try {
|
||||
const sp = new URLSearchParams(window.location.search)
|
||||
const t = sp.get('tab')
|
||||
return VALID_TABS.includes(t) ? t : 'artworks'
|
||||
} catch {
|
||||
return 'artworks'
|
||||
function getInitialTab(initialTab = 'posts') {
|
||||
if (typeof window === 'undefined') {
|
||||
return VALID_TABS.includes(initialTab) ? initialTab : 'posts'
|
||||
}
|
||||
|
||||
try {
|
||||
const pathname = window.location.pathname.replace(/\/+$/, '')
|
||||
const segments = pathname.split('/').filter(Boolean)
|
||||
const lastSegment = segments.at(-1)
|
||||
|
||||
if (VALID_TABS.includes(lastSegment)) {
|
||||
return lastSegment
|
||||
}
|
||||
} catch {
|
||||
return VALID_TABS.includes(initialTab) ? initialTab : 'posts'
|
||||
}
|
||||
|
||||
return VALID_TABS.includes(initialTab) ? initialTab : 'posts'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,34 +61,37 @@ export default function ProfileShow() {
|
||||
countryName,
|
||||
isOwner,
|
||||
auth,
|
||||
initialTab,
|
||||
profileUrl,
|
||||
galleryUrl,
|
||||
profileTabUrls,
|
||||
} = props
|
||||
|
||||
const [activeTab, setActiveTab] = useState(getInitialTab)
|
||||
const [activeTab, setActiveTab] = useState(() => getInitialTab(initialTab))
|
||||
|
||||
const handleTabChange = useCallback((tab) => {
|
||||
if (!VALID_TABS.includes(tab)) return
|
||||
setActiveTab(tab)
|
||||
|
||||
// Update URL query param without full navigation
|
||||
try {
|
||||
const url = new URL(window.location.href)
|
||||
if (tab === 'artworks') {
|
||||
url.searchParams.delete('tab')
|
||||
} else {
|
||||
url.searchParams.set('tab', tab)
|
||||
}
|
||||
window.history.pushState({}, '', url.toString())
|
||||
} catch (_) {}
|
||||
}, [])
|
||||
const currentUrl = new URL(window.location.href)
|
||||
const targetBase = profileTabUrls?.[tab] || `${profileUrl || `${window.location.origin}`}/${tab}`
|
||||
const nextUrl = new URL(targetBase, window.location.origin)
|
||||
const sharedPostId = currentUrl.searchParams.get('post')
|
||||
|
||||
if (sharedPostId) {
|
||||
nextUrl.searchParams.set('post', sharedPostId)
|
||||
}
|
||||
|
||||
window.history.pushState({}, '', nextUrl.toString())
|
||||
} catch (_) {}
|
||||
}, [profileTabUrls, profileUrl])
|
||||
|
||||
// Handle browser back/forward
|
||||
useEffect(() => {
|
||||
const onPop = () => setActiveTab(getInitialTab())
|
||||
const onPop = () => setActiveTab(getInitialTab(initialTab))
|
||||
window.addEventListener('popstate', onPop)
|
||||
return () => window.removeEventListener('popstate', onPop)
|
||||
}, [])
|
||||
}, [initialTab])
|
||||
|
||||
const isLoggedIn = !!(auth?.user)
|
||||
|
||||
@@ -98,9 +110,27 @@ export default function ProfileShow() {
|
||||
? socialLinks.reduce((acc, l) => { acc[l.platform] = l; return acc }, {})
|
||||
: (socialLinks ?? {})
|
||||
|
||||
const contentShellClassName = activeTab === 'artworks'
|
||||
? 'w-full px-4 md:px-6'
|
||||
: activeTab === 'posts'
|
||||
? 'mx-auto max-w-7xl px-4 md:px-6'
|
||||
: 'max-w-6xl mx-auto px-4'
|
||||
|
||||
return (
|
||||
<div className="min-h-screen pb-16">
|
||||
{/* Hero section */}
|
||||
<div className="relative min-h-screen overflow-hidden pb-16">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-x-0 top-0 -z-10 h-[34rem] opacity-90"
|
||||
style={{
|
||||
background: 'radial-gradient(circle at top left, rgba(56,189,248,0.18), transparent 32%), radial-gradient(circle at 82% 10%, rgba(249,115,22,0.16), transparent 28%), linear-gradient(180deg, #07101d 0%, #0a1220 42%, #0a1220 100%)',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-0 -z-10 opacity-[0.06]"
|
||||
style={{ backgroundImage: 'url(/gfx/noise.png)', backgroundSize: '180px' }}
|
||||
/>
|
||||
|
||||
<ProfileHero
|
||||
user={user}
|
||||
profile={profile}
|
||||
@@ -121,26 +151,20 @@ export default function ProfileShow() {
|
||||
) : null}
|
||||
/>
|
||||
|
||||
{/* Stats pills row */}
|
||||
<ProfileStatsRow
|
||||
stats={stats}
|
||||
followerCount={followerCount}
|
||||
onTabChange={handleTabChange}
|
||||
/>
|
||||
<div className="mt-6">
|
||||
<ProfileTabs
|
||||
activeTab={activeTab}
|
||||
onTabChange={handleTabChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Sticky tabs */}
|
||||
<ProfileTabs
|
||||
activeTab={activeTab}
|
||||
onTabChange={handleTabChange}
|
||||
/>
|
||||
|
||||
{/* Tab content area */}
|
||||
<div className={activeTab === 'artworks' ? 'w-full px-4 md:px-6' : 'max-w-6xl mx-auto px-4'}>
|
||||
<div className={`${contentShellClassName} pt-6`}>
|
||||
{activeTab === 'artworks' && (
|
||||
<TabArtworks
|
||||
artworks={{ data: artworkList, next_cursor: artworkNextCursor }}
|
||||
featuredArtworks={featuredArtworks}
|
||||
username={user.username || user.name}
|
||||
galleryUrl={galleryUrl}
|
||||
isActive
|
||||
/>
|
||||
)}
|
||||
@@ -156,6 +180,7 @@ export default function ProfileShow() {
|
||||
recentFollowers={recentFollowers}
|
||||
socialLinks={socialLinksObj}
|
||||
countryName={countryName}
|
||||
profileUrl={profileUrl}
|
||||
onTabChange={handleTabChange}
|
||||
/>
|
||||
)}
|
||||
@@ -175,9 +200,16 @@ export default function ProfileShow() {
|
||||
<TabAbout
|
||||
user={user}
|
||||
profile={profile}
|
||||
stats={stats}
|
||||
achievements={achievements}
|
||||
artworks={artworkList}
|
||||
creatorStories={creatorStories}
|
||||
profileComments={profileComments}
|
||||
socialLinks={socialLinksObj}
|
||||
countryName={countryName}
|
||||
followerCount={followerCount}
|
||||
recentFollowers={recentFollowers}
|
||||
leaderboardRank={leaderboardRank}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'stats' && (
|
||||
|
||||
Reference in New Issue
Block a user