Bladeren bron

V1.1.6 对话列表自定义图标

liuq 4 weken geleden
bovenliggende
commit
99f53f1cff

+ 4 - 2
src/renderer/src/App.tsx

@@ -17,7 +17,8 @@ import { logger } from './utils/logger'
 import { formatFullDateTime } from './utils/timeUtils'
 import { formatApiMessageToMessage } from './utils/formatMessage'
 import { formatConversationRemark } from './utils/conversationRemark'
-import { getDefaultAvatar, getSessionAvatar } from './utils/avatarUtils'
+import { getDefaultAvatar } from './utils/avatarUtils'
+import { renderConversationAvatar } from './utils/conversationAvatar'
 import { Contact, Message, SearchResult } from './types'
 
 function App(): JSX.Element {
@@ -415,12 +416,13 @@ function App(): JSX.Element {
       const newContact: Contact = {
         id: contact.id,
         name: contact.name || contact.english_name || `用户${contact.id}`,
-        avatar: getSessionAvatar(contact.id, contact.name || contact.english_name, 40),
+        avatar: null as unknown as Contact['avatar'],
         lastMessage: undefined,
         lastMessageTime: undefined,
         unreadCount: 0,
         remarks: undefined
       }
+      newContact.avatar = renderConversationAvatar(newContact, 40)
 
       setContacts(prev => [newContact, ...prev])
       void activateContact(contact.id)

+ 8 - 4
src/renderer/src/components/LaunchpadAppIcon.tsx

@@ -1,6 +1,6 @@
 import React, { useEffect, useState } from 'react'
 import type { LaunchpadIconResolvePayload, LaunchpadIconResolveResult } from '../types/ipcLaunchpad'
-import { getAvatarPlaceholder } from '../utils/avatarUtils'
+import { getAvatarPlaceholder, type ApplicationIconShape } from '../utils/avatarUtils'
 
 function isProbablyHttpIconUrl(raw: string): boolean {
   const s = raw.trim()
@@ -15,6 +15,8 @@ interface LaunchpadAppIconProps {
   sizePx: number
   /** 与 `getAvatarPlaceholder` 的 extraStyle 一致(如 marginRight) */
   extraStyle?: React.CSSProperties
+  /** 应用中心卡片为圆角方形;会话列表/聊天为圆形 */
+  applicationIconShape?: ApplicationIconShape
 }
 
 /** 应用中心:`icon_url` 为空用渐变占位;非空则经主进程缓存后通过自定义协议展示 */
@@ -25,6 +27,7 @@ export function LaunchpadAppIcon({
   iconObjectKey,
   sizePx,
   extraStyle,
+  applicationIconShape = 'roundedSquare',
 }: LaunchpadAppIconProps): JSX.Element {
   const [resolvedSrc, setResolvedSrc] = useState<string | null>(null)
   const [usePlaceholder, setUsePlaceholder] = useState(false)
@@ -77,14 +80,15 @@ export function LaunchpadAppIcon({
     sizePx,
     extraStyle,
     'applicationIcon',
-    'roundedSquare'
+    applicationIconShape
   )
 
   if (!resolvedSrc || usePlaceholder) {
     return <>{placeholderNode}</>
   }
 
-  const br = Math.round(sizePx * 0.22)
+  const borderRadius =
+    applicationIconShape === 'circle' ? '50%' : Math.round(sizePx * 0.22)
 
   return (
     <img
@@ -97,7 +101,7 @@ export function LaunchpadAppIcon({
         height: sizePx,
         minWidth: sizePx,
         minHeight: sizePx,
-        borderRadius: br,
+        borderRadius,
         objectFit: 'cover',
         flexShrink: 0,
         display: 'block',

+ 13 - 5
src/renderer/src/hooks/useContacts.ts

@@ -1,9 +1,9 @@
-import { useState, useEffect, useRef, useCallback } from 'react'
+import { useState, useEffect, useRef, useCallback, type ReactNode } from 'react'
 import { Contact } from '../types'
 import { api } from '../services/api'
 import { logger } from '../utils/logger'
 import { formatMessageTime } from '../utils/timeUtils'
-import { getSessionAvatar } from '../utils/avatarUtils'
+import { renderConversationAvatar } from '../utils/conversationAvatar'
 
 /** silent:不显示全屏「加载联系人」;失败时跳过更新 */
 export type FetchContactsOptions = { silent?: boolean }
@@ -59,16 +59,24 @@ export function useContacts(
           rawTime != null && rawTime !== ''
             ? new Date(rawTime).getTime()
             : undefined
-        return {
+        const row: Contact = {
           id: contact.id,
           name: contact.name || `用户${contact.id}`,
-          avatar: getSessionAvatar(contact.id, contact.name, 40),
+          avatar: null as unknown as ReactNode,
           lastMessage: contact.last_message,
           lastMessageTime: rawTime ? formatMessageTime(rawTime) : undefined,
           lastMessageAt: Number.isFinite(t) ? t : undefined,
           unreadCount: contact.unread_count ?? 0,
-          remarks: contact.remarks ?? null
+          remarks: contact.remarks ?? null,
+          is_system: contact.is_system,
+          app_id: contact.app_id ?? null,
+          app_name: contact.app_name ?? null,
+          application_app_id: contact.application_app_id ?? null,
+          icon_url: contact.icon_url ?? null,
+          icon_object_key: contact.icon_object_key ?? null
         }
+        row.avatar = renderConversationAvatar(row, 40)
+        return row
       })
 
       setContacts(formattedContacts)

+ 14 - 4
src/renderer/src/hooks/useWebSocket.ts

@@ -4,7 +4,7 @@ import { logger } from '../utils/logger'
 import { isNotificationLikeMessage } from '../utils/messageTypes'
 import { formatApiMessageToMessage } from '../utils/formatMessage'
 import { Message, Contact } from '../types'
-import { getSessionAvatar } from '../utils/avatarUtils'
+import { renderConversationAvatar } from '../utils/conversationAvatar'
 import type { FetchContactsOptions } from './useContacts'
 
 interface UseWebSocketProps {
@@ -159,16 +159,26 @@ export function useWebSocket({
         
         // 对于系统通知会话,如果当前联系人列表中不存在,则在本地创建一个虚拟联系人
         if (!contact && isNotification && appId) {
-          const systemName = (incomingMsg as any).app_name || '系统通知'
+          const raw = incomingMsg as Record<string, unknown>
+          const systemName =
+            (typeof raw.app_name === 'string' && raw.app_name.trim()) ? raw.app_name : '系统通知'
           const systemContact: Contact = {
             id: targetContactId,
             name: systemName,
-            avatar: getSessionAvatar(targetContactId, systemName, 40),
+            avatar: null as Contact['avatar'],
             lastMessage: undefined,
             lastMessageTime: undefined,
             unreadCount: 0,
-            remarks: undefined
+            remarks: undefined,
+            is_system: true,
+            app_id: appId,
+            application_app_id:
+              typeof raw.application_app_id === 'string' ? raw.application_app_id : null,
+            icon_url: typeof raw.icon_url === 'string' ? raw.icon_url : null,
+            icon_object_key:
+              typeof raw.icon_object_key === 'string' ? raw.icon_object_key : null
           }
+          systemContact.avatar = renderConversationAvatar(systemContact, 40)
           
           contact = systemContact
           

+ 4 - 3
src/renderer/src/pages/ChatPage.tsx

@@ -5,7 +5,8 @@ import { MessageBubble } from '../components/ChatWindow/MessageBubble'
 import { formatFullDateTime } from '../utils/timeUtils'
 import { isNotificationLikeMessage } from '../utils/messageTypes'
 import { formatConversationRemark } from '../utils/conversationRemark'
-import { getDefaultAvatar, getSessionAvatar } from '../utils/avatarUtils'
+import { getDefaultAvatar } from '../utils/avatarUtils'
+import { renderConversationAvatar } from '../utils/conversationAvatar'
 
 interface ChatPageProps {
   contacts: Contact[]
@@ -347,7 +348,7 @@ export const ChatPage: React.FC<ChatPageProps> = ({
                   >
                     {!msg.isSelf && activeContact && (
                       <div style={{ width: '35px', height: '35px', borderRadius: '50%', marginRight: '10px', overflow: 'hidden', cursor: 'pointer', flexShrink: 0 }}>
-                        {getSessionAvatar(activeContact.id, activeContact.name, 35)}
+                        {renderConversationAvatar(activeContact, 35)}
                       </div>
                     )}
 
@@ -401,7 +402,7 @@ export const ChatPage: React.FC<ChatPageProps> = ({
                   }}>
                     {!msg.isSelf && activeContact && (
                       <div style={{ width: '35px', height: '35px', borderRadius: '50%', marginRight: '10px', overflow: 'hidden', cursor: 'pointer', flexShrink: 0 }}>
-                        {getSessionAvatar(activeContact.id, activeContact.name, 35)}
+                        {renderConversationAvatar(activeContact, 35)}
                       </div>
                     )}
                     

+ 13 - 3
src/renderer/src/services/api.ts

@@ -45,6 +45,12 @@ export interface ContactResponse {
   last_message_time?: string;
   unread_count?: number;
   remarks?: string | null;
+  is_system?: boolean;
+  app_id?: number | null;
+  app_name?: string | null;
+  application_app_id?: string | null;
+  icon_url?: string | null;
+  icon_object_key?: string | null;
 }
 
 export interface UserContact {
@@ -914,15 +920,19 @@ export const api = {
     }
 
     const data = await response.json();
-    // 转换 API 返回格式为 ContactResponse 格式
-    // API 返回格式: { user_id, username, full_name, unread_count, last_message, last_message_type, updated_at }
     return data.map((item: any) => ({
       id: item.user_id || 0,
       name: item.full_name || item.username || '未知用户',
       last_message: item.last_message,
       last_message_time: item.updated_at,
       unread_count: item.unread_count || 0,
-      remarks: item.remarks ?? null
+      remarks: item.remarks ?? null,
+      is_system: item.is_system === true,
+      app_id: item.app_id ?? null,
+      app_name: item.app_name ?? null,
+      application_app_id: item.application_app_id ?? null,
+      icon_url: item.icon_url ?? null,
+      icon_object_key: item.icon_object_key ?? null
     }));
   },
 

+ 6 - 0
src/renderer/src/types/index.ts

@@ -27,6 +27,12 @@ export interface Contact {
   lastMessageAt?: number
   unreadCount: number
   remarks?: string | null
+  is_system?: boolean
+  app_id?: number | null
+  app_name?: string | null
+  application_app_id?: string | null
+  icon_url?: string | null
+  icon_object_key?: string | null
 }
 
 export interface SearchResult {

+ 40 - 0
src/renderer/src/utils/conversationAvatar.tsx

@@ -0,0 +1,40 @@
+import React from 'react'
+import type { Contact } from '../types'
+import { LaunchpadAppIcon } from '../components/LaunchpadAppIcon'
+import { getSessionAvatar } from './avatarUtils'
+import { launchpadIconKeyForContact } from './launchpadIconKey'
+
+function isAppNotificationSession(contact: Contact): boolean {
+  if ((contact.id ?? 0) < 0) return true
+  return contact.is_system === true && contact.app_id != null
+}
+
+/**
+ * 会话列表 / 聊天区头像:应用通知走 Launchpad 缓存与自定义协议;普通人会话走文字头像。
+ */
+export function renderConversationAvatar(
+  contact: Contact,
+  sizePx: number,
+  extraStyle?: React.CSSProperties
+): React.ReactNode {
+  if (!isAppNotificationSession(contact)) {
+    return getSessionAvatar(contact.id, contact.name, sizePx, extraStyle)
+  }
+
+  const appId = launchpadIconKeyForContact(contact)
+  if (!appId) {
+    return getSessionAvatar(contact.id, contact.name, sizePx, extraStyle)
+  }
+
+  return (
+    <LaunchpadAppIcon
+      appName={contact.name}
+      appId={appId}
+      iconUrl={contact.icon_url}
+      iconObjectKey={contact.icon_object_key}
+      sizePx={sizePx}
+      extraStyle={extraStyle}
+      applicationIconShape="circle"
+    />
+  )
+}

+ 18 - 0
src/renderer/src/utils/launchpadIconKey.ts

@@ -0,0 +1,18 @@
+import type { Contact } from '../types'
+
+/** 与主进程 `toSafeLaunchpadAppId` 一致:长度 ≤128 的 [a-zA-Z0-9_-]+ */
+export function launchpadIconKeyForContact(
+  c: Pick<Contact, 'id' | 'app_id' | 'application_app_id'>
+): string | null {
+  const fromBiz = c.application_app_id?.trim() ?? ''
+  if (fromBiz && /^[a-zA-Z0-9_-]+$/.test(fromBiz) && fromBiz.length <= 128) {
+    return fromBiz
+  }
+  if (c.app_id != null) {
+    return String(c.app_id)
+  }
+  if (c.id < 0) {
+    return String(Math.abs(c.id))
+  }
+  return null
+}