feat(export): add sns count and timeline popup in session detail

This commit is contained in:
aits2026
2026-03-05 18:08:09 +08:00
parent 9dd5ee2365
commit c301f36912
2 changed files with 643 additions and 3 deletions

View File

@@ -120,7 +120,7 @@
}
.session-load-detail-modal {
width: min(760px, 100%);
width: min(820px, 100%);
max-height: min(78vh, 860px);
overflow: hidden;
border-radius: 14px;
@@ -200,14 +200,14 @@
.session-load-detail-row {
display: grid;
grid-template-columns: 1.1fr 1fr 0.8fr 0.8fr;
grid-template-columns: minmax(76px, 0.78fr) minmax(260px, 1.55fr) minmax(84px, 0.74fr) minmax(84px, 0.74fr);
gap: 10px;
align-items: center;
padding: 9px 12px;
font-size: 12px;
color: var(--text-secondary);
border-bottom: 1px solid color-mix(in srgb, var(--border-color) 66%, transparent);
min-width: 540px;
min-width: 620px;
&:last-child {
border-bottom: none;
@@ -230,10 +230,14 @@
.session-load-detail-status-cell {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-start;
gap: 6px;
min-width: 0;
overflow: visible !important;
text-overflow: clip !important;
white-space: normal !important;
}
.session-load-detail-status-icon {
@@ -245,6 +249,7 @@
color: var(--text-tertiary);
font-size: 11px;
font-variant-numeric: tabular-nums;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
letter-spacing: 0.1px;
flex-shrink: 0;
}
@@ -2038,6 +2043,10 @@
}
}
.detail-sns-entry-btn {
white-space: nowrap;
}
.copy-btn {
display: flex;
align-items: center;
@@ -2149,6 +2158,218 @@
}
}
.export-session-sns-overlay {
position: fixed;
inset: 0;
z-index: 1200;
display: flex;
align-items: center;
justify-content: center;
padding: 24px 16px;
background: rgba(15, 23, 42, 0.38);
}
.export-session-sns-dialog {
width: min(760px, 100%);
max-height: min(86vh, 860px);
border-radius: 14px;
border: 1px solid var(--border-color);
background: var(--bg-secondary-solid, #ffffff);
box-shadow: 0 22px 46px rgba(0, 0, 0, 0.24);
display: flex;
flex-direction: column;
overflow: hidden;
.sns-dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 14px 16px;
border-bottom: 1px solid var(--border-color);
}
.sns-dialog-header-main {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
}
.sns-dialog-avatar {
width: 42px;
height: 42px;
border-radius: 10px;
background: linear-gradient(135deg, var(--primary), var(--primary-hover));
overflow: hidden;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
span {
color: #fff;
font-size: 14px;
font-weight: 600;
}
}
.sns-dialog-meta {
min-width: 0;
h4 {
margin: 0;
font-size: 15px;
color: var(--text-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.sns-dialog-username {
margin-top: 2px;
font-size: 12px;
color: var(--text-tertiary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sns-dialog-stats {
margin-top: 4px;
font-size: 12px;
color: var(--text-secondary);
}
.close-btn {
border: none;
background: transparent;
color: var(--text-secondary);
width: 28px;
height: 28px;
border-radius: 7px;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
&:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
}
.sns-dialog-body {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 12px 14px 14px;
}
.sns-post-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.sns-post-card {
border: 1px solid var(--border-color);
background: var(--bg-primary);
border-radius: 10px;
padding: 10px 11px;
}
.sns-post-time {
font-size: 12px;
color: var(--text-tertiary);
margin-bottom: 6px;
}
.sns-post-content {
white-space: pre-wrap;
word-break: break-word;
font-size: 13px;
color: var(--text-primary);
line-height: 1.55;
}
.sns-post-media-grid {
margin-top: 8px;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 6px;
}
.sns-post-media-item {
border: none;
padding: 0;
border-radius: 8px;
overflow: hidden;
background: var(--bg-secondary);
position: relative;
cursor: pointer;
aspect-ratio: 1 / 1;
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
}
.sns-post-media-video-tag {
position: absolute;
right: 6px;
bottom: 6px;
background: rgba(0, 0, 0, 0.64);
color: #fff;
border-radius: 5px;
font-size: 11px;
line-height: 1;
padding: 3px 5px;
}
.sns-dialog-status {
padding: 16px 0;
text-align: center;
color: var(--text-secondary);
font-size: 13px;
&.empty {
color: var(--text-tertiary);
}
}
.sns-dialog-load-more {
display: block;
margin: 12px auto 0;
border: 1px solid var(--border-color);
background: var(--bg-primary);
color: var(--text-primary);
border-radius: 8px;
padding: 8px 16px;
font-size: 13px;
cursor: pointer;
&:disabled {
opacity: 0.7;
cursor: not-allowed;
}
&:hover:not(:disabled) {
background: var(--bg-hover);
}
}
}
.table-state {
display: flex;
align-items: center;
@@ -2862,6 +3083,15 @@
margin-left: 0;
}
.session-load-detail-modal {
width: min(94vw, 820px);
}
.session-load-detail-row {
grid-template-columns: minmax(68px, 0.72fr) minmax(232px, 1.6fr) minmax(80px, 0.72fr) minmax(80px, 0.72fr);
min-width: 560px;
}
.table-wrap {
--contacts-message-col-width: 104px;
--contacts-media-col-width: 62px;
@@ -2961,4 +3191,21 @@
.export-session-detail-panel {
width: calc(100vw - 12px);
}
.export-session-sns-overlay {
padding: 12px 8px;
}
.export-session-sns-dialog {
width: min(100vw - 16px, 760px);
max-height: calc(100vh - 24px);
.sns-dialog-header {
padding: 12px;
}
.sns-dialog-body {
padding: 10px 10px 12px;
}
}
}

View File

@@ -38,6 +38,7 @@ import {
onOpenSingleExport
} from '../services/exportBridge'
import { useContactTypeCountsStore } from '../stores/contactTypeCountsStore'
import type { SnsPost } from '../types/sns'
import './ExportPage.scss'
type ConversationTab = 'private' | 'group' | 'official' | 'former_friend'
@@ -422,6 +423,20 @@ const formatYmdHmDateTime = (timestamp?: number): string => {
return `${y}-${m}-${day} ${h}:${min}`
}
const isSingleContactSession = (sessionId: string): boolean => {
const normalized = String(sessionId || '').trim()
if (!normalized) return false
if (normalized.includes('@chatroom')) return false
if (normalized.startsWith('gh_')) return false
return true
}
const isSnsVideoMediaUrl = (url?: string): boolean => {
if (!url) return false
const lower = url.toLowerCase()
return (lower.includes('snsvideodownload') || lower.includes('.mp4') || lower.includes('video')) && !lower.includes('vweixinthumb')
}
const formatPathBrief = (value: string, maxLength = 52): string => {
const normalized = String(value || '')
if (normalized.length <= maxLength) return normalized
@@ -661,6 +676,12 @@ interface SessionDetail {
messageTables: { dbName: string; tableName: string; count: number }[]
}
interface SessionSnsTimelineTarget {
username: string
displayName: string
avatarUrl?: string
}
interface SessionExportMetric {
totalMessages: number
voiceMessages: number
@@ -1302,6 +1323,15 @@ function ExportPage() {
const [isRefreshingSessionDetailStats, setIsRefreshingSessionDetailStats] = useState(false)
const [isLoadingSessionRelationStats, setIsLoadingSessionRelationStats] = useState(false)
const [copiedDetailField, setCopiedDetailField] = useState<string | null>(null)
const [snsUserPostCounts, setSnsUserPostCounts] = useState<Record<string, number>>({})
const [snsUserPostCountsStatus, setSnsUserPostCountsStatus] = useState<'idle' | 'loading' | 'ready' | 'error'>('idle')
const [sessionSnsTimelineTarget, setSessionSnsTimelineTarget] = useState<SessionSnsTimelineTarget | null>(null)
const [sessionSnsTimelinePosts, setSessionSnsTimelinePosts] = useState<SnsPost[]>([])
const [sessionSnsTimelineLoading, setSessionSnsTimelineLoading] = useState(false)
const [sessionSnsTimelineLoadingMore, setSessionSnsTimelineLoadingMore] = useState(false)
const [sessionSnsTimelineHasMore, setSessionSnsTimelineHasMore] = useState(false)
const [sessionSnsTimelineTotalPosts, setSessionSnsTimelineTotalPosts] = useState<number | null>(null)
const [sessionSnsTimelineStatsLoading, setSessionSnsTimelineStatsLoading] = useState(false)
const [exportFolder, setExportFolder] = useState('')
const [writeLayout, setWriteLayout] = useState<configService.ExportWriteLayout>('B')
@@ -1391,6 +1421,9 @@ function ExportPage() {
const isLoadingSessionCountsRef = useRef(false)
const activeTabRef = useRef<ConversationTab>('private')
const detailStatsPriorityRef = useRef(false)
const sessionSnsTimelinePostsRef = useRef<SnsPost[]>([])
const sessionSnsTimelineLoadingRef = useRef(false)
const sessionSnsTimelineRequestTokenRef = useRef(0)
const sessionPreciseRefreshAtRef = useRef<Record<string, number>>({})
const sessionLoadProgressSnapshotRef = useRef<Record<string, { loaded: number; total: number }>>({})
const sessionMediaMetricQueueRef = useRef<string[]>([])
@@ -1774,6 +1807,10 @@ function ExportPage() {
hasSeededSnsStatsRef.current = hasSeededSnsStats
}, [hasSeededSnsStats])
useEffect(() => {
sessionSnsTimelinePostsRef.current = sessionSnsTimelinePosts
}, [sessionSnsTimelinePosts])
const preselectSessionIds = useMemo(() => {
const state = location.state as { preselectSessionIds?: unknown; preselectSessionId?: unknown } | null
const rawList = Array.isArray(state?.preselectSessionIds)
@@ -1919,6 +1956,177 @@ function ExportPage() {
}
}, [])
const loadSnsUserPostCounts = useCallback(async (options?: { force?: boolean }) => {
if (snsUserPostCountsStatus === 'loading') return
if (!options?.force && snsUserPostCountsStatus === 'ready') return
setSnsUserPostCountsStatus('loading')
try {
const result = await window.electronAPI.sns.getUserPostCounts()
if (result.success && result.counts) {
const normalized: Record<string, number> = {}
for (const [rawUsername, rawCount] of Object.entries(result.counts)) {
const username = String(rawUsername || '').trim()
if (!username) continue
const value = Number(rawCount)
normalized[username] = Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0
}
setSnsUserPostCounts(normalized)
setSnsUserPostCountsStatus('ready')
return
}
setSnsUserPostCountsStatus('error')
} catch (error) {
console.error('加载朋友圈用户条数失败:', error)
setSnsUserPostCountsStatus('error')
}
}, [snsUserPostCountsStatus])
const loadSessionSnsTimelinePosts = useCallback(async (target: SessionSnsTimelineTarget, options?: { reset?: boolean }) => {
const reset = Boolean(options?.reset)
if (sessionSnsTimelineLoadingRef.current) return
sessionSnsTimelineLoadingRef.current = true
if (reset) {
setSessionSnsTimelineLoading(true)
setSessionSnsTimelineLoadingMore(false)
setSessionSnsTimelineHasMore(false)
} else {
setSessionSnsTimelineLoadingMore(true)
}
const requestToken = ++sessionSnsTimelineRequestTokenRef.current
try {
const limit = 20
let endTime: number | undefined
if (!reset && sessionSnsTimelinePostsRef.current.length > 0) {
endTime = sessionSnsTimelinePostsRef.current[sessionSnsTimelinePostsRef.current.length - 1].createTime - 1
}
const result = await window.electronAPI.sns.getTimeline(limit, 0, [target.username], '', undefined, endTime)
if (requestToken !== sessionSnsTimelineRequestTokenRef.current) return
if (!result.success || !Array.isArray(result.timeline)) {
if (reset) {
setSessionSnsTimelinePosts([])
setSessionSnsTimelineHasMore(false)
}
return
}
const timeline = [...(result.timeline as SnsPost[])].sort((a, b) => b.createTime - a.createTime)
if (reset) {
setSessionSnsTimelinePosts(timeline)
setSessionSnsTimelineHasMore(timeline.length >= limit)
return
}
const existingIds = new Set(sessionSnsTimelinePostsRef.current.map((post) => post.id))
const uniqueOlder = timeline.filter((post) => !existingIds.has(post.id))
if (uniqueOlder.length > 0) {
const merged = [...sessionSnsTimelinePostsRef.current, ...uniqueOlder].sort((a, b) => b.createTime - a.createTime)
setSessionSnsTimelinePosts(merged)
}
if (timeline.length < limit) {
setSessionSnsTimelineHasMore(false)
}
} catch (error) {
console.error('加载联系人朋友圈失败:', error)
if (requestToken === sessionSnsTimelineRequestTokenRef.current && reset) {
setSessionSnsTimelinePosts([])
setSessionSnsTimelineHasMore(false)
}
} finally {
if (requestToken === sessionSnsTimelineRequestTokenRef.current) {
sessionSnsTimelineLoadingRef.current = false
setSessionSnsTimelineLoading(false)
setSessionSnsTimelineLoadingMore(false)
}
}
}, [])
const closeSessionSnsTimeline = useCallback(() => {
sessionSnsTimelineRequestTokenRef.current += 1
sessionSnsTimelineLoadingRef.current = false
setSessionSnsTimelineTarget(null)
setSessionSnsTimelinePosts([])
setSessionSnsTimelineLoading(false)
setSessionSnsTimelineLoadingMore(false)
setSessionSnsTimelineHasMore(false)
setSessionSnsTimelineTotalPosts(null)
setSessionSnsTimelineStatsLoading(false)
}, [])
const openSessionSnsTimeline = useCallback(() => {
const normalizedSessionId = String(sessionDetail?.wxid || '').trim()
if (!isSingleContactSession(normalizedSessionId) || !sessionDetail) return
const target: SessionSnsTimelineTarget = {
username: normalizedSessionId,
displayName: sessionDetail.displayName || sessionDetail.remark || sessionDetail.nickName || normalizedSessionId,
avatarUrl: sessionDetail.avatarUrl
}
setSessionSnsTimelineTarget(target)
setSessionSnsTimelinePosts([])
setSessionSnsTimelineHasMore(false)
setSessionSnsTimelineLoadingMore(false)
setSessionSnsTimelineLoading(false)
if (snsUserPostCountsStatus === 'ready') {
const count = Number(snsUserPostCounts[normalizedSessionId] || 0)
setSessionSnsTimelineTotalPosts(Number.isFinite(count) ? Math.max(0, Math.floor(count)) : 0)
setSessionSnsTimelineStatsLoading(false)
} else {
setSessionSnsTimelineTotalPosts(null)
setSessionSnsTimelineStatsLoading(true)
}
void loadSessionSnsTimelinePosts(target, { reset: true })
void loadSnsUserPostCounts()
}, [
loadSessionSnsTimelinePosts,
loadSnsUserPostCounts,
sessionDetail,
snsUserPostCounts,
snsUserPostCountsStatus
])
const loadMoreSessionSnsTimeline = useCallback(() => {
if (!sessionSnsTimelineTarget || sessionSnsTimelineLoading || sessionSnsTimelineLoadingMore || !sessionSnsTimelineHasMore) return
void loadSessionSnsTimelinePosts(sessionSnsTimelineTarget, { reset: false })
}, [
loadSessionSnsTimelinePosts,
sessionSnsTimelineHasMore,
sessionSnsTimelineLoading,
sessionSnsTimelineLoadingMore,
sessionSnsTimelineTarget
])
const renderSessionSnsTimelineStats = useCallback((): string => {
const loadedCount = sessionSnsTimelinePosts.length
const loadPart = sessionSnsTimelineStatsLoading
? `已加载 ${loadedCount} / 总数统计中...`
: sessionSnsTimelineTotalPosts === null
? `已加载 ${loadedCount}`
: `已加载 ${loadedCount} / 共 ${sessionSnsTimelineTotalPosts}`
if (sessionSnsTimelineLoading && loadedCount === 0) return `${loadPart} 加载中...`
if (loadedCount === 0) return loadPart
const latest = sessionSnsTimelinePosts[0]?.createTime
const earliest = sessionSnsTimelinePosts[sessionSnsTimelinePosts.length - 1]?.createTime
const rangeText = `${formatYmdDateFromSeconds(earliest)} ~ ${formatYmdDateFromSeconds(latest)}`
return `${loadPart} ${rangeText}`
}, [
sessionSnsTimelineLoading,
sessionSnsTimelinePosts,
sessionSnsTimelineStatsLoading,
sessionSnsTimelineTotalPosts
])
const mergeSessionContentMetrics = useCallback((input: Record<string, SessionExportMetric | SessionContentMetric | undefined>) => {
const entries = Object.entries(input)
if (entries.length === 0) return
@@ -4081,6 +4289,27 @@ function ExportPage() {
.slice(0, 20)
}, [sessionDetail?.wxid, exportRecordsBySession])
const sessionDetailSupportsSnsTimeline = useMemo(() => {
const sessionId = String(sessionDetail?.wxid || '').trim()
return isSingleContactSession(sessionId)
}, [sessionDetail?.wxid])
const sessionDetailSnsCountLabel = useMemo(() => {
const sessionId = String(sessionDetail?.wxid || '').trim()
if (!sessionId || !sessionDetailSupportsSnsTimeline) return '朋友圈0条'
if (snsUserPostCountsStatus === 'loading' || snsUserPostCountsStatus === 'idle') {
return '朋友圈:统计中...'
}
if (snsUserPostCountsStatus === 'error') {
return '朋友圈:统计失败'
}
const count = Number(snsUserPostCounts[sessionId] || 0)
const normalized = Number.isFinite(count) ? Math.max(0, Math.floor(count)) : 0
return `朋友圈:${normalized}`
}, [sessionDetail?.wxid, sessionDetailSupportsSnsTimeline, snsUserPostCounts, snsUserPostCountsStatus])
const applySessionDetailStats = useCallback((
sessionId: string,
metric: SessionExportMetric,
@@ -4371,14 +4600,58 @@ function ExportPage() {
}
}, [applySessionDetailStats, isLoadingSessionRelationStats, sessionDetail?.wxid])
useEffect(() => {
if (!showSessionDetailPanel || !sessionDetailSupportsSnsTimeline) return
if (snsUserPostCountsStatus === 'idle') {
void loadSnsUserPostCounts()
}
}, [
loadSnsUserPostCounts,
sessionDetailSupportsSnsTimeline,
showSessionDetailPanel,
snsUserPostCountsStatus
])
useEffect(() => {
if (!sessionSnsTimelineTarget) return
if (snsUserPostCountsStatus === 'loading' || snsUserPostCountsStatus === 'idle') {
setSessionSnsTimelineStatsLoading(true)
return
}
if (snsUserPostCountsStatus === 'ready') {
const total = Number(snsUserPostCounts[sessionSnsTimelineTarget.username] || 0)
setSessionSnsTimelineTotalPosts(Number.isFinite(total) ? Math.max(0, Math.floor(total)) : 0)
setSessionSnsTimelineStatsLoading(false)
return
}
setSessionSnsTimelineTotalPosts(null)
setSessionSnsTimelineStatsLoading(false)
}, [sessionSnsTimelineTarget, snsUserPostCounts, snsUserPostCountsStatus])
useEffect(() => {
if (sessionSnsTimelineTotalPosts === null) return
if (sessionSnsTimelinePosts.length >= sessionSnsTimelineTotalPosts) {
setSessionSnsTimelineHasMore(false)
}
}, [sessionSnsTimelinePosts.length, sessionSnsTimelineTotalPosts])
const closeSessionDetailPanel = useCallback(() => {
detailRequestSeqRef.current += 1
detailStatsPriorityRef.current = false
sessionSnsTimelineRequestTokenRef.current += 1
sessionSnsTimelineLoadingRef.current = false
setShowSessionDetailPanel(false)
setIsLoadingSessionDetail(false)
setIsLoadingSessionDetailExtra(false)
setIsRefreshingSessionDetailStats(false)
setIsLoadingSessionRelationStats(false)
setSessionSnsTimelineTarget(null)
setSessionSnsTimelinePosts([])
setSessionSnsTimelineLoading(false)
setSessionSnsTimelineLoadingMore(false)
setSessionSnsTimelineHasMore(false)
setSessionSnsTimelineTotalPosts(null)
setSessionSnsTimelineStatsLoading(false)
}, [])
const openSessionDetail = useCallback((sessionId: string) => {
@@ -4410,6 +4683,17 @@ function ExportPage() {
return () => window.removeEventListener('keydown', handleKeyDown)
}, [showSessionLoadDetailModal])
useEffect(() => {
if (!sessionSnsTimelineTarget) return
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
closeSessionSnsTimeline()
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [closeSessionSnsTimeline, sessionSnsTimelineTarget])
const handleCopyDetailField = useCallback(async (text: string, field: string) => {
try {
await navigator.clipboard.writeText(text)
@@ -5228,6 +5512,21 @@ function ExportPage() {
</button>
</div>
)}
{sessionDetailSupportsSnsTimeline && (
<div className="detail-item">
<Aperture size={14} />
<span className="label"></span>
<span className="value">
<button
className="detail-inline-btn detail-sns-entry-btn"
type="button"
onClick={openSessionSnsTimeline}
>
{sessionDetailSnsCountLabel}
</button>
</span>
</div>
)}
</div>
<div className="detail-section">
@@ -5454,6 +5753,100 @@ function ExportPage() {
</aside>
</div>
)}
{sessionSnsTimelineTarget && (
<div className="export-session-sns-overlay" onClick={closeSessionSnsTimeline}>
<div
className="export-session-sns-dialog"
role="dialog"
aria-modal="true"
aria-label="联系人朋友圈"
onClick={(event) => event.stopPropagation()}
>
<div className="sns-dialog-header">
<div className="sns-dialog-header-main">
<div className="sns-dialog-avatar">
{sessionSnsTimelineTarget.avatarUrl ? (
<img src={sessionSnsTimelineTarget.avatarUrl} alt="" />
) : (
<span>{getAvatarLetter(sessionSnsTimelineTarget.displayName || sessionSnsTimelineTarget.username)}</span>
)}
</div>
<div className="sns-dialog-meta">
<h4>{sessionSnsTimelineTarget.displayName}</h4>
<div className="sns-dialog-username">@{sessionSnsTimelineTarget.username}</div>
<div className="sns-dialog-stats">{renderSessionSnsTimelineStats()}</div>
</div>
</div>
<button className="close-btn" type="button" onClick={closeSessionSnsTimeline}>
<X size={16} />
</button>
</div>
<div className="sns-dialog-body">
{sessionSnsTimelinePosts.length > 0 && (
<div className="sns-post-list">
{sessionSnsTimelinePosts.map((post) => (
<article className="sns-post-card" key={post.id}>
<div className="sns-post-time">{formatYmdHmDateTime(post.createTime * 1000)}</div>
{post.contentDesc && <div className="sns-post-content">{post.contentDesc}</div>}
{Array.isArray(post.media) && post.media.length > 0 && (
<div className="sns-post-media-grid">
{post.media.slice(0, 9).map((media, mediaIndex) => {
const mediaUrl = String(media?.url || media?.thumb || '')
const previewUrl = String(media?.thumb || media?.url || '')
if (!mediaUrl || !previewUrl) return null
const isVideo = isSnsVideoMediaUrl(mediaUrl)
return (
<button
className="sns-post-media-item"
key={`${post.id}-media-${mediaIndex}`}
type="button"
onClick={() => {
if (isVideo) {
void window.electronAPI.window.openVideoPlayerWindow(mediaUrl)
return
}
void window.electronAPI.window.openImageViewerWindow(
mediaUrl,
media?.livePhoto?.url || undefined
)
}}
>
<img src={previewUrl} alt="" loading="lazy" referrerPolicy="no-referrer" />
{isVideo && <span className="sns-post-media-video-tag"></span>}
</button>
)
})}
</div>
)}
</article>
))}
</div>
)}
{sessionSnsTimelineLoading && (
<div className="sns-dialog-status">...</div>
)}
{!sessionSnsTimelineLoading && sessionSnsTimelinePosts.length === 0 && (
<div className="sns-dialog-status empty"></div>
)}
{!sessionSnsTimelineLoading && sessionSnsTimelineHasMore && (
<button
className="sns-dialog-load-more"
type="button"
onClick={loadMoreSessionSnsTimeline}
disabled={sessionSnsTimelineLoadingMore}
>
{sessionSnsTimelineLoadingMore ? '正在加载...' : '加载更多'}
</button>
)}
</div>
</div>
</div>
)}
</div>
</div>