utils.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. import TUIChatEngine, {
  2. TUIFriendService,
  3. TUIConversationService,
  4. TUIGroupService,
  5. TUIChatService,
  6. TUITranslateService,
  7. SearchCloudMessagesParams,
  8. IGroupModel,
  9. TUIStore,
  10. StoreName,
  11. IMessageModel,
  12. } from '@tencentcloud/chat-uikit-engine';
  13. import { ISearchCloudMessageResult, IFriendType, ISearchResultListItem, IUserProfile } from '../../interface';
  14. import { searchMessageTypeList } from './search-type-list';
  15. import { Toast, TOAST_TYPE } from '../common/Toast/index';
  16. import { messageTypeAbstractMap } from './type';
  17. import { isUniFrameWork } from '../../utils/env';
  18. import { TUIGlobal } from '@tencentcloud/universal-api';
  19. /**************************************
  20. * TUISearch search logic
  21. **************************************/
  22. export const searchCloudMessages = (
  23. params: SearchCloudMessagesParams,
  24. ): Promise<{ data: ISearchCloudMessageResult }> => {
  25. return TUIChatService.searchCloudMessages(params)
  26. .then((imResponse) => {
  27. return imResponse;
  28. })
  29. .catch((error) => {
  30. Toast({
  31. message: TUITranslateService.t('TUISearch.消息云端搜索失败:') + error?.message,
  32. type: TOAST_TYPE.ERROR,
  33. duration: 3000,
  34. });
  35. return null;
  36. });
  37. };
  38. export const searchFriends = (userIDList: any[]): Promise<any[]> => {
  39. // Only show users who are already friends
  40. return TUIFriendService.getFriendProfile({ userIDList })
  41. .then((imResponse) => {
  42. return imResponse;
  43. })
  44. .catch((error) => {
  45. console.warn('search user failed:', error?.message);
  46. Toast({
  47. message: TUITranslateService.t('TUISearch.查找联系人失败:') + error?.message,
  48. type: TOAST_TYPE.ERROR,
  49. duration: 1000,
  50. });
  51. return null;
  52. });
  53. };
  54. // Search all joined group chats
  55. export const searchGroups = (groupIDList: any[]): Promise<any[]> => {
  56. const promiseList: any[] = [];
  57. groupIDList.forEach((groupID: string) => {
  58. const promise = TUIGroupService.searchGroupByID(groupID)
  59. .then((imResponse) => {
  60. // Only show joined group chats
  61. if (imResponse?.data?.group?.isJoinedGroup) {
  62. return imResponse?.data?.group;
  63. }
  64. })
  65. .catch((error) => {
  66. console.warn('search group failed:', error?.message);
  67. });
  68. promiseList.push(promise);
  69. });
  70. return Promise.all(promiseList)
  71. .then((imResponse) => {
  72. return imResponse.filter(x => x !== undefined);
  73. })
  74. .catch((error) => {
  75. Toast({
  76. message: TUITranslateService.t('TUISearch.查找群聊失败:') + error?.message,
  77. type: TOAST_TYPE.ERROR,
  78. duration: 1000,
  79. });
  80. return [];
  81. });
  82. };
  83. /**************************************
  84. * TUISearch interaction logic
  85. **************************************/
  86. // switch conversation
  87. export const enterConversation = (item: { conversationID?: string; groupID?: string; userID?: string }) => {
  88. const conversationID
  89. = item?.conversationID || (item?.groupID ? `GROUP${item?.groupID}` : `C2C${item?.userID}`);
  90. TUIConversationService.switchConversation(conversationID)
  91. .then(() => {
  92. TUIStore.update(StoreName.SEARCH, 'currentSearchingStatus', {
  93. isSearching: false,
  94. searchType: 'global',
  95. });
  96. TUIStore.update(StoreName.SEARCH, 'currentSearchInputValue', {
  97. value: '',
  98. searchType: 'global',
  99. });
  100. isUniFrameWork && TUIGlobal?.navigateTo({
  101. url: '/TUIKit/components/TUIChat/index',
  102. });
  103. })
  104. .catch((error) => {
  105. console.warn('switch conversation failed:', error?.message);
  106. Toast({
  107. message: TUITranslateService.t('TUISearch.进入会话失败'),
  108. type: TOAST_TYPE.ERROR,
  109. duration: 1000,
  110. });
  111. });
  112. };
  113. /**************************************
  114. * TUISearch UI display logic
  115. **************************************/
  116. export const generateSearchResultShowName = (result: IMessageModel | ISearchResultListItem | IGroupModel | IFriendType | IUserProfile, resultContent: Record<string, string>): string => {
  117. if (!result) {
  118. return '';
  119. }
  120. if ((result as IMessageModel).ID) {
  121. return resultContent?.showName;
  122. }
  123. if ((result as IGroupModel).groupID) {
  124. return (result as IGroupModel).name || (result as IGroupModel).groupID;
  125. }
  126. if ((result as IFriendType | IUserProfile).userID) {
  127. return (result as IFriendType).remark || (result as IUserProfile).nick || (result as IFriendType).userID || '';
  128. }
  129. if ((result as ISearchResultListItem).conversation?.conversationID) {
  130. if (typeof (result as ISearchResultListItem).conversation.getShowName === 'function') {
  131. return (result as ISearchResultListItem).conversation.getShowName();
  132. } else {
  133. return TUIStore.getConversationModel((result as ISearchResultListItem).conversation.conversationID)?.getShowName?.() || (result as ISearchResultListItem).conversation.conversationID;
  134. }
  135. }
  136. return '';
  137. };
  138. export const generateSearchResultAvatar = (result: IMessageModel | ISearchResultListItem | IGroupModel | IFriendType | IUserProfile): string => {
  139. if (!result) {
  140. return '';
  141. }
  142. if ((result as IMessageModel).ID) {
  143. return (result as IMessageModel).avatar || 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png';
  144. }
  145. if ((result as IGroupModel).groupID) {
  146. return (result as IGroupModel).avatar || `https://web.sdk.qcloud.com/im/assets/images/${(result as IGroupModel)?.type}.svg`;
  147. }
  148. if ((result as IUserProfile).userID) {
  149. return (result as IUserProfile).avatar || 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png';
  150. }
  151. if ((result as ISearchResultListItem)?.conversation?.conversationID) {
  152. if (typeof (result as ISearchResultListItem).conversation.getAvatar === 'function') {
  153. return (result as ISearchResultListItem).conversation?.getAvatar();
  154. } else {
  155. return TUIStore.getConversationModel((result as ISearchResultListItem).conversation.conversationID)?.getAvatar?.();
  156. }
  157. }
  158. return '';
  159. };
  160. // Generate the search results and display the content (including highlighting the keyword content matches)
  161. export const generateSearchResultShowContent = (
  162. result: IMessageModel | ISearchResultListItem | IGroupModel | IUserProfile,
  163. resultType: string,
  164. keywordList: any[],
  165. isTypeShow = true,
  166. ): any[] => {
  167. if ((result as IGroupModel)?.groupID) {
  168. return [
  169. { text: 'groupID: ', isHighlight: false },
  170. { text: (result as IGroupModel).groupID, isHighlight: true },
  171. ];
  172. }
  173. if ((result as IUserProfile)?.userID) {
  174. return [
  175. { text: 'userID: ', isHighlight: false },
  176. { text: (result as IUserProfile).userID, isHighlight: true },
  177. ];
  178. }
  179. if ((result as ISearchResultListItem)?.conversation || (result as IMessageModel)?.flow) {
  180. if ((result as ISearchResultListItem)?.messageCount === 1 || (result as IMessageModel)?.flow) {
  181. // Single message summary display result:
  182. // Text message, display message content + keyword highlight
  183. // File type message, display [file] file name + keyword highlight
  184. // Custom type message, display [custom message] description + keyword highlight
  185. // Other types of messages, display [message type]
  186. const message: IMessageModel = (result as IMessageModel)?.flow
  187. ? (result as IMessageModel)
  188. : (result as ISearchResultListItem)?.messageList[0];
  189. const text
  190. = message?.payload?.text || message?.payload?.fileName || message?.payload?.description;
  191. const abstract: any[] = [];
  192. if (message?.type && isTypeShow && message.type !== TUIChatEngine.TYPES.MSG_TEXT) {
  193. abstract.push({
  194. text: TUITranslateService.t(`TUISearch.${messageTypeAbstractMap[message.type]}`),
  195. isHighlight: false,
  196. });
  197. }
  198. abstract.push(...generateMessageContentHighlight(text, keywordList));
  199. return abstract;
  200. } else {
  201. return [
  202. {
  203. text: `${(result as ISearchResultListItem)?.messageCount}${TUITranslateService.t(
  204. 'TUISearch.条相关',
  205. )}${TUITranslateService.t(
  206. `TUISearch.${resultType === 'allMessage' ? '结果' : searchMessageTypeList[resultType]?.label
  207. }`,
  208. )}`,
  209. isHighlight: false,
  210. },
  211. ];
  212. }
  213. }
  214. return [];
  215. };
  216. // Parse the search message results [highlight keywords] position
  217. export const generateMessageContentHighlight = (
  218. content: string,
  219. keywordList: any[],
  220. ): any[] => {
  221. if (!content || !keywordList || !keywordList.length) {
  222. return [{ text: content || '', isHighlight: false }];
  223. }
  224. // Get the start and end positions of all key matches
  225. const matches: any[] = [];
  226. for (let i = 0; i < keywordList.length; i++) {
  227. // Special character translation
  228. const substring = keywordList[i]?.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  229. const regex = new RegExp(substring, 'gi'); // Global search and ignore case
  230. let match;
  231. while ((match = regex.exec(content)) !== null) {
  232. const start: number = match.index;
  233. const end: number = match.index + match[0].length - 1;
  234. matches.push([start, end]);
  235. }
  236. }
  237. // Merge repeated range results and arrange them in order of smallest arrival, for example, [[1,3],[2,4]] is merged into [[1,4]]
  238. const mergedRanges = [matches[0]];
  239. if (matches.length > 1) {
  240. matches.sort((a: any[], b: any[]) => a[0] - b[0]);
  241. // const mergedRanges = [matches[0]];
  242. for (let i = 1; i < matches.length; i++) {
  243. const currentRange = matches[i];
  244. const lastMergedRange = mergedRanges[mergedRanges.length - 1];
  245. // currentRange[0] - 1 is to handle the special case where [[1,2],[3,4]] can be merged into [[1,4]]
  246. if (currentRange[0] - 1 <= lastMergedRange[1]) {
  247. lastMergedRange[1] = Math.max(lastMergedRange[1], currentRange[1]);
  248. } else {
  249. mergedRanges.push(currentRange);
  250. }
  251. }
  252. }
  253. if (!mergedRanges[0]) {
  254. return [{ text: content, isHighlight: false }];
  255. }
  256. // Split the original content string according to the highlight range and add highlight related identification fields
  257. const contentArray: any[] = [];
  258. let start = 0;
  259. for (let i = 0; i < mergedRanges.length; i++) {
  260. const str1 = content.substring(start, mergedRanges[i][0]);
  261. str1 && contentArray.push({ text: str1, isHighlight: false });
  262. const str2 = content.substring(mergedRanges[i][0], mergedRanges[i][1] + 1);
  263. str2 && contentArray.push({ text: str2, isHighlight: true });
  264. start = mergedRanges[i][1] + 1;
  265. }
  266. // Add the last string
  267. const lastStr = content.substring(start);
  268. lastStr && contentArray.push({ text: lastStr, isHighlight: false });
  269. return contentArray;
  270. };
  271. // calculate timestamp
  272. export const generateSearchResultTime = (timestamp: number): string => {
  273. const todayZero = new Date().setHours(0, 0, 0, 0);
  274. const thisYear = new Date(new Date().getFullYear(), 0, 1, 0, 0, 0, 0).getTime();
  275. const target = new Date(timestamp);
  276. const oneDay = 24 * 60 * 60 * 1000;
  277. const oneWeek = 7 * oneDay;
  278. const diff = todayZero - target.getTime();
  279. function formatNum(num: number): string {
  280. return num < 10 ? '0' + num : num.toString();
  281. }
  282. if (diff <= 0) {
  283. // today, only display hour:minute
  284. return `${formatNum(target.getHours())}:${formatNum(target.getMinutes())}`;
  285. } else if (diff <= oneDay) {
  286. // yesterday, display yesterday:hour:minute
  287. return `${TUITranslateService.t('time.昨天')} ${formatNum(target.getHours())}:${formatNum(
  288. target.getMinutes(),
  289. )}`;
  290. } else if (diff <= oneWeek - oneDay) {
  291. // Within a week, display weekday hour:minute
  292. const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
  293. const weekday = weekdays[target.getDay()];
  294. return `${TUITranslateService.t('time.' + weekday)} ${formatNum(target.getHours())}:${formatNum(
  295. target.getMinutes(),
  296. )}`;
  297. } else if (target.getTime() >= thisYear) {
  298. // Over a week, within this year, display mouth/day hour:minute
  299. return `${target.getMonth() + 1}/${target.getDate()} ${formatNum(
  300. target.getHours(),
  301. )}:${formatNum(target.getMinutes())}`;
  302. } else {
  303. // Not within this year, display year/mouth/day hour:minute
  304. return `${target.getFullYear()}/${target.getMonth() + 1}/${target.getDate()} ${formatNum(
  305. target.getHours(),
  306. )}:${formatNum(target.getMinutes())}`;
  307. }
  308. };
  309. // Calculated date functions
  310. export const generateSearchResultYMD = (timestamp: number): string => {
  311. const date = new Date(timestamp * 1000); // Convert timestamp to milliseconds
  312. const year = date.getFullYear();
  313. const month = ('0' + (date.getMonth() + 1)).slice(-2);
  314. const day = ('0' + date.getDate()).slice(-2);
  315. return `${year}-${month}-${day}`; // Returns a string in year-month-day format
  316. };
  317. export const debounce = <F extends (...args: any[]) => void>(func: F, waitFor: number) => {
  318. let timeout: ReturnType<typeof setTimeout> | null = null;
  319. const debounced = (...args: Parameters<F>) => {
  320. if (timeout !== null) {
  321. clearTimeout(timeout);
  322. timeout = null;
  323. }
  324. timeout = setTimeout(() => func(...args), waitFor);
  325. };
  326. return debounced as (...args: Parameters<F>) => ReturnType<F>;
  327. };