修复朋友圈缓存问题
This commit is contained in:
1 parent
4daeb59b6b
commit
46b49ef305
5 files changed
+738
-35
No files matched your search
@@ -202,16 +202,16 @@ export function ContactSnsTimelineDialog({
|
|||||||
setTimelineStatsLoading(true)
|
setTimelineStatsLoading(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await window.electronAPI.sns.getUserPostCounts()
|
const result = await window.electronAPI.sns.getUserPostStats(nextTarget.username)
|
||||||
if (requestToken !== totalPostsRequestTokenRef.current) return
|
if (requestToken !== totalPostsRequestTokenRef.current) return
|
||||||
|
|
||||||
if (!result.success || !result.counts) {
|
if (!result.success || !result.data) {
|
||||||
setTimelineTotalPosts(null)
|
setTimelineTotalPosts(null)
|
||||||
setRankTotalPosts(null)
|
setRankTotalPosts(null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawCount = Number(result.counts[nextTarget.username] || 0)
|
const rawCount = Number(result.data.totalPosts || 0)
|
||||||
const normalized = Number.isFinite(rawCount) ? Math.max(0, Math.floor(rawCount)) : 0
|
const normalized = Number.isFinite(rawCount) ? Math.max(0, Math.floor(rawCount)) : 0
|
||||||
setTimelineTotalPosts(normalized)
|
setTimelineTotalPosts(normalized)
|
||||||
setRankTotalPosts(normalized)
|
setRankTotalPosts(normalized)
|
||||||
|
|||||||
@@ -0,0 +1,533 @@
|
|||||||
|
:root {
|
||||||
|
--sns-card-bg: transparent;
|
||||||
|
--sns-border-radius-md: 8px;
|
||||||
|
--sns-border-radius-sm: 6px;
|
||||||
|
--sns-media-cell: 88px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sns-post-item {
|
||||||
|
background: var(--sns-card-bg);
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid color-mix(in srgb, var(--border-color) 82%, transparent);
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
overflow: visible;
|
||||||
|
padding: 10px 6px 12px;
|
||||||
|
position: relative;
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: color-mix(in srgb, var(--bg-secondary) 72%, transparent);
|
||||||
|
box-shadow: none;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.post-deleted {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-deleted-badge {
|
||||||
|
align-items: center;
|
||||||
|
background: rgba(255, 77, 79, 0.1);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #ff4d4f;
|
||||||
|
display: flex;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-avatar-col {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.author-trigger-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-trigger {
|
||||||
|
border-radius: var(--sns-border-radius-md);
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: none;
|
||||||
|
opacity: 0.86;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid var(--primary);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-content-col {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-header-row {
|
||||||
|
align-items: flex-start;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
|
||||||
|
.post-author-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.author-name {
|
||||||
|
color: var(--primary);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.author-name-trigger {
|
||||||
|
align-self: flex-start;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
|
||||||
|
.author-name {
|
||||||
|
margin-bottom: 0;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-color: transparent;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
transition: color 0.15s ease, text-decoration-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .author-name {
|
||||||
|
color: var(--primary);
|
||||||
|
text-decoration-color: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid var(--primary);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-time {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-header-actions {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-time-standalone {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.2;
|
||||||
|
padding-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-btn {
|
||||||
|
align-items: center;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
height: 24px;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
padding: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
width: 24px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.delete-btn:hover {
|
||||||
|
background: rgba(255, 77, 79, 0.08);
|
||||||
|
border-color: rgba(255, 77, 79, 0.4);
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-protected-badge {
|
||||||
|
align-items: center;
|
||||||
|
background: rgba(76, 175, 80, 0.08);
|
||||||
|
border: 1px solid rgba(76, 175, 80, 0.2);
|
||||||
|
border-radius: 5px;
|
||||||
|
color: var(--color-success, #4caf50);
|
||||||
|
display: flex;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
gap: 3px;
|
||||||
|
opacity: 0;
|
||||||
|
padding: 3px 7px;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sns-post-item:hover .debug-btn,
|
||||||
|
.sns-post-item:hover .post-protected-badge {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-text {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.48;
|
||||||
|
margin: 0 0 8px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-location {
|
||||||
|
align-items: flex-start;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
display: flex;
|
||||||
|
font-size: 12px;
|
||||||
|
gap: 6px;
|
||||||
|
line-height: 1.45;
|
||||||
|
margin: -2px 0 8px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-location-text {
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-media-container {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-link-card {
|
||||||
|
align-items: center;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--sns-border-radius-md);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
text-align: left;
|
||||||
|
transition: background 0.15s ease, border-color 0.15s ease;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(var(--primary-rgb), 0.08);
|
||||||
|
border-color: rgba(var(--primary-rgb), 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-thumb {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: var(--sns-border-radius-sm);
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: 48px;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 48px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-thumb-fallback {
|
||||||
|
align-items: center;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-meta {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
.link-title {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-url {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-size: 11px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-arrow {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-interactions {
|
||||||
|
border-top: none;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 8px;
|
||||||
|
padding-top: 0;
|
||||||
|
|
||||||
|
.likes-block {
|
||||||
|
color: var(--primary);
|
||||||
|
display: flex;
|
||||||
|
font-weight: 500;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
|
||||||
|
.like-icon {
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.comments-block {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border-radius: var(--sns-border-radius-sm);
|
||||||
|
padding: 6px 8px;
|
||||||
|
|
||||||
|
.comment-row {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.45;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-user {
|
||||||
|
color: var(--primary);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-text {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
margin: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-colon {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-custom-emoji {
|
||||||
|
border-radius: 4px;
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 2px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sns-media-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
&.grid-1 .sns-media-item {
|
||||||
|
aspect-ratio: auto;
|
||||||
|
border-radius: var(--sns-border-radius-md);
|
||||||
|
height: auto;
|
||||||
|
max-height: 340px;
|
||||||
|
max-width: 270px;
|
||||||
|
width: fit-content;
|
||||||
|
|
||||||
|
&.deleted-media,
|
||||||
|
&.failed-media {
|
||||||
|
aspect-ratio: 1;
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
img,
|
||||||
|
video {
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
display: block;
|
||||||
|
height: auto;
|
||||||
|
max-height: 340px;
|
||||||
|
max-width: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.grid-2,
|
||||||
|
&.grid-4 {
|
||||||
|
grid-template-columns: repeat(2, var(--sns-media-cell));
|
||||||
|
max-width: calc((var(--sns-media-cell) * 2) + 4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.grid-3,
|
||||||
|
&.grid-6,
|
||||||
|
&.grid-9 {
|
||||||
|
grid-template-columns: repeat(3, var(--sns-media-cell));
|
||||||
|
max-width: calc((var(--sns-media-cell) * 3) + 8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sns-media-item {
|
||||||
|
aspect-ratio: 1;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border-radius: var(--sns-border-radius-sm);
|
||||||
|
cursor: zoom-in;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
img,
|
||||||
|
video {
|
||||||
|
animation: snsPostFadeIn 0.2s ease;
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-badge {
|
||||||
|
align-items: center;
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
border-radius: 50%;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
height: 32px;
|
||||||
|
justify-content: center;
|
||||||
|
left: 50%;
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 32px;
|
||||||
|
|
||||||
|
&.live {
|
||||||
|
height: 24px;
|
||||||
|
left: auto;
|
||||||
|
right: 6px;
|
||||||
|
top: 6px;
|
||||||
|
transform: none;
|
||||||
|
width: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-decrypting-mask {
|
||||||
|
align-items: center;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: 12px;
|
||||||
|
gap: 8px;
|
||||||
|
inset: 0;
|
||||||
|
justify-content: center;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-download-btn {
|
||||||
|
align-items: center;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
border-radius: 50%;
|
||||||
|
bottom: 5px;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
height: 24px;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
right: 5px;
|
||||||
|
transition: opacity 0.15s ease, background 0.15s ease;
|
||||||
|
width: 24px;
|
||||||
|
z-index: 5;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
inset: -4px;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .media-download-btn {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.deleted-media {
|
||||||
|
cursor: default;
|
||||||
|
opacity: 0.6;
|
||||||
|
|
||||||
|
.deleted-placeholder {
|
||||||
|
align-items: center;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: 12px;
|
||||||
|
gap: 6px;
|
||||||
|
height: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.failed-media {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.failed-placeholder {
|
||||||
|
align-items: center;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: 12px;
|
||||||
|
gap: 6px;
|
||||||
|
height: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .failed-placeholder {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes snsPostFadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import { SnsPost, SnsLinkCardData, SnsLocation } from '../../types/sns'
|
|||||||
import { Avatar } from '../Avatar'
|
import { Avatar } from '../Avatar'
|
||||||
import { SnsMediaGrid } from './SnsMediaGrid'
|
import { SnsMediaGrid } from './SnsMediaGrid'
|
||||||
import { renderTextWithEmoji } from '../../utils/renderTextWithEmoji'
|
import { renderTextWithEmoji } from '../../utils/renderTextWithEmoji'
|
||||||
|
import './SnsPostItem.scss'
|
||||||
|
|
||||||
// Helper functions (extracted from SnsPage.tsx but simplified/reused)
|
// Helper functions (extracted from SnsPage.tsx but simplified/reused)
|
||||||
const LINK_XML_URL_TAGS = ['url', 'shorturl', 'weburl', 'webpageurl', 'jumpurl']
|
const LINK_XML_URL_TAGS = ['url', 'shorturl', 'weburl', 'webpageurl', 'jumpurl']
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
serializeExportDateRangeConfig,
|
serializeExportDateRangeConfig,
|
||||||
type ExportDefaultDateRangeConfig
|
type ExportDefaultDateRangeConfig
|
||||||
} from '../../../../utils/exportDateRange'
|
} from '../../../../utils/exportDateRange'
|
||||||
import type { ExportDialogState, ExportOptions, TextExportFormat } from '../../types'
|
import type { DisplayNamePreference, ExportDialogState, ExportOptions, TextExportFormat } from '../../types'
|
||||||
import { conflictStrategyOptions, displayNameOptions, formatOptions, MAX_EXPORT_FILE_SIZE_MB_LIMIT } from '../../constants'
|
import { conflictStrategyOptions, displayNameOptions, formatOptions, MAX_EXPORT_FILE_SIZE_MB_LIMIT } from '../../constants'
|
||||||
import { formatPathBrief } from '../../utils/format'
|
import { formatPathBrief } from '../../utils/format'
|
||||||
import './ExportDialog.scss'
|
import './ExportDialog.scss'
|
||||||
@@ -65,6 +65,20 @@ const ExportDialog: React.FC<ExportDialogProps> = ({
|
|||||||
return getExportDateRangeLabel(currentSelection)
|
return getExportDateRangeLabel(currentSelection)
|
||||||
}, [currentSelection])
|
}, [currentSelection])
|
||||||
|
|
||||||
|
const hasGroupSession = React.useMemo(() => {
|
||||||
|
return dialogState.sessionIds.some(sessionId => String(sessionId || '').includes('@chatroom'))
|
||||||
|
}, [dialogState.sessionIds])
|
||||||
|
|
||||||
|
const visibleDisplayNameOptions = React.useMemo(() => {
|
||||||
|
return hasGroupSession
|
||||||
|
? displayNameOptions
|
||||||
|
: displayNameOptions.filter(item => item.value !== 'group-nickname')
|
||||||
|
}, [hasGroupSession])
|
||||||
|
|
||||||
|
const effectiveDisplayNamePreference: DisplayNamePreference = !hasGroupSession && draftOptions.displayNamePreference === 'group-nickname'
|
||||||
|
? 'remark'
|
||||||
|
: draftOptions.displayNamePreference
|
||||||
|
|
||||||
if (!dialogState.open) return null
|
if (!dialogState.open) return null
|
||||||
|
|
||||||
const { intent, scope, sessionNames, title } = dialogState
|
const { intent, scope, sessionNames, title } = dialogState
|
||||||
@@ -320,12 +334,15 @@ const ExportDialog: React.FC<ExportDialogProps> = ({
|
|||||||
|
|
||||||
<div className="display-name-control">
|
<div className="display-name-control">
|
||||||
<span>命名方式:</span>
|
<span>命名方式:</span>
|
||||||
<div className="display-name-segmented" title="控制导出群消息时发送者名称的优先级">
|
<div
|
||||||
{displayNameOptions.map(item => (
|
className="display-name-segmented"
|
||||||
|
title={hasGroupSession ? '控制导出群消息时发送者名称的优先级' : '控制导出私聊消息时联系人名称的优先级'}
|
||||||
|
>
|
||||||
|
{visibleDisplayNameOptions.map(item => (
|
||||||
<button
|
<button
|
||||||
key={item.value}
|
key={item.value}
|
||||||
type="button"
|
type="button"
|
||||||
className={draftOptions.displayNamePreference === item.value ? 'active' : ''}
|
className={effectiveDisplayNamePreference === item.value ? 'active' : ''}
|
||||||
title={item.desc}
|
title={item.desc}
|
||||||
onClick={() => updateDraftOptions({ displayNamePreference: item.value })}
|
onClick={() => updateDraftOptions({ displayNamePreference: item.value })}
|
||||||
>
|
>
|
||||||
|
|||||||
+180
-28
@@ -21,7 +21,7 @@ import {
|
|||||||
getExportDateRangeLabel,
|
getExportDateRangeLabel,
|
||||||
type ExportDateRangeSelection
|
type ExportDateRangeSelection
|
||||||
} from '../utils/exportDateRange'
|
} from '../utils/exportDateRange'
|
||||||
import { displayNameForCompare, displayNameOrFallback } from '../utils/displayName'
|
import { displayNameForCompare, displayNameOrFallback, pickDisplayName } from '../utils/displayName'
|
||||||
|
|
||||||
const SNS_PAGE_CACHE_TTL_MS = 24 * 60 * 60 * 1000
|
const SNS_PAGE_CACHE_TTL_MS = 24 * 60 * 60 * 1000
|
||||||
const SNS_PAGE_CACHE_POST_LIMIT = 200
|
const SNS_PAGE_CACHE_POST_LIMIT = 200
|
||||||
@@ -158,6 +158,21 @@ const normalizeAccountId = (value?: string | null): string => {
|
|||||||
|
|
||||||
const normalizeNameForCompare = (value?: string | null): string => displayNameForCompare(value)
|
const normalizeNameForCompare = (value?: string | null): string => displayNameForCompare(value)
|
||||||
|
|
||||||
|
const isRawWxidDisplayName = (value?: string | null): boolean => {
|
||||||
|
const normalized = String(value || '').trim()
|
||||||
|
return /^wxid_[a-z0-9_]+$/i.test(normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
const pickMeaningfulContactDisplayName = (username: string, ...values: Array<unknown>): string | undefined => {
|
||||||
|
const picked = pickDisplayName(...values)
|
||||||
|
if (!picked) return undefined
|
||||||
|
const normalizedPicked = normalizeAccountId(picked)
|
||||||
|
const normalizedUsername = normalizeAccountId(username)
|
||||||
|
if (normalizedPicked && normalizedUsername && normalizedPicked === normalizedUsername) return undefined
|
||||||
|
if (isRawWxidDisplayName(picked)) return undefined
|
||||||
|
return picked
|
||||||
|
}
|
||||||
|
|
||||||
export default function SnsPage() {
|
export default function SnsPage() {
|
||||||
const [posts, setPosts] = useState<SnsPost[]>([])
|
const [posts, setPosts] = useState<SnsPost[]>([])
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
@@ -439,6 +454,38 @@ export default function SnsPage() {
|
|||||||
return [...input].sort(compareContactsForRanking)
|
return [...input].sort(compareContactsForRanking)
|
||||||
}, [compareContactsForRanking])
|
}, [compareContactsForRanking])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!contactsLoading || contactsRef.current.length > 0 || posts.length === 0) return
|
||||||
|
|
||||||
|
const fallbackMap = new Map<string, Contact>()
|
||||||
|
for (const post of posts) {
|
||||||
|
const username = String(post.username || '').trim()
|
||||||
|
if (!username || username.endsWith('@chatroom') || username.startsWith('gh_') || fallbackMap.has(username)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const displayName = pickMeaningfulContactDisplayName(username, post.nickname)
|
||||||
|
if (!displayName) continue
|
||||||
|
fallbackMap.set(username, {
|
||||||
|
username,
|
||||||
|
displayName,
|
||||||
|
avatarUrl: post.avatarUrl,
|
||||||
|
type: 'friend',
|
||||||
|
lastSessionTimestamp: Number(post.createTime || 0),
|
||||||
|
postCountStatus: 'idle'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallbackContacts = sortContactsForRanking(Array.from(fallbackMap.values()))
|
||||||
|
if (fallbackContacts.length === 0) return
|
||||||
|
setContacts(fallbackContacts)
|
||||||
|
setContactsLoading(false)
|
||||||
|
setContactsCountProgress({
|
||||||
|
resolved: 0,
|
||||||
|
total: fallbackContacts.length,
|
||||||
|
running: false
|
||||||
|
})
|
||||||
|
}, [contactsLoading, posts, sortContactsForRanking])
|
||||||
|
|
||||||
const resolvedCurrentUserContact = useMemo(() => {
|
const resolvedCurrentUserContact = useMemo(() => {
|
||||||
const normalizedWxid = normalizeAccountId(currentUserProfile.wxid)
|
const normalizedWxid = normalizeAccountId(currentUserProfile.wxid)
|
||||||
const normalizedAlias = normalizeAccountId(currentUserProfile.alias)
|
const normalizedAlias = normalizeAccountId(currentUserProfile.alias)
|
||||||
@@ -1448,21 +1495,42 @@ export default function SnsPage() {
|
|||||||
setContactsLoading(true)
|
setContactsLoading(true)
|
||||||
try {
|
try {
|
||||||
const snsPostCountsScopeKey = await ensureSnsUserPostCountsCacheScopeKey()
|
const snsPostCountsScopeKey = await ensureSnsUserPostCountsCacheScopeKey()
|
||||||
const [cachedPostCountsItem, cachedContactsItem, cachedAvatarItem] = await Promise.all([
|
const snsPageCacheScopeKey = await ensureSnsCacheScopeKey()
|
||||||
|
const [cachedPostCountsItem, cachedContactsItem, cachedAvatarItem, cachedSnsPageItem] = await Promise.all([
|
||||||
configService.getExportSnsUserPostCountsCache(snsPostCountsScopeKey),
|
configService.getExportSnsUserPostCountsCache(snsPostCountsScopeKey),
|
||||||
configService.getContactsListCache(snsPostCountsScopeKey),
|
configService.getContactsListCache(snsPostCountsScopeKey),
|
||||||
configService.getContactsAvatarCache(snsPostCountsScopeKey)
|
configService.getContactsAvatarCache(snsPostCountsScopeKey),
|
||||||
|
configService.getSnsPageCache(snsPageCacheScopeKey)
|
||||||
])
|
])
|
||||||
const cachedPostCounts = forceCounts ? {} : (cachedPostCountsItem?.counts || {})
|
const cachedPostCounts = forceCounts ? {} : (cachedPostCountsItem?.counts || {})
|
||||||
const cachedAvatarMap = cachedAvatarItem?.avatars || {}
|
const cachedAvatarMap = cachedAvatarItem?.avatars || {}
|
||||||
|
const isCachedSnsPost = (post: unknown): post is SnsPost => {
|
||||||
|
if (!post || typeof post !== 'object') return false
|
||||||
|
const candidate = post as Partial<SnsPost>
|
||||||
|
return typeof candidate.username === 'string' && typeof candidate.createTime === 'number'
|
||||||
|
}
|
||||||
|
const cachedPagePosts = Array.isArray(cachedSnsPageItem?.posts)
|
||||||
|
? cachedSnsPageItem.posts.filter(isCachedSnsPost)
|
||||||
|
: []
|
||||||
|
const latestPostByUsername = new Map<string, SnsPost>()
|
||||||
|
for (const post of [...cachedPagePosts, ...postsRef.current]) {
|
||||||
|
const username = String(post.username || '').trim()
|
||||||
|
if (!username) continue
|
||||||
|
const previous = latestPostByUsername.get(username)
|
||||||
|
if (!previous || Number(post.createTime || 0) > Number(previous.createTime || 0)) {
|
||||||
|
latestPostByUsername.set(username, post)
|
||||||
|
}
|
||||||
|
}
|
||||||
const cachedContacts = (cachedContactsItem?.contacts || [])
|
const cachedContacts = (cachedContactsItem?.contacts || [])
|
||||||
.filter((contact) => contact.type === 'friend' || contact.type === 'former_friend')
|
.filter((contact) => contact.type === 'friend' || contact.type === 'former_friend')
|
||||||
.map((contact) => {
|
.map((contact): Contact | null => {
|
||||||
|
const displayName = pickMeaningfulContactDisplayName(contact.username, contact.displayName, contact.remark, contact.nickname)
|
||||||
|
if (!displayName) return null
|
||||||
const cachedCount = cachedPostCounts[contact.username]
|
const cachedCount = cachedPostCounts[contact.username]
|
||||||
const hasCachedCount = typeof cachedCount === 'number' && Number.isFinite(cachedCount)
|
const hasCachedCount = typeof cachedCount === 'number' && Number.isFinite(cachedCount)
|
||||||
return {
|
return {
|
||||||
username: contact.username,
|
username: contact.username,
|
||||||
displayName: displayNameOrFallback(contact.username, contact.displayName),
|
displayName,
|
||||||
avatarUrl: cachedAvatarMap[contact.username]?.avatarUrl,
|
avatarUrl: cachedAvatarMap[contact.username]?.avatarUrl,
|
||||||
remark: contact.remark,
|
remark: contact.remark,
|
||||||
nickname: contact.nickname,
|
nickname: contact.nickname,
|
||||||
@@ -1476,6 +1544,36 @@ export default function SnsPage() {
|
|||||||
: 'idle' as ContactPostCountStatus
|
: 'idle' as ContactPostCountStatus
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.filter((contact): contact is Contact => Boolean(contact))
|
||||||
|
const fallbackContactsFromSns = (() => {
|
||||||
|
const map = new Map<string, Contact>()
|
||||||
|
const addPost = (rawUsername: string, post?: SnsPost) => {
|
||||||
|
const username = String(rawUsername || '').trim()
|
||||||
|
if (!username || username.endsWith('@chatroom') || username.startsWith('gh_') || map.has(username)) return
|
||||||
|
const displayName = pickMeaningfulContactDisplayName(username, post?.nickname)
|
||||||
|
if (!displayName) return
|
||||||
|
const cachedCount = cachedPostCounts[username]
|
||||||
|
const hasCachedCount = typeof cachedCount === 'number' && Number.isFinite(cachedCount)
|
||||||
|
map.set(username, {
|
||||||
|
username,
|
||||||
|
displayName,
|
||||||
|
avatarUrl: post?.avatarUrl,
|
||||||
|
type: 'friend',
|
||||||
|
lastSessionTimestamp: Number(post?.createTime || 0),
|
||||||
|
postCount: hasCachedCount ? Math.max(0, Math.floor(cachedCount)) : undefined,
|
||||||
|
postCountStatus: hasCachedCount
|
||||||
|
? 'ready' as ContactPostCountStatus
|
||||||
|
: forceCounts
|
||||||
|
? 'loading' as ContactPostCountStatus
|
||||||
|
: 'idle' as ContactPostCountStatus
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [username, post] of latestPostByUsername.entries()) {
|
||||||
|
addPost(username, post)
|
||||||
|
}
|
||||||
|
return Array.from(map.values())
|
||||||
|
})()
|
||||||
|
|
||||||
if (requestToken !== contactsLoadTokenRef.current) {
|
if (requestToken !== contactsLoadTokenRef.current) {
|
||||||
if (activeContactsLoadTaskIdRef.current === taskId) {
|
if (activeContactsLoadTaskIdRef.current === taskId) {
|
||||||
@@ -1486,8 +1584,9 @@ export default function SnsPage() {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (cachedContacts.length > 0) {
|
const initialContacts = cachedContacts.length > 0 ? cachedContacts : fallbackContactsFromSns
|
||||||
const cachedContactsSorted = sortContactsForRanking(cachedContacts)
|
if (initialContacts.length > 0) {
|
||||||
|
const cachedContactsSorted = sortContactsForRanking(initialContacts)
|
||||||
setContacts(cachedContactsSorted)
|
setContacts(cachedContactsSorted)
|
||||||
setContactsLoading(false)
|
setContactsLoading(false)
|
||||||
const cachedReadyCount = cachedContactsSorted.filter(contact => contact.postCountStatus === 'ready').length
|
const cachedReadyCount = cachedContactsSorted.filter(contact => contact.postCountStatus === 'ready').length
|
||||||
@@ -1502,9 +1601,10 @@ export default function SnsPage() {
|
|||||||
detail: '正在读取联系人与最近会话数据',
|
detail: '正在读取联系人与最近会话数据',
|
||||||
progressText: '联系人快照'
|
progressText: '联系人快照'
|
||||||
})
|
})
|
||||||
const [contactsResult, sessionsResult] = await Promise.all([
|
const [contactsResult, sessionsResult, snsUsernamesResult] = await Promise.all([
|
||||||
window.electronAPI.chat.getContacts(),
|
window.electronAPI.chat.getContacts({ lite: true }),
|
||||||
window.electronAPI.chat.getSessions()
|
window.electronAPI.chat.getSessions(),
|
||||||
|
window.electronAPI.sns.getSnsUsernames()
|
||||||
])
|
])
|
||||||
if (isBackgroundTaskCancelRequested(taskId)) {
|
if (isBackgroundTaskCancelRequested(taskId)) {
|
||||||
if (activeContactsLoadTaskIdRef.current === taskId) {
|
if (activeContactsLoadTaskIdRef.current === taskId) {
|
||||||
@@ -1536,12 +1636,14 @@ export default function SnsPage() {
|
|||||||
if (contactsResult.success && contactsResult.contacts) {
|
if (contactsResult.success && contactsResult.contacts) {
|
||||||
for (const c of contactsResult.contacts) {
|
for (const c of contactsResult.contacts) {
|
||||||
if (c.type === 'friend' || c.type === 'former_friend') {
|
if (c.type === 'friend' || c.type === 'former_friend') {
|
||||||
|
const displayName = pickMeaningfulContactDisplayName(c.username, c.displayName, c.remark, c.nickname)
|
||||||
|
if (!displayName) continue
|
||||||
const cachedCount = cachedPostCounts[c.username]
|
const cachedCount = cachedPostCounts[c.username]
|
||||||
const hasCachedCount = typeof cachedCount === 'number' && Number.isFinite(cachedCount)
|
const hasCachedCount = typeof cachedCount === 'number' && Number.isFinite(cachedCount)
|
||||||
contactMap.set(c.username, {
|
contactMap.set(c.username, {
|
||||||
username: c.username,
|
username: c.username,
|
||||||
displayName: c.displayName,
|
displayName,
|
||||||
avatarUrl: c.avatarUrl,
|
avatarUrl: c.avatarUrl || cachedAvatarMap[c.username]?.avatarUrl,
|
||||||
remark: c.remark,
|
remark: c.remark,
|
||||||
nickname: c.nickname,
|
nickname: c.nickname,
|
||||||
type: c.type === 'former_friend' ? 'former_friend' : 'friend',
|
type: c.type === 'former_friend' ? 'former_friend' : 'friend',
|
||||||
@@ -1553,6 +1655,17 @@ export default function SnsPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const snsOnlyUsernames: string[] = []
|
||||||
|
if (snsUsernamesResult.success && Array.isArray(snsUsernamesResult.usernames)) {
|
||||||
|
for (const rawUsername of snsUsernamesResult.usernames) {
|
||||||
|
const username = String(rawUsername || '').trim()
|
||||||
|
if (!username || username.endsWith('@chatroom') || username.startsWith('gh_') || contactMap.has(username)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
snsOnlyUsernames.push(username)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let contactsList = sortContactsForRanking(Array.from(contactMap.values()))
|
let contactsList = sortContactsForRanking(Array.from(contactMap.values()))
|
||||||
if (requestToken !== contactsLoadTokenRef.current) {
|
if (requestToken !== contactsLoadTokenRef.current) {
|
||||||
if (activeContactsLoadTaskIdRef.current === taskId) {
|
if (activeContactsLoadTaskIdRef.current === taskId) {
|
||||||
@@ -1563,18 +1676,11 @@ export default function SnsPage() {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setContacts(contactsList)
|
|
||||||
const readyUsernames = new Set(
|
|
||||||
contactsList
|
|
||||||
.filter((contact) => contact.postCountStatus === 'ready' && typeof contact.postCount === 'number')
|
|
||||||
.map((contact) => contact.username)
|
|
||||||
)
|
|
||||||
void hydrateContactPostCounts(
|
|
||||||
contactsList.map(contact => contact.username),
|
|
||||||
{ readyUsernames, force: forceCounts }
|
|
||||||
)
|
|
||||||
|
|
||||||
const allUsernames = contactsList.map(c => c.username)
|
const allUsernames = Array.from(new Set([
|
||||||
|
...contactsList.map(c => c.username),
|
||||||
|
...snsOnlyUsernames
|
||||||
|
]))
|
||||||
|
|
||||||
// 用 enrichSessionsContactInfo 统一补充头像和显示名
|
// 用 enrichSessionsContactInfo 统一补充头像和显示名
|
||||||
if (allUsernames.length > 0) {
|
if (allUsernames.length > 0) {
|
||||||
@@ -1593,15 +1699,44 @@ export default function SnsPage() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (enriched.success && enriched.contacts) {
|
if (enriched.success && enriched.contacts) {
|
||||||
|
const nextContactMap = new Map<string, Contact>()
|
||||||
contactsList = contactsList.map((contact) => {
|
contactsList = contactsList.map((contact) => {
|
||||||
const extra = enriched.contacts?.[contact.username]
|
const extra = enriched.contacts?.[contact.username]
|
||||||
if (!extra) return contact
|
if (!extra) {
|
||||||
return {
|
nextContactMap.set(contact.username, contact)
|
||||||
|
return contact
|
||||||
|
}
|
||||||
|
const displayName = pickMeaningfulContactDisplayName(contact.username, extra.displayName, contact.displayName)
|
||||||
|
const nextContact = {
|
||||||
...contact,
|
...contact,
|
||||||
displayName: displayNameOrFallback(contact.displayName, extra.displayName),
|
displayName: displayName || contact.displayName,
|
||||||
avatarUrl: extra.avatarUrl || contact.avatarUrl
|
avatarUrl: extra.avatarUrl || contact.avatarUrl
|
||||||
}
|
}
|
||||||
|
nextContactMap.set(contact.username, nextContact)
|
||||||
|
return nextContact
|
||||||
})
|
})
|
||||||
|
|
||||||
|
for (const username of snsOnlyUsernames) {
|
||||||
|
if (nextContactMap.has(username)) continue
|
||||||
|
const extra = enriched.contacts[username]
|
||||||
|
const displayName = pickMeaningfulContactDisplayName(username, extra?.displayName)
|
||||||
|
if (!displayName) continue
|
||||||
|
const cachedCount = cachedPostCounts[username]
|
||||||
|
const hasCachedCount = typeof cachedCount === 'number' && Number.isFinite(cachedCount)
|
||||||
|
nextContactMap.set(username, {
|
||||||
|
username,
|
||||||
|
displayName,
|
||||||
|
avatarUrl: extra?.avatarUrl || cachedAvatarMap[username]?.avatarUrl,
|
||||||
|
type: 'friend',
|
||||||
|
lastSessionTimestamp: Number(sessionTimestampMap.get(username) || 0),
|
||||||
|
postCount: hasCachedCount ? Math.max(0, Math.floor(cachedCount)) : undefined,
|
||||||
|
postCountStatus: hasCachedCount ? 'ready' : (forceCounts ? 'loading' : 'idle')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
contactsList = sortContactsForRanking(Array.from(nextContactMap.values()).filter(contact => (
|
||||||
|
Boolean(pickMeaningfulContactDisplayName(contact.username, contact.displayName, contact.remark, contact.nickname))
|
||||||
|
)))
|
||||||
if (requestToken !== contactsLoadTokenRef.current) {
|
if (requestToken !== contactsLoadTokenRef.current) {
|
||||||
if (activeContactsLoadTaskIdRef.current === taskId) {
|
if (activeContactsLoadTaskIdRef.current === taskId) {
|
||||||
activeContactsLoadTaskIdRef.current = null
|
activeContactsLoadTaskIdRef.current = null
|
||||||
@@ -1618,13 +1753,30 @@ export default function SnsPage() {
|
|||||||
return {
|
return {
|
||||||
...contact,
|
...contact,
|
||||||
lastSessionTimestamp: previous?.lastSessionTimestamp ?? contact.lastSessionTimestamp,
|
lastSessionTimestamp: previous?.lastSessionTimestamp ?? contact.lastSessionTimestamp,
|
||||||
postCount: previous?.postCount,
|
postCount: previous?.postCount ?? contact.postCount,
|
||||||
postCountStatus: previous?.postCountStatus ?? contact.postCountStatus
|
postCountStatus: previous?.postCountStatus ?? contact.postCountStatus
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return sortContactsForRanking(merged)
|
return sortContactsForRanking(merged)
|
||||||
})
|
})
|
||||||
|
} else if (contactsList.length > 0) {
|
||||||
|
setContacts(contactsList)
|
||||||
}
|
}
|
||||||
|
} else if (contactsList.length > 0) {
|
||||||
|
setContacts(contactsList)
|
||||||
|
}
|
||||||
|
|
||||||
|
setContactsLoading(false)
|
||||||
|
const readyUsernames = new Set(
|
||||||
|
contactsList
|
||||||
|
.filter((contact) => contact.postCountStatus === 'ready' && typeof contact.postCount === 'number')
|
||||||
|
.map((contact) => contact.username)
|
||||||
|
)
|
||||||
|
if (contactsList.length > 0) {
|
||||||
|
void hydrateContactPostCounts(
|
||||||
|
contactsList.map(contact => contact.username),
|
||||||
|
{ readyUsernames, force: forceCounts }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (activeContactsLoadTaskIdRef.current === taskId) {
|
if (activeContactsLoadTaskIdRef.current === taskId) {
|
||||||
activeContactsLoadTaskIdRef.current = null
|
activeContactsLoadTaskIdRef.current = null
|
||||||
@@ -1659,7 +1811,7 @@ export default function SnsPage() {
|
|||||||
setContactsLoading(false)
|
setContactsLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [ensureSnsUserPostCountsCacheScopeKey, hydrateContactPostCounts, sortContactsForRanking, stopContactsCountHydration])
|
}, [ensureSnsCacheScopeKey, ensureSnsUserPostCountsCacheScopeKey, hydrateContactPostCounts, sortContactsForRanking, stopContactsCountHydration])
|
||||||
|
|
||||||
const closeAuthorTimeline = useCallback(() => {
|
const closeAuthorTimeline = useCallback(() => {
|
||||||
setAuthorTimelineTarget(null)
|
setAuthorTimelineTarget(null)
|
||||||
|
|||||||
Reference in new issue
Block a user