feat: add GlobalSearch components and styles for improved search functionality

This commit is contained in:
tt 2025-02-26 10:38:19 +08:00
parent 559812ec8b
commit 16fc0b6221
27 changed files with 1135 additions and 55 deletions

View File

@ -480,12 +480,12 @@ export class Conversation extends Component<ConversationProps> implements Conver
}
render() {
const { chatBg, channel } = this.props
const { chatBg, channel,initLocateMessageSeq } = this.props
const channelInfo = WKSDK.shared().channelManager.getChannelInfo(channel)
return <Provider create={() => {
this.vm = new ConversationVM(channel)
this.vm = new ConversationVM(channel,initLocateMessageSeq)
return this.vm
}} render={(vm: ConversationVM) => {
return <>

View File

@ -24,7 +24,7 @@ export default class ConversationVM extends ProviderListener {
currentConversation?: Conversation // 当前最近会话
messagesOfOrigin: MessageWrap[] = [] // 原始消息集合(不包含时间消息等本地消息)
browseToMessageSeq: number = 0 // 已经预览到的最新的messageSeq
initLocateMessageSeq: number = 0 // 初始定位的消息messageSeq 0为不定位
initLocateMessageSeq?: number = 0 // 初始定位的消息messageSeq 0为不定位
shouldShowHistorySplit: boolean = false // 是否应该显示历史消息分割线
private _editOn: boolean = false // 是否开启编辑模式
orgUnreadCount: number = 0 // 原未读数量
@ -57,10 +57,14 @@ export default class ConversationVM extends ProviderListener {
onFirstMessagesLoaded?: Function // 第一屏消息已加载完成
constructor(channel: Channel) {
constructor(channel: Channel, initLocateMessageSeq?: number) {
super()
this.channel = channel
// this.initLocateMessageSeq = initLocateMessageSeq
if(initLocateMessageSeq==0) {
this.initLocateMessageSeq = undefined
}else {
this.initLocateMessageSeq = initLocateMessageSeq
}
}
get currentReplyMessage() {
@ -131,7 +135,7 @@ export default class ConversationVM extends ProviderListener {
}
}
// 选中消息
checkedMessage(message: Message, checked: boolean): void {
let messageWrap = this.findMessageWithClientMsgNo(message.clientMsgNo)
@ -255,11 +259,11 @@ export default class ConversationVM extends ProviderListener {
didMount(): void {
this.conversationListener = (conversation: Conversation, action: ConversationAction) => {
if(!conversation.channel.isEqual(this.channel)) {
if (!conversation.channel.isEqual(this.channel)) {
return
}
if(action == ConversationAction.update) {
console.log("update-2--->",conversation.unread)
if (action == ConversationAction.update) {
console.log("update-2--->", conversation.unread)
this.unreadCount = conversation.unread
}
}
@ -315,8 +319,8 @@ export default class ConversationVM extends ProviderListener {
WKApp.endpointManager.setMethod(EndpointID.clearChannelMessages, (channel: Channel) => {
if (channel.isEqual(this.channel)) {
if(this.messagesOfOrigin.length > 0) {
this.browseToMessageSeq = this.messagesOfOrigin[this.messagesOfOrigin.length-1].messageSeq
if (this.messagesOfOrigin.length > 0) {
this.browseToMessageSeq = this.messagesOfOrigin[this.messagesOfOrigin.length - 1].messageSeq
}
this.messagesOfOrigin = []
this.messages = []
@ -364,7 +368,7 @@ export default class ConversationVM extends ProviderListener {
this.orgUnreadCount = unread
this.unreadCount = unread
this.currentConversation = conversation
this.shouldShowHistorySplit = unread > 0
if (unread > 0) {
@ -383,7 +387,7 @@ export default class ConversationVM extends ProviderListener {
WKSDK.shared().conversationManager.openConversation = conversation
}
this.requestMessagesOfFirstPage(undefined, () => {
this.requestMessagesOfFirstPage(this.initLocateMessageSeq, () => {
if (this.onFirstMessagesLoaded) {
this.onFirstMessagesLoaded()
}
@ -404,8 +408,8 @@ export default class ConversationVM extends ProviderListener {
}
// 加载频道信息完成
async loadChannelInfoFinished() {
if(this.channel.channelType !== ChannelTypeGroup) {
async loadChannelInfoFinished() {
if (this.channel.channelType !== ChannelTypeGroup) {
return
}
this.reloadSubscribers()
@ -416,25 +420,25 @@ export default class ConversationVM extends ProviderListener {
this.reloadSubscribers()
})
if(this.channelInfo?.orgData?.group_type == SuperGroup) {
if (this.channelInfo?.orgData?.group_type == SuperGroup) {
// 如果是超级群则只获取第一页成员
this.subscribers = await this.getFirstPageMembers()
WKSDK.shared().channelManager.subscribeCacheMap.set(this.channel.getChannelKey(), this.subscribers)
WKSDK.shared().channelManager.notifySubscribeChangeListeners(this.channel)
this.notifyListener()
}else {
this.subscribers = await this.getFirstPageMembers()
WKSDK.shared().channelManager.subscribeCacheMap.set(this.channel.getChannelKey(), this.subscribers)
WKSDK.shared().channelManager.notifySubscribeChangeListeners(this.channel)
this.notifyListener()
} else {
WKSDK.shared().channelManager.syncSubscribes(this.channel)
}
}
// 获取第一页成员列表(超大群)
getFirstPageMembers() {
return WKApp.dataSource.channelDataSource.subscribers(this.channel,{
limit: 100,
page: 1
})
return WKApp.dataSource.channelDataSource.subscribers(this.channel, {
limit: 100,
page: 1
})
}
// 标记提醒已完成
@ -690,7 +694,7 @@ export default class ConversationVM extends ProviderListener {
// 刷新新消息数量
refreshNewMsgCount() {
const oldUnreadCount = this.unreadCount
if (this.browseToMessageSeq == 0) {
this.unreadCount = 0
@ -1023,7 +1027,7 @@ export default class ConversationVM extends ProviderListener {
}
}
newMessages.push(message)
if (shouldShowHistorySplit && this.initLocateMessageSeq > 0 && message.messageSeq === this.initLocateMessageSeq) {
if (shouldShowHistorySplit && this.initLocateMessageSeq && this.initLocateMessageSeq > 0 && message.messageSeq === this.initLocateMessageSeq) {
newMessages.push(new MessageWrap(this.getHistorySplit()))
}
}

View File

@ -0,0 +1,126 @@
import { Component, ReactNode } from "react";
import React from "react";
import { Input } from '@douyinfe/semi-ui';
import { IconSearch } from '@douyinfe/semi-icons';
import { Tabs } from '@douyinfe/semi-ui';
import Provider from "../../Service/Provider";
import GlobalSearchVM from "./vm";
import TabAll from "./tab-all";
import TabContacts from "./tab-contacts";
import TabGroup from "./tab-group";
import TabFile from "./tab-file";
import { Channel } from "wukongimjssdk";
interface GlobalSearchProps {
channel?: Channel; // 查询指定频道的聊天记录
// item点击事件传递item和typetype为contacts、group、message,file
onClick?: (item: any, type: string) => void;
}
export default class GlobalSearch extends Component<GlobalSearchProps> {
vm!: GlobalSearchVM
tabPanel(key: string) {
// message
if (key === 'all') {
return <TabAll
searchResult={this.vm.searchResult}
keyword={this.vm.keyword}
loadMore={() => {
this.vm.loadMore()
}}
onClick={(item, type) => {
if (this.props.onClick) {
this.props.onClick(item, type)
}
}}
/>
}
// contacts
if (key === 'contacts') {
return <TabContacts
friends={this.vm.searchResult?.friends}
keyword={this.vm.keyword}
onClick={(item) => {
if (this.props.onClick) {
this.props.onClick(item, "contacts")
}
}}
></TabContacts>
}
// groups
if (key === 'groups') {
return <TabGroup
groups={this.vm.searchResult?.groups}
keyword={this.vm.keyword}
onClick={(item) => {
if (this.props.onClick) {
this.props.onClick(item, "group")
}
}}
></TabGroup>
}
// files
if (key === 'files') {
return <TabFile
files={this.vm.searchResult?.messages}
keyword={this.vm.keyword}
loadMore={() => {
this.vm.loadMore()
}}
onClick={(item) => {
if (this.props.onClick) {
this.props.onClick(item, "file")
}
}}
/>
}
}
render(): ReactNode {
const { channel } = this.props;
return <Provider
create={() => {
this.vm = new GlobalSearchVM()
this.vm.channel = channel
return this.vm
}}
render={(vm: GlobalSearchVM) => {
return <div>
{
vm.searchInChannel ? <div style={{ fontSize: "14px", fontWeight: "500",width:"100%",textAlign:"center",marginBottom: "10px" }}>{vm.searchTitle}</div> : undefined
}
<Input
prefix={<IconSearch />}
showClear
style={{ height: "40px" }}
onCompositionStart={() => { vm.isComposing = true; }}
onCompositionEnd={(e: any) => {
vm.isComposing = false;
vm.handleInputChange(e.target.value);
}}
onChange={(value) => {
vm.handleInputChange(value);
}}></Input>
<div className="wk-search-tabs">
<Tabs
tabList={vm.tabList}
onChange={key => {
vm.onTabClick(key);
}}
>
{this.tabPanel(vm.selectedTabKey)}
</Tabs>
</div>
</div>
}}>
</Provider>
}
}

View File

@ -0,0 +1,34 @@
.wk-item-contacts {
display: flex;
align-items: center;
padding: 10px;
border-radius: 4px;
cursor: pointer;
}
.wk-item-contacts:hover {
background-color: #f0f0f0; /* Change this color to your desired hover color */
}
body[theme-mode=dark] .wk-item-contacts:hover {
background-color: #333333; /* Change this color to your desired hover color */
}
.wk-item-contacts-name {
margin-left: 15px;
color: var(--wk-text-item);
font-size: 14px;
font-weight: 500;
}
body[theme-mode=dark] .wk-item-contacts-name {
color: white;
}
mark {
background-color: transparent; /* 移除默认背景色 */
color: var(--wk-color-theme); /* 设置文本颜色为红色,可以根据需要更改 */
}

View File

@ -0,0 +1,23 @@
import React from "react";
import { Component, ReactNode } from "react";
import WKAvatar from "../WKAvatar";
import "./item-contacts.css"
interface ItemContactsProps {
avatar: string;
name: string;
onClick?: () => void;
}
export default class ItemContacts extends Component<ItemContactsProps> {
render(): ReactNode {
return <div className="wk-item-contacts" onClick={()=>{
if(this.props.onClick){
this.props.onClick()
}
}}>
<WKAvatar src={this.props.avatar} style={{width:"40px",height:"40px"}}></WKAvatar>
<div className="wk-item-contacts-name" dangerouslySetInnerHTML={{ __html: this.props.name }}></div>
</div>
}
}

View File

@ -0,0 +1,60 @@
.wk-item-file-icon {
width: 42px;
height: 42px;
display: flex;
justify-content: center;
align-items: center;
}
.wk-item-file {
display: grid;
grid-template-columns: auto 1fr;
grid-template-rows: auto auto; /* 两行 */
gap: 0px;
padding: 10px;
border-radius: 4px;
}
.wk-item-file:hover {
background-color: #f0f0f0; /* Change this color to your desired hover color */
}
body[theme-mode=dark] .wk-item-file:hover {
background-color: #333333; /* Change this color to your desired hover color */
}
.wk-item-file-icon {
grid-column: 1 / 2; /* 第一列 */
grid-row: 1 / 3; /* 第一行 */
}
.wk-item-file-name {
grid-column: 2 / 3; /* 第二列 */
grid-row: 1 / 2; /* 第一行 */
margin-left: 15px;
color: var(--wk-text-item);
font-size: 14px;
font-weight: 500;
}
.wk-item-file-desc {
grid-row: 2 / 3; /* 第二行 */
grid-column: 2 / 3; /* 第二列 */
display: flex;
margin-left: 15px;
color: #666;
font-size: 12px;
align-items: center;
}
.wk-item-file-line {
width: 1px;
height: 8px;
background-color: #666;
margin-top: 10px;
margin-bottom: 10px;
margin: 4px;
}

View File

@ -0,0 +1,36 @@
import React from "react";
import { Component, ReactNode } from "react";
import "./item-file.css"
import FileHelper from "../../Utils/filehelper";
import { getTimeStringAutoShort2 } from "../../Utils/time";
interface ItemFileProps {
message: any;
sender?: string;
onClick?: () => void;
}
export default class ItemFile extends Component<ItemFileProps> {
render(): ReactNode {
const file = this.props.message.payload;
const channel = this.props.message.channel;
const realName = file.name?.replaceAll("<mark>", "").replaceAll("</mark>", "");
const fileIconInfo = FileHelper.getFileIconInfo(realName);
return <div className="wk-item-file" onClick={() => {
if (this.props.onClick) {
this.props.onClick()
}
}}>
<div className="wk-item-file-icon" style={{ backgroundColor: fileIconInfo?.color, borderRadius: "4px" }}>
<img alt="" src={fileIconInfo?.icon} style={{ width: '32px', height: '32px' }} />
</div>
<div className="wk-item-file-name" dangerouslySetInnerHTML={{ __html: file.name }}></div>
<div className="wk-item-file-desc">
<div className="wk-item-file-sender">{this.props.sender}</div><div className="wk-item-file-line" />
<div className="wk-item-file-recv">{channel.channel_name}</div><div className="wk-item-file-line" />
<div className="wk-item-file-size">{FileHelper.getFileSizeFormat(file.size || 0)}</div><div className="wk-item-file-line" />
<div className="wk-item-file-time">{getTimeStringAutoShort2(this.props.message.timestamp * 1000, true)}</div>
</div>
</div>
}
}

View File

@ -0,0 +1,32 @@
.wk-item-group {
display: flex;
align-items: center;
padding: 10px;
border-radius: 4px;
cursor: pointer;
}
body[theme-mode=dark] .wk-item-group:hover {
background-color: #333333; /* Change this color to your desired hover color */
}
.wk-item-group:hover {
background-color: #f0f0f0; /* Change this color to your desired hover color */
}
.wk-item-group-name {
margin-left: 15px;
color: #333;
font-size: 14px;
font-weight: 500;
}
body[theme-mode=dark] .wk-item-group-name {
color: white;
}

View File

@ -0,0 +1,23 @@
import React from "react";
import { Component, ReactNode } from "react";
import WKAvatar from "../WKAvatar";
import "./item-group.css"
interface ItemGroupProps {
avatar: string;
name: string;
onClick?: () => void;
}
export default class ItemGroup extends Component<ItemGroupProps> {
render(): ReactNode {
return <div className="wk-item-group" onClick={()=>{
if(this.props.onClick){
this.props.onClick()
}
}}>
<WKAvatar src={this.props.avatar} style={{width:"40px",height:"40px"}}></WKAvatar>
<div className="wk-item-group-name" dangerouslySetInnerHTML={{ __html: this.props.name }}></div>
</div>
}
}

View File

@ -0,0 +1,53 @@
.wk-item-message {
display: flex;
align-items: center;
padding: 10px;
border-radius: 4px;
cursor: pointer;
}
.wk-item-message:hover {
background-color: #f0f0f0; /* Change this color to your desired hover color */
}
body[theme-mode=dark] .wk-item-message:hover {
background-color: #333333; /* Change this color to your desired hover color */
}
.wk-item-message-content {
display: grid;
grid-template-columns: 1fr; /* 两列,左边自适应,右边自动宽度 */
grid-template-rows: auto auto; /* 两行 */
gap: 0px;
min-width: 200px;
max-width: 420px;
}
.wk-item-message-name {
margin-left: 15px;
color: #333;
font-size: 14px;
font-weight: 500;
grid-column: 1 / 2; /* 第一列 */
grid-row: 1 / 2; /* 第一行 */
}
body[theme-mode=dark] .wk-item-message-name {
color: white;
}
.wk-item-message-digest {
margin-left: 15px;
color: #999;
white-space: nowrap; /* 不换行 */
overflow: hidden; /* 超出部分隐藏 */
text-overflow: ellipsis; /* 超出部分以省略号显示 */
}
mark {
background-color: transparent; /* 移除默认背景色 */
color: var(--wk-color-theme); /* 设置文本颜色为红色,可以根据需要更改 */
}

View File

@ -0,0 +1,35 @@
import React from "react";
import { Component, ReactNode } from "react";
import WKAvatar from "../WKAvatar";
import "./item-message.css"
interface ItemMessageProps {
avatar: string; // 会话头像
name: string; // 会话名字
digest: string; // 消息摘要
sender?: string; // 发送者
onClick?: () => void;
}
export default class ItemMessage extends Component<ItemMessageProps> {
render(): ReactNode {
let digest = this.props?.digest
if(this.props.sender && this.props.sender !== ""){
digest = this.props.sender + ": " + digest
}
return <div className="wk-item-message" onClick={() => {
if (this.props.onClick) {
this.props.onClick()
}
} }>
<WKAvatar src={this.props.avatar} style={{ width: "40px", height: "40px" }}></WKAvatar>
<div className="wk-item-message-content">
<div className="wk-item-message-name">{this.props.name}</div>
{/* <div className="wk-item-message-time">{this.props.time}</div> */}
<div className="wk-item-message-digest" dangerouslySetInnerHTML={{ __html: digest}}></div>
</div>
</div>
}
}

View File

@ -0,0 +1,17 @@
import React from "react";
import { Component, ReactNode } from "react";
interface SectionProps {
title: string;
children?: ReactNode;
}
export default class Section extends Component<SectionProps> {
render(): ReactNode {
return <div>
<div style={{color:"#666",marginLeft:"10px",marginTop:"10px"}}>{this.props.title}</div>
{this.props.children}
</div>
}
}

View File

@ -0,0 +1,9 @@
.wk-tab-all {
width: 100%;
height: 50vh;
display: flex;
overflow: auto;
flex-direction: column;
}

View File

@ -0,0 +1,131 @@
import React, { Component } from "react";
import { ReactNode } from "react";
import Section from "./section";
import ItemContacts from "./item-contacts";
import ItemGroup from "./item-group";
import ItemMessage from "./item-message";
import WKApp from "../../App";
import "./tab-all.css"
import WKSDK, { Channel, ChannelTypePerson, MessageContentType } from "wukongimjssdk";
import { MessageContentTypeConst } from "../../Service/Const";
interface TabAllProps {
keyword?: string;
searchResult?: any;
loadMore?: () => void; // 添加加载更多的回调函数
// item点击事件传递item和typetype为contacts、group、message
onClick?: (item: any, type: string) => void;
}
export default class TabAll extends Component<TabAllProps> {
handleScroll = (event: React.UIEvent<HTMLDivElement>) => {
const { scrollTop, scrollHeight, clientHeight } = event.currentTarget;
if (scrollTop + clientHeight >= scrollHeight) {
if (this.props.loadMore) {
this.props.loadMore();
}
}
};
render(): ReactNode {
let existFriends = this.props.searchResult?.friends.length > 0
let existGroups = this.props.searchResult?.groups.length > 0
let existMessages = this.props.searchResult?.messages.length > 0
return <div className="wk-tab-all" onScroll={this.handleScroll}>
{
existFriends ? (<Section title="联系人">
{
this.props.searchResult.friends.map((item: any) => {
return <ItemContacts
key={item.channel_id}
name={item.channel_name}
avatar={WKApp.shared.avatarUser(item.channel_id)}
onClick={() => {
if (this.props.onClick) {
this.props.onClick(item, "contacts")
}
}}
/>
})
}
</Section>) : null
}
{
existGroups ? (
<Section title="群组">
{
this.props.searchResult?.groups.map((item: any) => {
if (this.props.keyword && item.channel_name.indexOf(this.props.keyword) !== -1) {
item.channel_name = item.channel_name.replace(this.props.keyword, `<mark>${this.props.keyword}</mark>`)
}
return <ItemGroup
key={item.channel_id}
name={item.channel_name}
avatar={WKApp.shared.avatarGroup(item.channel_id)}
onClick={() => {
if (this.props.onClick) {
this.props.onClick(item, "group")
}
}}
/>
})
}
</Section>
) : null
}
{
existMessages ? (
<Section title="消息">
{
this.props.searchResult?.messages.map((item: any) => {
let digest = "[未知消息]"
if(item.content) {
digest = item.content.conversationDigest
}else {
if (item.payload.type === MessageContentType.text) {
digest = item.payload.content
} else if (item.payload.type === MessageContentTypeConst.file) {
digest = `[${item.payload.name}]`
}
}
let sender;
if (item.channel.channel_type !== ChannelTypePerson && item.from_uid && item.from_uid !== "") {
const senderChannel = new Channel(item.from_uid, ChannelTypePerson)
const channelInfo = WKSDK.shared().channelManager.getChannelInfo(senderChannel)
if (channelInfo) {
sender = channelInfo.title
} else {
WKSDK.shared().channelManager.fetchChannelInfo(senderChannel)
}
}
return <ItemMessage
key={item.message_idstr}
sender={sender}
digest={digest}
name={item.channel.channel_name}
avatar={WKApp.shared.avatarChannel(new Channel(item.channel.channel_id, item.channel.channel_type))}
onClick={() => {
if (this.props.onClick) {
this.props.onClick(item, "message")
}
}}
/>
})
}
</Section>
) : null
}
</div>
}
}

View File

@ -0,0 +1,8 @@
.wk-tab-contacts {
width: 100%;
height: 50vh;
display: flex;
overflow: auto;
flex-direction: column;
}

View File

@ -0,0 +1,37 @@
import React, { Component } from "react";
import { ReactNode } from "react";
import ItemContacts from "./item-contacts";
import WKApp from "../../App";
import "./tab-contacts.css"
interface TabContactsProps {
keyword?: string;
friends?: any[];
onClick?: (item: any) => void;
}
export default class TabContacts extends Component<TabContactsProps> {
render(): ReactNode {
return <div className="wk-tab-contacts">
{
this.props.friends?.map((item: any) => {
if (this.props.keyword && item.channel_name.indexOf(this.props.keyword) !== -1) {
item.channel_name = item.channel_name.replace(this.props.keyword, `<mark>${this.props.keyword}</mark>`)
}
return <ItemContacts
key={item.channel_id}
name={item.channel_name}
avatar={WKApp.shared.avatarUser(item.channel_id)}
onClick={()=>{
if(this.props.onClick) {
this.props.onClick(item)
}
}}
/>
})
}
</div>
}
}

View File

@ -0,0 +1,8 @@
.wk-tab-file {
width: 100%;
height: 50vh;
display: flex;
overflow: auto;
flex-direction: column;
cursor: pointer;
}

View File

@ -0,0 +1,52 @@
import React, { Component } from "react";
import { ReactNode } from "react";
import ItemFile from "./item-file";
import WKApp from "../../App";
import "./tab-file.css"
import WKSDK, { Channel, ChannelTypePerson } from "wukongimjssdk";
interface TabFileProps {
keyword?: string;
files?: any[];
loadMore?: () => void; // 添加加载更多的回调函数
onClick?: (item: any) => void;
}
export default class TabFile extends Component<TabFileProps> {
handleScroll = (event: React.UIEvent<HTMLDivElement>) => {
const { scrollTop, scrollHeight, clientHeight } = event.currentTarget;
if (scrollTop + clientHeight >= scrollHeight) {
if (this.props.loadMore) {
this.props.loadMore();
}
}
};
render(): ReactNode {
return <div className="wk-tab-file" onScroll={this.handleScroll}>
{
this.props.files?.map((item: any) => {
let sender;
const senderChannel = new Channel(item.from_uid, ChannelTypePerson)
const channelInfo = WKSDK.shared().channelManager.getChannelInfo(senderChannel)
if (channelInfo) {
sender = channelInfo.title
} else {
WKSDK.shared().channelManager.fetchChannelInfo(senderChannel)
}
return <ItemFile
key={item.message_idstr}
sender={sender}
message={item}
onClick={()=>{
if(this.props.onClick) {
this.props.onClick(item)
}
}}
/>
})
}
</div>
}
}

View File

@ -0,0 +1,9 @@
.wk-tab-group {
width: 100%;
height: 50vh;
display: flex;
overflow: auto;
flex-direction: column;
}

View File

@ -0,0 +1,37 @@
import React, { Component } from "react";
import { ReactNode } from "react";
import ItemGroup from "./item-group";
import WKApp from "../../App";
import "./tab-group.css"
interface TabGroupProps {
keyword?: string;
groups?: any[];
onClick?: (item: any) => void;
}
export default class TabGroup extends Component<TabGroupProps> {
render(): ReactNode {
return <div className="wk-tab-group">
{
this.props.groups?.map((item: any) => {
if (this.props.keyword && item.channel_name.indexOf(this.props.keyword) !== -1) {
item.channel_name = item.channel_name.replace(this.props.keyword, `<mark>${this.props.keyword}</mark>`)
}
return <ItemGroup
key={item.channel_id}
name={item.channel_name}
avatar={WKApp.shared.avatarGroup(item.channel_id)}
onClick={()=>{
if(this.props.onClick) {
this.props.onClick(item)
}
}}
/>
})
}
</div>
}
}

View File

@ -0,0 +1,219 @@
import WKSDK, { Channel, ChannelInfo, ChannelInfoListener, ChannelTypePerson, MessageContentManager, SystemContent } from "wukongimjssdk";
import APIClient from "../../Service/APIClient";
import { MessageContentTypeConst } from "../../Service/Const";
import { ProviderListener } from "../../Service/Provider";
export default class GlobalSearchVM extends ProviderListener {
// 选中的tab组件
private _selectedTabKey = "all";
public page = 1 // 当前页码
public limit = 20 // 每页条数
public keyword = "" // 搜索关键字
public searchResult: any
public isComposing: boolean = false; // 是否正在输入(防止中文输入法干扰)
public loadMoreing = false; // 是否正在加载更多中
public loadFinish = false; // 是否加载完成
public contentTypes = new Array<number>() // 内容类型
private channelInfoListener!: ChannelInfoListener;
public channel?: Channel // 查询指定频道的消息
// tab数据列表
public get tabList() {
if (this.searchInChannel) {
return [
{ tab: '聊天', itemKey: 'all' },
{ tab: '文件', itemKey: 'files' },
];
}
return [
{ tab: '聊天', itemKey: 'all' },
{ tab: '联系人', itemKey: 'contacts' },
{ tab: '群组', itemKey: 'groups' },
{ tab: '文件', itemKey: 'files' },
];
}
public get selectedTabKey() {
return this._selectedTabKey;
}
public set selectedTabKey(value: string) {
this._selectedTabKey = value;
this.notifyListener()
}
// 是否在频道内搜索
public get searchInChannel(): boolean {
return this.channel !== undefined
}
// 搜索标题
public get searchTitle() {
if (this.searchInChannel) {
const channelInfo = WKSDK.shared().channelManager.getChannelInfo(this.channel!)
if(channelInfo) {
return `与“${channelInfo.title}”的聊天记录`
}
return ""
}
return undefined
}
// tab选中事件
public onTabClick(key: string) {
if (key === "files") {
this.contentTypes = [MessageContentTypeConst.file]
this.initLoad()
this.requestSearch()
} else {
this.contentTypes = []
this.initLoad()
this.requestSearch()
}
this.selectedTabKey = key;
}
didMount(): void {
this.requestSearch()
this.channelInfoListener = (channelInfo: ChannelInfo) => {
if (channelInfo.channel.channelType !== ChannelTypePerson) {
return
}
if (this.searchResult?.messages && this.searchResult.messages.length > 0) {
this.searchResult.messages.forEach((item: any) => {
if (item.from_uid === channelInfo.channel.channelID) {
this.notifyListener()
return
}
})
}
}
WKSDK.shared().channelManager.addListener(this.channelInfoListener)
}
didUnMount(): void {
WKSDK.shared().channelManager.removeListener(this.channelInfoListener)
}
// 输入框输入事件
public handleInputChange = (value: string) => {
if (!this.isComposing) {
this.keyword = value;
console.log(this.keyword);
this.initLoad()
this.requestSearch();
}
};
public initLoad() {
this.page = 1
this.loadFinish = false
this.loadMoreing = false
this.searchResult = null
this.notifyListener()
}
// 请求搜索
public requestSearch() {
const param: any = {
keyword: this.keyword || "",
page: this.page,
limit: this.limit,
content_type: this.contentTypes
}
if (this.channel) {
param.channel_id = this.channel.channelID
param.channel_type = this.channel.channelType
param.only_message = 1
}
console.log("requestSearch", param);
APIClient.shared.post("/search/global", param).then(res => {
if (res.messages.length < this.limit) {
this.loadFinish = true
}
if (this.loadMoreing) {
if (this.searchResult) {
this.searchResult.messages = this.searchResult.messages?.concat(res.messages)
} else {
this.searchResult = res
}
} else {
this.searchResult = res
}
// 替换备注如果有备注的话
this.searchResult.friends?.forEach((v: any) => {
if (v.channel_remark && v.channel_remark !== "") {
v.channel_name = v.channel_remark
}
})
this.searchResult.groups?.forEach((v: any) => {
if (v.channel_remark && v.channel_remark !== "") {
v.channel_name = v.channel_remark
}
})
this.searchResult.messages?.forEach((v: any) => {
if (v.channel.channel_remark && v.channel.channel_remark !== "") {
v.channel.channel_name = v.channel.channel_remark
}
// 解析消息内容
if(v.payload) {
const contentType = v.payload.type
const messageContent = MessageContentManager.shared().getMessageContent(contentType)
if (messageContent) {
messageContent.decode(this.jsonToUint8Array(v.payload))
}
if(messageContent instanceof SystemContent) {
messageContent.content["content"] = "[系统消息]"
}
v.content = messageContent
}
})
}).finally(() => {
this.loadMoreing = false
this.notifyListener()
})
}
jsonToUint8Array(json: any): Uint8Array {
// 将 JSON 对象转换为字符串
const jsonString = JSON.stringify(json);
return this.stringToUint8Array(jsonString)
}
stringToUint8Array(str: string): Uint8Array {
const newStr = unescape(encodeURIComponent(str))
const arr = new Array<number>();
for (let i = 0, j = newStr.length; i < j; ++i) {
arr.push(newStr.charCodeAt(i));
}
const tmpUint8Array = new Uint8Array(arr);
return tmpUint8Array
}
// 加载更多消息
loadMore() {
if (this.loadMoreing) {
return
}
this.loadMoreing = true
this.page++
this.requestSearch()
console.log("加载更多");
}
}

View File

@ -35,6 +35,7 @@ export class GlobalModalOptions {
footer?: ReactNode;
className?: string;
closable?: boolean;
onCancel?: () => void;
}
export interface WKBaseProps {
@ -248,6 +249,7 @@ export default class WKBase
visible={this.state.showGlobalModal}
width={this.state.globalModalOptions?.width}
footer={this.state.globalModalOptions?.footer}
onCancel={this.state.globalModalOptions?.onCancel}
>
{this.state.globalModalOptions?.body}
</Modal>

View File

@ -11,6 +11,11 @@ export class MessageContextMenus {
onClick?: () => void;
}
export class ShowConversationOptions {
// 聊天消息定位的messageSeq
initLocateMessageSeq?: number;
}
export class EndpointCommon {
private _onLogins: VoidFunction[] = []; // 登录成功
@ -32,10 +37,11 @@ export class EndpointCommon {
}
}
showConversation(channel: Channel) {
showConversation(channel: Channel, opts?: ShowConversationOptions) {
WKApp.shared.openChannel = channel;
EndpointManager.shared.invoke(EndpointID.showConversation, {
channel: channel,
opts: opts,
});
WKApp.shared.notifyListener();
}
@ -65,21 +71,38 @@ export class EndpointCommon {
EndpointID.showConversation,
(param: any) => {
const channel = param.channel as Channel;
const conversation =
WKSDK.shared().conversationManager.findConversation(channel);
let initLocateMessageSeq = 0;
if (
conversation &&
conversation.lastMessage &&
conversation.unread > 0 &&
conversation.lastMessage.messageSeq > conversation.unread
) {
initLocateMessageSeq =
conversation.lastMessage.messageSeq - conversation.unread;
let opts: ShowConversationOptions = {}
if (param.opts) {
opts = param.opts
}
let initLocateMessageSeq = 0;
if (opts && opts.initLocateMessageSeq && opts.initLocateMessageSeq > 0) {
initLocateMessageSeq = opts.initLocateMessageSeq;
}
if (initLocateMessageSeq <= 0) {
const conversation =
WKSDK.shared().conversationManager.findConversation(channel);
if (
conversation &&
conversation.lastMessage &&
conversation.unread > 0 &&
conversation.lastMessage.messageSeq > conversation.unread
) {
initLocateMessageSeq =
conversation.lastMessage.messageSeq - conversation.unread;
}
}
let key = channel.getChannelKey()
if (initLocateMessageSeq > 0) {
key = `${key}-${initLocateMessageSeq}`
}
WKApp.routeRight.replaceToRoot(
<ChatContentPage
key={channel.getChannelKey()}
key={key}
channel={channel}
initLocateMessageSeq={initLocateMessageSeq}
></ChatContentPage>

View File

@ -3,9 +3,9 @@ import { Conversation } from "../../Components/Conversation";
import ConversationList from "../../Components/ConversationList";
import Provider from "../../Service/Provider";
import { Spin, Button, Popover } from "@douyinfe/semi-ui";
import { IconPlus } from "@douyinfe/semi-icons";
import { ChatVM } from "./vm";
import { Spin, Modal, Popover } from "@douyinfe/semi-ui";
import { IconPlus, IconSearch } from "@douyinfe/semi-icons";
import { ChatVM, handleGlobalSearchClick } from "./vm";
import "./index.css";
import { ConversationWrap } from "../../Service/Model";
import WKApp, { ThemeMode } from "../../App";
@ -15,11 +15,12 @@ import { Channel, ChannelInfo, WKSDK } from "wukongimjssdk";
import { ChannelInfoListener } from "wukongimjssdk";
import { ChatMenus } from "../../App";
import ConversationContext from "../../Components/Conversation/context";
import { EndpointID } from "../../Service/Const";
import GlobalSearch from "../../Components/GlobalSearch";
import { ShowConversationOptions } from "../../EndpointCommon";
export interface ChatContentPageProps {
channel: Channel;
initLocateMessageSeq?: number;
initLocateMessageSeq?: number; // 打开时定位到某条消息
}
export interface ChatContentPageState {
@ -180,7 +181,7 @@ export default class ChatPage extends Component<any> {
componentWillUnmount() { }
render(): ReactNode {
return (
@ -201,6 +202,14 @@ export default class ChatPage extends Component<any> {
<div className="wk-chat-content-left">
<div className="wk-chat-search">
<div className="wk-chat-title">{vm.connectTitle}</div>
<div
style={{ marginRight: '20px', alignItems: 'center', display: 'flex', cursor: 'pointer' }}
onClick={() => {
vm.showGlobalSearch = true;
}}
>
<IconSearch size="large" />
</div>
<Popover
onClickOutSide={() => {
vm.showAddPopover = false;
@ -220,6 +229,7 @@ export default class ChatPage extends Component<any> {
>
<div
className="wk-chat-search-add"
style={{ alignItems: 'center', display: 'flex' }}
onClick={() => {
vm.showAddPopover = !vm.showAddPopover;
}}
@ -253,6 +263,23 @@ export default class ChatPage extends Component<any> {
</div>
</div>
</div>
<Modal
visible={vm.showGlobalSearch}
closeOnEsc={true}
onCancel={() => {
vm.showGlobalSearch = false
}}
footer={null}
width="80%"
>
<div style={{ marginTop: '30px' }}>
<GlobalSearch onClick={(item,type:string)=>{
handleGlobalSearchClick(item,type,()=>{
vm.showGlobalSearch = false
})
}}/>
</div>
</Modal>
</div>
);
}}

View File

@ -9,6 +9,7 @@ import { ProviderListener } from "../../Service/Provider";
import { animateScroll, scroller } from 'react-scroll';
import { ProhibitwordsService } from "../../Service/ProhibitwordsService";
import { EndpointID } from "../../Service/Const";
import { ShowConversationOptions } from "../../EndpointCommon";
export class ChatVM extends ProviderListener {
conversations: ConversationWrap[] = new Array()
@ -22,6 +23,7 @@ export class ChatVM extends ProviderListener {
private channelListener!: ChannelInfoListener
private messageDeleteListener!: MessageDeleteListener
private conversationListID = "wk-conversationlist"
private _showGlobalSearch = false // 是否显示全局搜索
set showAddPopover(v: boolean) {
@ -33,6 +35,15 @@ export class ChatVM extends ProviderListener {
return this._showAddPopover
}
set showGlobalSearch(v: boolean) {
this._showGlobalSearch = v
this.notifyListener()
}
get showGlobalSearch() {
return this._showGlobalSearch
}
set selectedConversation(v: ConversationWrap | undefined) {
this._selectedConversation = v
this.notifyListener()
@ -64,7 +75,7 @@ export class ChatVM extends ProviderListener {
// 根据连接状态设置标题
this.setConnectTitleWithConnectStatus(WKSDK.shared().connectManager.status)
if(WKSDK.shared().connectManager.status == ConnectStatus.Connected){ // 如果已经连接则直接加载
if (WKSDK.shared().connectManager.status == ConnectStatus.Connected) { // 如果已经连接则直接加载
this.reloadRequestConversationList()
}
@ -87,7 +98,7 @@ export class ChatVM extends ProviderListener {
}
if (action === ConversationAction.add) {
console.log("ConversationAction-----add")
if(conversation.lastMessage?.content && conversation.lastMessage?.contentType === MessageContentType.text) {
if (conversation.lastMessage?.content && conversation.lastMessage?.contentType === MessageContentType.text) {
conversation.lastMessage.content.text = ProhibitwordsService.shared.filter(conversation.lastMessage?.content.text)
}
this.conversations = [new ConversationWrap(conversation), ...this.conversations]
@ -97,7 +108,7 @@ export class ChatVM extends ProviderListener {
const existConversation = this.findConversation(conversation.channel)
if (existConversation) {
existConversation.conversation = conversation
if(existConversation.lastMessage?.content && existConversation.lastMessage?.contentType === MessageContentType.text) {
if (existConversation.lastMessage?.content && existConversation.lastMessage?.contentType === MessageContentType.text) {
existConversation.lastMessage.content.text = ProhibitwordsService.shared.filter(existConversation.lastMessage?.content.text)
}
}
@ -183,11 +194,11 @@ export class ChatVM extends ProviderListener {
}
}
async clearMessages(channel: Channel) {
async clearMessages(channel: Channel) {
const conversationWrap = this.findConversation(channel)
if (!conversationWrap) {
return
return
}
await WKApp.conversationProvider.clearConversationMessages(conversationWrap.conversation)
conversationWrap.conversation.lastMessage = undefined
@ -195,7 +206,7 @@ export class ChatVM extends ProviderListener {
WKApp.endpointManager.invoke(EndpointID.clearChannelMessages, channel)
this.sortConversations()
this.notifyListener()
}
}
setConnectTitleWithConnectStatus(connectStatus: ConnectStatus) {
@ -255,7 +266,7 @@ export class ChatVM extends ProviderListener {
const conversations = await WKSDK.shared().conversationManager.sync({})
if (conversations && conversations.length > 0) {
for (const conversation of conversations) {
if(conversation.lastMessage?.content && conversation.lastMessage?.contentType == MessageContentType.text) {
if (conversation.lastMessage?.content && conversation.lastMessage?.contentType == MessageContentType.text) {
conversation.lastMessage.content.text = ProhibitwordsService.shared.filter(conversation.lastMessage.content.text)
}
conversationWraps.push(new ConversationWrap(conversation))
@ -268,4 +279,32 @@ export class ChatVM extends ProviderListener {
WKApp.menus.refresh()
}
}
// 处理搜索内容点击事件
export function handleGlobalSearchClick(item: any, type: string,hideModal?:()=>void) {
if (type === "contacts" || type === "group") {
if(hideModal){
hideModal()
}
WKApp.endpoints.showConversation(new Channel(item.channel_id, item.channel_type))
} else if (type === "message") {
const opts = new ShowConversationOptions()
opts.initLocateMessageSeq = item.message_seq
if(hideModal){
hideModal()
}
WKApp.endpoints.showConversation(new Channel(item.channel.channel_id, item.channel.channel_type), opts)
} else if (type === "file") {
// 下载文件
const payload = item.payload
let downloadURL = WKApp.dataSource.commonDataSource.getImageURL(payload.url || '')
if (downloadURL.indexOf("?") != -1) {
downloadURL += "&filename=" + payload.name
} else {
downloadURL += "?filename=" + payload.name
}
window.open(`${downloadURL}`, 'top');
}
}

View File

@ -80,6 +80,8 @@ import { ScreenshotCell, ScreenshotContent } from "./Messages/Screenshot";
import ImageToolbar from "./Components/ImageToolbar";
import { ProhibitwordsService } from "./Service/ProhibitwordsService";
import { SubscriberList } from "./Components/Subscribers/list";
import GlobalSearch from "./Components/GlobalSearch";
import { handleGlobalSearchClick } from "./Pages/Chat/vm";
export default class BaseModule implements IModule {
messageTone?: Howl;
@ -1211,6 +1213,40 @@ export default class BaseModule implements IModule {
1000
);
WKApp.shared.channelSettingRegister(
"channel.base.settingMessageHistory",
(context) => {
const data = context.routeData() as ChannelSettingRouteData;
const channel = data.channel
return new Section({
rows: [
new Row({
cell: ListItem,
properties: {
title: "查找聊天内容",
onClick: () => {
WKApp.shared.baseContext.showGlobalModal({
body: <GlobalSearch channel={channel} onClick={(item: any, type: string) => {
handleGlobalSearchClick(item, type,()=>{
WKApp.shared.baseContext.hideGlobalModal()
})
}} />,
width: "80%",
height: "80%",
onCancel: () => {
WKApp.shared.baseContext.hideGlobalModal()
}
})
},
},
}),
],
});
},
1100
);
WKApp.shared.channelSettingRegister(
"channel.base.setting2",
(context) => {