联系人页面优化算法,同时支持获取曾经的好友;支持通过联系人页面打开聊天会话;朋友圈页面优化;支持检测并标记部分已删除的朋友圈

This commit is contained in:
cc
2026-02-21 23:06:41 +08:00
parent d49c44f3be
commit 5ab0466a87
8 changed files with 203 additions and 82 deletions

View File

@@ -281,6 +281,8 @@ function ChatPage(_props: ChatPageProps) {
const [highlightedMessageKeys, setHighlightedMessageKeys] = useState<string[]>([])
const [isRefreshingSessions, setIsRefreshingSessions] = useState(false)
const [hasInitialMessages, setHasInitialMessages] = useState(false)
const [noMessageTable, setNoMessageTable] = useState(false)
const [fallbackDisplayName, setFallbackDisplayName] = useState<string | null>(null)
const [showVoiceTranscribeDialog, setShowVoiceTranscribeDialog] = useState(false)
const [pendingVoiceTranscriptRequest, setPendingVoiceTranscriptRequest] = useState<{ sessionId: string; messageId: string } | null>(null)
@@ -857,6 +859,10 @@ function ChatPage(_props: ChatPageProps) {
if (result.success && result.messages) {
if (offset === 0) {
setMessages(result.messages)
if (result.messages.length === 0) {
setNoMessageTable(true)
setHasMoreMessages(false)
}
// 预取发送者信息:在关闭加载遮罩前处理
const unreadCount = session?.unreadCount ?? 0
@@ -929,7 +935,7 @@ function ChatPage(_props: ChatPageProps) {
}
setCurrentOffset(offset + result.messages.length)
} else if (!result.success) {
setConnectionError(result.error || '加载消息失败')
setNoMessageTable(true)
setHasMoreMessages(false)
}
} catch (e) {
@@ -1247,6 +1253,7 @@ function ChatPage(_props: ChatPageProps) {
useEffect(() => {
if (currentSessionId !== prevSessionRef.current) {
prevSessionRef.current = currentSessionId
setNoMessageTable(false)
if (initialRevealTimerRef.current !== null) {
window.clearTimeout(initialRevealTimerRef.current)
initialRevealTimerRef.current = null
@@ -1260,11 +1267,11 @@ function ChatPage(_props: ChatPageProps) {
}, [currentSessionId, messages.length, isLoadingMessages])
useEffect(() => {
if (currentSessionId && messages.length === 0 && !isLoadingMessages && !isLoadingMore) {
if (currentSessionId && messages.length === 0 && !isLoadingMessages && !isLoadingMore && !noMessageTable) {
setHasInitialMessages(false)
loadMessages(currentSessionId, 0)
}
}, [currentSessionId, messages.length, isLoadingMessages, isLoadingMore])
}, [currentSessionId, messages.length, isLoadingMessages, isLoadingMore, noMessageTable])
useEffect(() => {
return () => {
@@ -1340,10 +1347,24 @@ function ChatPage(_props: ChatPageProps) {
sortTimestamp: 0,
lastTimestamp: 0,
lastMsgType: 0,
displayName: currentSessionId,
displayName: fallbackDisplayName || currentSessionId,
} as ChatSession
})()
// 从通讯录跳转时,会话不在列表中,主动加载联系人显示名称
useEffect(() => {
if (!currentSessionId) return
const found = Array.isArray(sessions) ? sessions.find(s => s.username === currentSessionId) : undefined
if (found) {
setFallbackDisplayName(null)
return
}
loadContactInfoBatch([currentSessionId]).then(() => {
const cached = senderAvatarCache.get(currentSessionId)
if (cached?.displayName) setFallbackDisplayName(cached.displayName)
})
}, [currentSessionId, sessions])
// 判断是否为群聊
const isGroupChat = (username: string) => username.includes('@chatroom')

View File

@@ -7,8 +7,8 @@
// 左侧联系人面板
.contacts-panel {
width: 400px;
min-width: 400px;
width: 350px;
min-width: 350px;
display: flex;
flex-direction: column;
border-right: 1px solid var(--border-color);
@@ -115,11 +115,11 @@
}
.type-filters {
display: flex;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
padding: 0 20px 16px;
flex-wrap: nowrap;
overflow-x: auto;
max-width: 300px;
&::-webkit-scrollbar {
display: none;
@@ -397,6 +397,7 @@
.detail-value {
color: var(--text-primary);
word-break: break-all;
user-select: text;
}
}

View File

@@ -10,7 +10,7 @@ interface ContactInfo {
remark?: string
nickname?: string
avatarUrl?: string
type: 'friend' | 'group' | 'official' | 'deleted_friend' | 'other'
type: 'friend' | 'group' | 'official' | 'former_friend' | 'other'
}
function ContactsPage() {
@@ -21,8 +21,8 @@ function ContactsPage() {
const [searchKeyword, setSearchKeyword] = useState('')
const [contactTypes, setContactTypes] = useState({
friends: true,
groups: true,
officials: true,
groups: false,
officials: false,
deletedFriends: false
})
@@ -94,7 +94,7 @@ function ContactsPage() {
if (c.type === 'friend' && !contactTypes.friends) return false
if (c.type === 'group' && !contactTypes.groups) return false
if (c.type === 'official' && !contactTypes.officials) return false
if (c.type === 'deleted_friend' && !contactTypes.deletedFriends) return false
if (c.type === 'former_friend' && !contactTypes.deletedFriends) return false
return true
})
@@ -164,7 +164,7 @@ function ContactsPage() {
case 'friend': return <User size={14} />
case 'group': return <Users size={14} />
case 'official': return <MessageSquare size={14} />
case 'deleted_friend': return <UserX size={14} />
case 'former_friend': return <UserX size={14} />
default: return <User size={14} />
}
}
@@ -174,7 +174,7 @@ function ContactsPage() {
case 'friend': return '好友'
case 'group': return '群聊'
case 'official': return '公众号'
case 'deleted_friend': return '已删除'
case 'former_friend': return '曾经的好友'
default: return '其他'
}
}
@@ -292,7 +292,7 @@ function ContactsPage() {
</label>
<label className={`filter-chip ${contactTypes.deletedFriends ? 'active' : ''}`}>
<input type="checkbox" checked={contactTypes.deletedFriends} onChange={e => setContactTypes({ ...contactTypes, deletedFriends: e.target.checked })} />
<UserX size={16} /><span></span>
<UserX size={16} /><span></span>
</label>
</div>

View File

@@ -105,11 +105,28 @@
gap: 16px;
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.2s;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.02);
position: relative;
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.06);
}
&.post-deleted {
opacity: 0.7;
}
.post-deleted-badge {
display: flex;
align-items: center;
gap: 4px;
background: rgba(255, 77, 79, 0.1);
color: #ff4d4f;
font-size: 12px;
font-weight: 500;
padding: 3px 8px;
border-radius: 6px;
}
}
.post-avatar-col {
@@ -147,6 +164,12 @@
}
}
.post-header-actions {
display: flex;
align-items: center;
gap: 6px;
}
.debug-btn {
opacity: 0;
transition: opacity 0.2s;
@@ -313,12 +336,15 @@
width: fit-content;
height: auto;
max-width: 300px;
/* Max width constraint */
max-height: 480px;
/* Increased max height a bit */
aspect-ratio: auto;
border-radius: var(--sns-border-radius-md);
&.deleted-media {
width: 200px;
aspect-ratio: 1;
}
img,
video {
max-width: 100%;
@@ -327,7 +353,6 @@
height: auto;
object-fit: contain;
display: block;
/* Remove baseline space */
background: rgba(0, 0, 0, 0.05);
}
}
@@ -444,6 +469,22 @@
&:hover .media-download-btn {
opacity: 1;
}
&.deleted-media {
cursor: default;
opacity: 0.6;
.deleted-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
gap: 6px;
color: var(--text-tertiary);
font-size: 12px;
}
}
}
}