Selaa lähdekoodia

feat: 增加腾讯的 IM 组件

陈文艺 4 kuukautta sitten
vanhempi
sitoutus
361ceadfeb
100 muutettua tiedostoa jossa 10338 lisäystä ja 5 poistoa
  1. 45 0
      Lanu.xcodeproj/project.pbxproj
  2. 12 0
      Lanu/Manager/IM/LNIMManager.swift
  3. 16 4
      Podfile
  4. 96 1
      Podfile.lock
  5. 249 0
      ThirdParty/TUIKit/TIMCommon/BaseCell/TUIMessageCell.h
  6. 721 0
      ThirdParty/TUIKit/TIMCommon/BaseCell/TUIMessageCell.m
  7. 20 0
      ThirdParty/TUIKit/TIMCommon/BaseCell/TUISecurityStrikeView.h
  8. 73 0
      ThirdParty/TUIKit/TIMCommon/BaseCell/TUISecurityStrikeView.m
  9. 30 0
      ThirdParty/TUIKit/TIMCommon/BaseCell/TUISystemMessageCell.h
  10. 107 0
      ThirdParty/TUIKit/TIMCommon/BaseCell/TUISystemMessageCell.m
  11. 90 0
      ThirdParty/TUIKit/TIMCommon/BaseCellData/NSString+TUIEmoji.h
  12. 637 0
      ThirdParty/TUIKit/TIMCommon/BaseCellData/NSString+TUIEmoji.m
  13. 30 0
      ThirdParty/TUIKit/TIMCommon/BaseCellData/TUIBubbleMessageCellData.h
  14. 15 0
      ThirdParty/TUIKit/TIMCommon/BaseCellData/TUIBubbleMessageCellData.m
  15. 283 0
      ThirdParty/TUIKit/TIMCommon/BaseCellData/TUIMessageCellData.h
  16. 207 0
      ThirdParty/TUIKit/TIMCommon/BaseCellData/TUIMessageCellData.m
  17. 110 0
      ThirdParty/TUIKit/TIMCommon/BaseCellData/TUIMessageCellLayout.h
  18. 160 0
      ThirdParty/TUIKit/TIMCommon/BaseCellData/TUIMessageCellLayout.m
  19. 24 0
      ThirdParty/TUIKit/TIMCommon/BaseCellData/TUIRelationUserModel.h
  20. 26 0
      ThirdParty/TUIKit/TIMCommon/BaseCellData/TUIRelationUserModel.m
  21. 73 0
      ThirdParty/TUIKit/TIMCommon/BaseCellData/TUISystemMessageCellData.h
  22. 97 0
      ThirdParty/TUIKit/TIMCommon/BaseCellData/TUISystemMessageCellData.m
  23. 17 0
      ThirdParty/TUIKit/TIMCommon/CommonModel/NSTimer+TUISafe.h
  24. 21 0
      ThirdParty/TUIKit/TIMCommon/CommonModel/NSTimer+TUISafe.m
  25. 27 0
      ThirdParty/TUIKit/TIMCommon/CommonModel/TIMCommonMediator.h
  26. 41 0
      ThirdParty/TUIKit/TIMCommon/CommonModel/TIMCommonMediator.m
  27. 587 0
      ThirdParty/TUIKit/TIMCommon/CommonModel/TIMCommonModel.h
  28. 2437 0
      ThirdParty/TUIKit/TIMCommon/CommonModel/TIMCommonModel.m
  29. 44 0
      ThirdParty/TUIKit/TIMCommon/CommonModel/TIMConfig.h
  30. 80 0
      ThirdParty/TUIKit/TIMCommon/CommonModel/TIMConfig.m
  31. 26 0
      ThirdParty/TUIKit/TIMCommon/CommonModel/TIMDefine.h
  32. 20 0
      ThirdParty/TUIKit/TIMCommon/CommonModel/TIMGroupInfo+TUIDataProvider.h
  33. 46 0
      ThirdParty/TUIKit/TIMCommon/CommonModel/TIMGroupInfo+TUIDataProvider.m
  34. 19 0
      ThirdParty/TUIKit/TIMCommon/CommonModel/TIMInputViewMoreActionProtocol.h
  35. 33 0
      ThirdParty/TUIKit/TIMCommon/CommonModel/TIMPopActionProtocol.h
  36. 51 0
      ThirdParty/TUIKit/TIMCommon/CommonModel/TIMRTLUtil.h
  37. 291 0
      ThirdParty/TUIKit/TIMCommon/CommonModel/TIMRTLUtil.m
  38. 661 0
      ThirdParty/TUIKit/TIMCommon/CommonModel/TUIAttributedLabel.h
  39. 1797 0
      ThirdParty/TUIKit/TIMCommon/CommonModel/TUIAttributedLabel.m
  40. 27 0
      ThirdParty/TUIKit/TIMCommon/CommonModel/TUIEmojiMeditorProtocol.h
  41. 29 0
      ThirdParty/TUIKit/TIMCommon/CommonModel/TUIFitButton.h
  42. 66 0
      ThirdParty/TUIKit/TIMCommon/CommonModel/TUIFitButton.m
  43. 54 0
      ThirdParty/TUIKit/TIMCommon/CommonModel/TUIFloatViewController.h
  44. 283 0
      ThirdParty/TUIKit/TIMCommon/CommonModel/TUIFloatViewController.m
  45. 21 0
      ThirdParty/TUIKit/TIMCommon/CommonModel/TUIGroupAvatar+Helper.h
  46. 125 0
      ThirdParty/TUIKit/TIMCommon/CommonModel/TUIGroupAvatar+Helper.m
  47. 22 0
      ThirdParty/TUIKit/TIMCommon/CommonModel/TUISecondConfirm.h
  48. 105 0
      ThirdParty/TUIKit/TIMCommon/CommonModel/TUISecondConfirm.m
  49. 25 0
      ThirdParty/TUIKit/TIMCommon/CommonModel/TUITextView.h
  50. 65 0
      ThirdParty/TUIKit/TIMCommon/CommonModel/TUITextView.m
  51. 25 0
      ThirdParty/TUIKit/TIMCommon/CommonModel/TUIUserAuthorizationCenter.h
  52. 136 0
      ThirdParty/TUIKit/TIMCommon/CommonModel/TUIUserAuthorizationCenter.m
  53. 36 0
      ThirdParty/TUIKit/TIMCommon/Resources/PrivacyInfo.xcprivacy
  54. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/chat_nav_more_menu@3x.png
  55. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/default_c2c_head@2x.png
  56. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/default_choose@3x.png
  57. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/default_fold_group@2x.png
  58. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/default_group_head@2x.png
  59. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/default_group_head_avchatroom@2x.png
  60. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/default_group_head_community@2x.png
  61. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/default_group_head_meeting@2x.png
  62. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/default_group_head_public@2x.png
  63. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/icon_avatar_selected@2x.png
  64. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/icon_cell_blue_normal@2x.png
  65. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/icon_cell_blue_normal@3x.png
  66. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/icon_flex_arrow@3x.png
  67. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/icon_offline_status@2x.png
  68. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/icon_offline_status@3x.png
  69. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/icon_online_status@2x.png
  70. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/icon_online_status@3x.png
  71. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/icon_secure_cancel_img@2x.png
  72. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/icon_secure_info_img@2x.png
  73. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/icon_security_strike@2x.png
  74. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/icon_select_normal@2x.png
  75. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/icon_select_normal@3x.png
  76. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/icon_select_pressed@2x.png
  77. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/icon_select_pressed@3x.png
  78. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/icon_select_selected@2x.png
  79. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/icon_select_selected@3x.png
  80. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/icon_select_selected_disable@2x.png
  81. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/icon_select_selected_disable@3x.png
  82. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/icon_translate@2x.png
  83. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/icon_translate@3x.png
  84. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/messageReplyIcon@2x.png
  85. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/message_translation_loading@2x.png
  86. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/message_translation_loading@3x.png
  87. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/message_translation_tips@2x.png
  88. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/message_translation_tips@3x.png
  89. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/more@2x.png
  90. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/more_customer_service_evaluation@2x.png
  91. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/more_customer_service_evaluation@3x.png
  92. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/more_group_note@2x.png
  93. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/more_group_note@3x.png
  94. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/more_poll@2x.png
  95. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/more_poll@3x.png
  96. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/more_video_call@2x.png
  97. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/more_video_call@3x.png
  98. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/more_voice_call@2x.png
  99. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/more_voice_call@3x.png
  100. BIN
      ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/nav_back@3x.png

+ 45 - 0
Lanu.xcodeproj/project.pbxproj

@@ -54,6 +54,7 @@
 				Manager/Account/LNAccountManager.swift,
 				"Manager/Account/Network/LNHttpManager+Login.swift",
 				Manager/Account/Network/LNLoginResponse.swift,
+				Manager/IM/LNIMManager.swift,
 				Manager/LNDelayTask.swift,
 				Manager/LNEventDeliver.swift,
 				Manager/Network/LNHttpManager.swift,
@@ -84,6 +85,13 @@
 			path = Lanu;
 			sourceTree = "<group>";
 		};
+		FBB67E232EC48B440070E686 /* ThirdParty */ = {
+			isa = PBXFileSystemSynchronizedRootGroup;
+			exceptions = (
+			);
+			path = ThirdParty;
+			sourceTree = "<group>";
+		};
 /* End PBXFileSystemSynchronizedRootGroup section */
 
 /* Begin PBXFrameworksBuildPhase section */
@@ -126,6 +134,7 @@
 		FBFE13B72EBC39B000DCE6E9 = {
 			isa = PBXGroup;
 			children = (
+				FBB67E232EC48B440070E686 /* ThirdParty */,
 				FB1A37952EBE04E40063ED8C /* Lanu */,
 				FBFE13C12EBC39B000DCE6E9 /* Products */,
 				3BCF7245871F5939F104E2E4 /* Pods */,
@@ -152,6 +161,8 @@
 				FBFE13BC2EBC39B000DCE6E9 /* Sources */,
 				FBFE13BD2EBC39B000DCE6E9 /* Frameworks */,
 				FBFE13BE2EBC39B000DCE6E9 /* Resources */,
+				E2E510A5ADE6EF4B1CF63BE0 /* [CP] Embed Pods Frameworks */,
+				1DE0C24AA7AC8D1572EDA949 /* [CP] Copy Pods Resources */,
 			);
 			buildRules = (
 			);
@@ -217,6 +228,23 @@
 /* End PBXResourcesBuildPhase section */
 
 /* Begin PBXShellScriptBuildPhase section */
+		1DE0C24AA7AC8D1572EDA949 /* [CP] Copy Pods Resources */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputFileListPaths = (
+				"${PODS_ROOT}/Target Support Files/Pods-Lanu/Pods-Lanu-resources-${CONFIGURATION}-input-files.xcfilelist",
+			);
+			name = "[CP] Copy Pods Resources";
+			outputFileListPaths = (
+				"${PODS_ROOT}/Target Support Files/Pods-Lanu/Pods-Lanu-resources-${CONFIGURATION}-output-files.xcfilelist",
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Lanu/Pods-Lanu-resources.sh\"\n";
+			showEnvVarsInLog = 0;
+		};
 		AC205607300F610A882B2531 /* [CP] Check Pods Manifest.lock */ = {
 			isa = PBXShellScriptBuildPhase;
 			buildActionMask = 2147483647;
@@ -239,6 +267,23 @@
 			shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n    # print error to STDERR\n    echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n    exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
 			showEnvVarsInLog = 0;
 		};
+		E2E510A5ADE6EF4B1CF63BE0 /* [CP] Embed Pods Frameworks */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputFileListPaths = (
+				"${PODS_ROOT}/Target Support Files/Pods-Lanu/Pods-Lanu-frameworks-${CONFIGURATION}-input-files.xcfilelist",
+			);
+			name = "[CP] Embed Pods Frameworks";
+			outputFileListPaths = (
+				"${PODS_ROOT}/Target Support Files/Pods-Lanu/Pods-Lanu-frameworks-${CONFIGURATION}-output-files.xcfilelist",
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Lanu/Pods-Lanu-frameworks.sh\"\n";
+			showEnvVarsInLog = 0;
+		};
 /* End PBXShellScriptBuildPhase section */
 
 /* Begin PBXSourcesBuildPhase section */

+ 12 - 0
Lanu/Manager/IM/LNIMManager.swift

@@ -0,0 +1,12 @@
+//
+//  LNIMManager.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/11/12.
+//
+
+import Foundation
+
+class LNIMManager {
+    static let appId = "20030346"
+}

+ 16 - 4
Podfile

@@ -8,9 +8,21 @@ target 'Lanu' do
   use_frameworks!
 
   # # 腾讯IM
-  # pod 'TUIChat', :path => "./TUIKit/TUIChat"
-  # pod 'TUIConversation', :path => "./TUIKit/TUIConversation"
-  # pod 'TIMCommon', :path => "./TUIKit/TIMCommon"
-  # pod 'TUICore', :path => "./TUIKit/TUICore"
+  pod 'TUIChat', :path => "./ThirdParty/TUIKit/TUIChat"
+  pod 'TUIConversation', :path => "./ThirdParty/TUIKit/TUIConversation"
+  pod 'TIMCommon', :path => "./ThirdParty/TUIKit/TIMCommon"
+  pod 'TUICore', :path => "./ThirdParty/TUIKit/TUICore"
+  
+  pod 'TIMPush'
+  pod 'TXIMSDK_Plus_iOS_XCFramework'
 
 end
+
+post_install do |installer|
+  installer.pods_project.targets.each do |target|
+    target.build_configurations.each do |config|
+      # 强制所有库使用 iOS 15 作为最低版本
+      config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0'
+    end
+  end
+end

+ 96 - 1
Podfile.lock

@@ -1,3 +1,98 @@
-PODFILE CHECKSUM: bbbad0836b8cd86f2e2056c8cb52391f831c7775
+PODS:
+  - Masonry (1.1.0)
+  - Protobuf (3.29.5)
+  - ReactiveObjC (3.1.1)
+  - SDWebImage (5.21.3):
+    - SDWebImage/Core (= 5.21.3)
+  - SDWebImage/Core (5.21.3)
+  - SSZipArchive (2.4.3)
+  - SVGAPlayer-iOS (1.1.13):
+    - SVGAPlayer-iOS/Core (= 1.1.13)
+    - SVGAPlayer-iOS/ProtoFiles (= 1.1.13)
+  - SVGAPlayer-iOS/Core (1.1.13):
+    - SSZipArchive (>= 1.8.1)
+    - SVGAPlayer-iOS/ProtoFiles
+  - SVGAPlayer-iOS/ProtoFiles (1.1.13):
+    - Protobuf (~> 3.4)
+  - TIMCommon (1.0.0):
+    - Masonry
+    - ReactiveObjC
+    - SDWebImage
+    - SVGAPlayer-iOS
+    - TUICore
+  - TIMPush (8.7.7201):
+    - TXIMSDK_Plus_iOS_XCFramework (>= 8.7.7201)
+  - TUIChat (1.0.0):
+    - ReactiveObjC
+    - SDWebImage
+    - SVGAPlayer-iOS
+    - TIMCommon
+    - TUICore
+  - TUIConversation (1.0.0):
+    - ReactiveObjC
+    - SVGAPlayer-iOS
+    - TIMCommon
+    - TUICore
+    - YYKit
+  - TUICore (1.0.0):
+    - SDWebImage
+    - TUICore/ImSDK_Plus (= 1.0.0)
+  - TUICore/Base (1.0.0):
+    - SDWebImage
+  - TUICore/ImSDK_Plus (1.0.0):
+    - SDWebImage
+    - TUICore/Base
+    - TXIMSDK_Plus_iOS_XCFramework
+  - TXIMSDK_Plus_iOS_XCFramework (8.7.7201)
+  - YYKit (1.0.9):
+    - YYKit/no-arc (= 1.0.9)
+  - YYKit/no-arc (1.0.9)
+
+DEPENDENCIES:
+  - TIMCommon (from `./ThirdParty/TUIKit/TIMCommon`)
+  - TIMPush
+  - TUIChat (from `./ThirdParty/TUIKit/TUIChat`)
+  - TUIConversation (from `./ThirdParty/TUIKit/TUIConversation`)
+  - TUICore (from `./ThirdParty/TUIKit/TUICore`)
+  - TXIMSDK_Plus_iOS_XCFramework
+
+SPEC REPOS:
+  https://mirrors.tuna.tsinghua.edu.cn/git/CocoaPods/Specs.git:
+    - Masonry
+    - Protobuf
+    - ReactiveObjC
+    - SDWebImage
+    - SSZipArchive
+    - SVGAPlayer-iOS
+    - TIMPush
+    - TXIMSDK_Plus_iOS_XCFramework
+    - YYKit
+
+EXTERNAL SOURCES:
+  TIMCommon:
+    :path: "./ThirdParty/TUIKit/TIMCommon"
+  TUIChat:
+    :path: "./ThirdParty/TUIKit/TUIChat"
+  TUIConversation:
+    :path: "./ThirdParty/TUIKit/TUIConversation"
+  TUICore:
+    :path: "./ThirdParty/TUIKit/TUICore"
+
+SPEC CHECKSUMS:
+  Masonry: 678fab65091a9290e40e2832a55e7ab731aad201
+  Protobuf: 164aea2ae380c3951abdc3e195220c01d17400e0
+  ReactiveObjC: 011caa393aa0383245f2dcf9bf02e86b80b36040
+  SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a
+  SSZipArchive: fe6a26b2a54d5a0890f2567b5cc6de5caa600aef
+  SVGAPlayer-iOS: cc24eb156f93f793b9e72e7b8de617f22f1864d0
+  TIMCommon: 39343d50032dd1e022aef14faafdd59041cd4cfa
+  TIMPush: 4f4fa655697c4106309054d0b50a485e642b4f80
+  TUIChat: 47d612d109c68854b427dcfe92e109171559ae6a
+  TUIConversation: ea832cc28987495091203ca892fe046414b29e5f
+  TUICore: c1e480d7644ad9efb0db8c353e26ff39d7e4933b
+  TXIMSDK_Plus_iOS_XCFramework: 3b435eae84c639f35ae8dc9c8b92c399a8b0a67f
+  YYKit: 7cda43304a8dc3696c449041e2cb3107b4e236e7
+
+PODFILE CHECKSUM: 79149af608319cad7ddddf1975edb1df49217cf3
 
 COCOAPODS: 1.16.2

+ 249 - 0
ThirdParty/TUIKit/TIMCommon/BaseCell/TUIMessageCell.h

@@ -0,0 +1,249 @@
+
+//  Created by Tencent on 2023/06/09.
+//  Copyright © 2023 Tencent. All rights reserved.
+/**
+ *
+ *
+ *  This document declares the modules and components used to implement the message unit.
+ *  The message unit (TUIMessageCell) is a general term for the bubble message/picture message/emoticon message/video message displayed in the chat view.
+ *  The above messages are implemented by inheriting from this class or a subclass of this class. If you want to customize the message, you also need to
+ * implement it by inheriting from this class or a subclass of this class. The XXXX message unit (TUIXXXXMessageCell) is mainly responsible for displaying on
+ * the page and responding to user interaction events. For data processing and acquisition in the message unit, please refer to
+ * TUIChat\CellData\TUIXXXXMessageCellData.h according to the specific message unit
+ *
+ *  The interaction callbacks provided by the TUIMessageCellDelegate protocol include: long press, resend, click on the message, click on the avatar, etc.
+ *  The TUIMessageCell class stores message-related information, such as the sender's avatar, sender's nickname, and message content (supports various formats
+ * such as text, pictures, and videos). At the same time, TUIMessageeCell, as a parent class, provides basic properties and behavior templates for subclass
+ * messages.
+ */
+
+#import <UIKit/UIKit.h>
+#import "TUIFitButton.h"
+#import "TUIMessageCellData.h"
+#import "TUISecurityStrikeView.h"
+
+
+@class TUIMessageCell;
+
+
+@protocol TUIMessageCellProtocol <NSObject>
+
+@required
++ (CGFloat)getHeight:(TUIMessageCellData *)data withWidth:(CGFloat)width;
++ (CGFloat)getEstimatedHeight:(TUIMessageCellData *)data;
++ (CGSize)getContentSize:(TUIMessageCellData *)data;
+
+@end
+
+
+/////////////////////////////////////////////////////////////////////////////////
+//
+//                              TUIMessageCellDelegate
+//
+/////////////////////////////////////////////////////////////////////////////////
+
+@protocol TUIMessageCellDelegate <NSObject>
+
+/**
+ *  Callback for long press message
+ *  You can use this callback to implement secondary operations such as delete and recall (when the sender of the message long-presses his own message) on top
+ * of the long-pressed message.
+ */
+- (void)onLongPressMessage:(TUIMessageCell *)cell;
+
+/**
+ *  Callback for clicking retryView
+ *  You can use this callback to implement: resend the message.
+ */
+- (void)onRetryMessage:(TUIMessageCell *)cell;
+
+/**
+ *  Callback for clicking message cell
+ *  Usually:
+ *  - Clicking on the sound message means playing voice
+ *  - Clicking on the file message means opening the file
+ *  - Clicking on the picture message means showing the larger image
+ *  - Clicking on the video message means playing the video.
+ *  Usually, it only provides a reference for the function implementation, and you can implement the delegate function according to your needs.
+ */
+- (void)onSelectMessage:(TUIMessageCell *)cell;
+
+/**
+ *  Callback for clicking avatar view of the messageCell
+ *  You can use this callback to implement: in response to the user's click, jump to the detailed information interface of the corresponding user.
+ */
+- (void)onSelectMessageAvatar:(TUIMessageCell *)cell;
+
+/**
+ *  Callback for long pressing avatar view of messageCell
+ */
+- (void)onLongSelectMessageAvatar:(TUIMessageCell *)cell;
+
+/**
+ *  Callback for clicking read receipt label
+ */
+- (void)onSelectReadReceipt:(TUIMessageCellData *)cell;
+
+/**
+ * Clicking the x-person reply button to jump to the multi-person reply details page
+ */
+- (void)onJumpToRepliesDetailPage:(TUIMessageCellData *)data;
+
+
+- (void)onJumpToMessageInfoPage:(TUIMessageCellData *)data selectCell:(TUIMessageCell *)cell;
+
+@end
+
+/////////////////////////////////////////////////////////////////////////////////
+//
+//                              TUIMessageCell
+//
+/////////////////////////////////////////////////////////////////////////////////
+
+@interface TUIMessageCell : TUICommonTableViewCell <TUIMessageCellProtocol>
+
+/**
+ * Icon that identifies the message selected
+ * In the multi-selection scenario, it is used to identify whether the message is selected
+ */
+@property(nonatomic, strong) UIImageView *selectedIcon;
+
+/**
+ * Message selection view
+ * When multiple selection is activated, the view will be overlaid on this cell, and clicking on the view will trigger the check/uncheck of the message
+ */
+@property(nonatomic, strong) UIButton *selectedView;
+
+/**
+ *  
+ *  The icon view of displays user's avatar
+ */
+@property(nonatomic, strong) UIImageView *avatarView;
+
+/**
+ *  
+ *  The label of displays user's displayname
+ */
+@property(nonatomic, strong) UILabel *nameLabel;
+
+/**
+ *  Container view
+ *  It wraps various views of MesageCell as the "bottom" of MessageCell, which is convenient for view management and layout.
+ */
+@property(nonatomic, strong) UIView *container;
+
+/**
+ *  Activity indicator
+ *  A circling icon is provided while the message is being sent to indicate that the message is being sent.
+ */
+@property(nonatomic, strong) UIActivityIndicatorView *indicator;
+
+/**
+ *  Retry view, displayed after sending failed, click on this view to trigger onRetryMessage: callback.
+ */
+@property(nonatomic, strong) UIImageView *retryView;
+
+
+/**
+ *  security Strike View
+ */
+@property (nonatomic, strong) TUISecurityStrikeView * securityStrikeView;
+
+/**
+ *  Message reply details button
+ */
+@property(nonatomic, strong) TUIFitButton *messageModifyRepliesButton;
+
+/**
+ * The message data class which stores the information required in the messageCell, including sender ID, sender avatar, message sending status, message bubble
+ * icon, etc. For details of messageData, please refer to: TUIChat\Cell\CellData\TUIMessageCellData.h
+ */
+@property(readonly) TUIMessageCellData *messageData;
+
+/**
+ *  A control that identifies whether a message has been read
+ */
+@property(nonatomic, strong) UILabel *readReceiptLabel;
+
+/**
+ * The message time label control, which is not displayed by default, is located at the far right of the message cell
+ * In the message forwarding scenario, open the forwarded message list, and the time of the current message will be displayed on the far right of the message.
+ */
+@property(nonatomic, strong) UILabel *timeLabel;
+
+/**
+ * Whether to disable the default selection behavior encapsulated in TUIKit, such as group live broadcast by default to create live room and other behaviors,
+ * default: NO
+ */
+@property(nonatomic, assign) BOOL disableDefaultSelectAction;
+
+@property(nonatomic, weak) id<TUIMessageCellDelegate> delegate;
+
+/**
+ * 
+ * Whether the highlight flashing animation is in progress
+ */
+@property(nonatomic, assign) BOOL highlightAnimating;
+
+- (void)fillWithData:(TUICommonCellData *)data;
+
+/**
+ * Set the highlighting effect after matching the keyword, mainly used for jumping after message search, subclass rewriting
+ * The base class provides the default highlighting effect, and the subclass can implement it freely
+ *
+ * @param keyword  Highlight keywords
+ */
+- (void)highlightWhenMatchKeyword:(NSString *)keyword;
+
+/**
+ * Returns the view for highlighting
+ */
+- (UIView *)highlightAnimateView;
+
+/**
+ * Update the content of the read label
+ */
+- (void)updateReadLabelText;
+
+/// Preset bottom container in cell, which can be added custom view/viewControllers.
+@property(nonatomic, strong) UIView *bottomContainer;
+
+/// When bottom container is layout ready, notify it to add custom extensions.
+- (void)notifyBottomContainerReadyOfData:(TUIMessageCellData *)cellData;
+
+/// Callback of SelectCell
+@property(nonatomic, copy) TUIValueCallbck pluginMsgSelectCallback;
+
+@end
+
+
+@interface TUIMessageCell (TUILayoutConfiguration)
+
+/**
+ *  The color of the label that displays the recipient's nickname
+ *  Used when the nickname needs to be displayed and the message direction is MsgDirectionIncoming
+ */
+@property(nonatomic, class) UIColor *incommingNameColor;
+
+/**
+ *
+ *  The font of the label that displays the recipient's nickname
+ *  Used when the nickname needs to be displayed and the message direction is MsgDirectionIncoming
+ *
+ */
+@property(nonatomic, class) UIFont *incommingNameFont;
+
+/**
+ *  The color of the label showing the sender's nickname
+ *  Used when the nickname needs to be displayed and the message direction is MsgDirectionOutgoing.
+ */
+@property(nonatomic, class) UIColor *outgoingNameColor;
+
+/**
+ *
+ *  The font of the label that displays the sender's nickname
+ *  Used when the nickname needs to be displayed and the message direction is MsgDirectionOutgoing.
+ */
+@property(nonatomic, class) UIFont *outgoingNameFont;
+
+@end

+ 721 - 0
ThirdParty/TUIKit/TIMCommon/BaseCell/TUIMessageCell.m

@@ -0,0 +1,721 @@
+//
+//  TUIMessageCell.m
+//  UIKit
+//
+//  Created by kennethmiao on 2018/9/17.
+//  Copyright © 2018 Tencent. All rights reserved.
+//
+
+#import "TUIMessageCell.h"
+#import <TIMCommon/TIMDefine.h>
+#import <TUICore/TUIThemeManager.h>
+#import <TUICore/TUITool.h>
+#import "NSString+TUIEmoji.h"
+#import "TUISystemMessageCellData.h"
+#import <TUICore/TUICore.h>
+#import <TIMCommon/MOHeadNormalView.h>
+
+@interface TUIMessageCell () <CAAnimationDelegate>
+@property(nonatomic, strong) TUIMessageCellData *messageData;
+
+@property (nonatomic, strong) MOHeadNormalView *headBgView;//头像框
+
+@end
+
+@implementation TUIMessageCell
+
+#pragma mark - Life cycle
+- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
+    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
+    if (self) {
+        [self setupSubViews];
+        [self setupRAC];
+    }
+    return self;
+}
+
+- (void)setupSubViews {
+    // head
+    _avatarView = [[UIImageView alloc] init];
+    _avatarView.contentMode = UIViewContentModeScaleAspectFit;
+    [self.contentView addSubview:_avatarView];
+    UITapGestureRecognizer *tap1 = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onSelectMessageAvatar:)];
+    [_avatarView addGestureRecognizer:tap1];
+    UILongPressGestureRecognizer *tap2 = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(onLongSelectMessageAvatar:)];
+    [_avatarView addGestureRecognizer:tap2];
+    [_avatarView setUserInteractionEnabled:YES];
+    
+    [self.contentView addSubview:self.headBgView];
+
+    // nameLabel
+    _nameLabel = [[UILabel alloc] init];
+    _nameLabel.font = [self fontWithSize:13];
+    _nameLabel.textColor = [UIColor d_systemGrayColor];
+    [self.contentView addSubview:_nameLabel];
+
+    // container
+    _container = [[UIView alloc] init];
+    _container.backgroundColor = [UIColor clearColor];
+    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onSelectMessage:)];
+    tap.cancelsTouchesInView = NO;
+    [_container addGestureRecognizer:tap];
+    UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(onLongPress:)];
+    [_container addGestureRecognizer:longPress];
+    [self.contentView addSubview:_container];
+
+    // indicator
+    _indicator = [[UIActivityIndicatorView alloc] init];
+    _indicator.activityIndicatorViewStyle = UIActivityIndicatorViewStyleGray;
+    [_indicator sizeToFit];
+    [self.contentView addSubview:_indicator];
+
+    // error
+    _retryView = [[UIImageView alloc] init];
+    _retryView.userInteractionEnabled = YES;
+    UITapGestureRecognizer *resendTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onRetryMessage:)];
+    [_retryView addGestureRecognizer:resendTap];
+    [self.contentView addSubview:_retryView];
+
+    // messageModifyRepliesLabel
+    _messageModifyRepliesButton = [[TUIFitButton alloc] initWithFrame:CGRectMake(0, 0, 12, 12)];
+    _messageModifyRepliesButton.imageSize = CGSizeMake(12, 12);
+    [_messageModifyRepliesButton addTarget:self action:@selector(onJumpToRepliesDetailPage:) forControlEvents:UIControlEventTouchUpInside];
+    [_messageModifyRepliesButton.titleLabel setFont:[self fontWithSize:12]];
+    [_messageModifyRepliesButton setTitleColor:TIMCommonDynamicColor(@"chat_message_read_name_date_text_color", @"#999999") forState:UIControlStateNormal];
+    [_messageModifyRepliesButton setImage:TIMCommonBundleThemeImage(@"chat_messageReplyIcon_img", @"messageReplyIcon") forState:UIControlStateNormal];
+    [self.contentView addSubview:_messageModifyRepliesButton];
+
+    _readReceiptLabel = [[UILabel alloc] init];
+    _readReceiptLabel.hidden = YES;
+    _readReceiptLabel.font = [self fontWithSize:12];
+    _readReceiptLabel.textColor = TIMCommonDynamicColor(@"chat_message_read_status_text_gray_color", @"#BBBBBB");
+    _readReceiptLabel.lineBreakMode = NSLineBreakByCharWrapping;
+    UITapGestureRecognizer *showReadReceiptTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onSelectReadReceipt:)];
+    [_readReceiptLabel addGestureRecognizer:showReadReceiptTap];
+    _readReceiptLabel.userInteractionEnabled = YES;
+    [self.contentView addSubview:_readReceiptLabel];
+
+    // selectedIcon
+    _selectedIcon = [[UIImageView alloc] init];
+    [self.contentView addSubview:_selectedIcon];
+
+    // selectedView
+    _selectedView = [UIButton buttonWithType:UIButtonTypeCustom];
+    _selectedView.backgroundColor = [UIColor clearColor];
+    [_selectedView addTarget:self action:@selector(onSelectMessage:) forControlEvents:UIControlEventTouchUpInside];
+    [self.contentView addSubview:_selectedView];
+
+    // timeLabel
+    _timeLabel = [[UILabel alloc] init];
+    _timeLabel.textColor = [UIColor darkGrayColor];
+    _timeLabel.font = [self fontWithSize:11.0];
+    [self.contentView addSubview:_timeLabel];
+
+    self.selectionStyle = UITableViewCellSelectionStyleNone;
+    self.backgroundColor = UIColor.clearColor;
+    self.contentView.backgroundColor = UIColor.clearColor;
+    
+    [self makeConstraints];
+}
+
+- (void)makeConstraints {
+    
+    [self.headBgView mas_makeConstraints:^(MASConstraintMaker *make) {
+        make.centerX.equalTo(self.avatarView);
+        make.centerY.equalTo(self.avatarView);
+        make.width.height.equalTo(@40.0);
+    }];
+    self.headBgView.headImgWidth = 40.0;
+    
+    [self.nameLabel mas_makeConstraints:^(MASConstraintMaker *make) {
+        make.leading.mas_equalTo(_container.mas_leading).mas_offset(7);
+        make.top.mas_equalTo(self.avatarView.mas_top);
+        make.width.mas_equalTo(1);
+        make.height.mas_equalTo(20);
+    }];
+    
+    [self.selectedIcon mas_makeConstraints:^(MASConstraintMaker *make) {
+        make.leading.mas_equalTo(self.contentView.mas_leading).mas_offset(3);
+        make.top.mas_equalTo(self.avatarView.mas_centerY).mas_offset(-10);
+        make.width.mas_equalTo(20);
+        make.height.mas_equalTo(20);
+    }];
+
+    [self.timeLabel mas_makeConstraints:^(MASConstraintMaker *make) {
+        make.trailing.mas_equalTo(self.contentView.mas_trailing).mas_offset(-10);
+        make.top.mas_equalTo(self.avatarView);
+        make.width.mas_greaterThanOrEqualTo(10);
+        make.height.mas_equalTo(10);
+    }];
+    
+    [self.selectedView mas_makeConstraints:^(MASConstraintMaker *make) {
+        make.edges.mas_equalTo(self.contentView);
+    }];
+
+}
+
++ (BOOL)requiresConstraintBasedLayout {
+    return YES;
+}
+
+// this is Apple's recommended place for adding/updating constraints
+- (void)updateConstraints {
+    TUIMessageCellLayout *cellLayout = self.messageData.cellLayout;
+    BOOL isInComing = (self.messageData.direction == MsgDirectionIncoming);
+
+    [self.nameLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
+        if (isInComing) {
+            make.leading.mas_equalTo(_container.mas_leading).mas_offset(7);
+            make.trailing.mas_equalTo(self.contentView).mas_offset(-7);
+        } else {
+            make.leading.mas_equalTo(self.contentView).mas_offset(7);
+            make.trailing.mas_equalTo(self.container.mas_trailing);
+        }
+        if (self.messageData.showName) {
+            make.width.mas_greaterThanOrEqualTo(20);
+            make.height.mas_greaterThanOrEqualTo(20);
+        } else {
+            make.height.mas_equalTo(0);
+        }
+        make.top.mas_equalTo(self.avatarView.mas_top);
+    }];
+
+    [self.selectedIcon mas_updateConstraints:^(MASConstraintMaker *make) {
+        if (self.messageData.showCheckBox) {
+            make.width.mas_equalTo(20);
+            make.height.mas_equalTo(20);
+        } else {
+            make.size.mas_equalTo(CGSizeZero);
+        }
+    }];
+
+    [self.timeLabel sizeToFit];
+    [self.timeLabel mas_updateConstraints:^(MASConstraintMaker *make) {
+        if (self.messageData.showMessageTime) {
+            make.width.mas_equalTo(self.timeLabel.frame.size.width);
+            make.height.mas_equalTo(self.timeLabel.frame.size.height);
+        } else {
+            make.width.mas_equalTo(0);
+            make.height.mas_equalTo(0);
+        }
+    }];
+
+    CGSize csize = [self.class getContentSize:self.messageData];
+    CGFloat contentWidth = csize.width;
+    CGFloat contentHeight = csize.height;
+
+    if (!CGSizeEqualToSize(self.messageData.messageContainerAppendSize, CGSizeZero)) {
+        /**
+         * Taking the maximum width between the "emoji reply message" and the text content
+         */
+        contentWidth = MAX(self.messageData.messageContainerAppendSize.width, csize.width);
+        /**
+         * Limit the maximum width to Screen_Width *0.25 * 3
+         */
+        contentWidth = MIN(contentWidth, Screen_Width * 0.25 * 3);
+        contentHeight = csize.height + self.messageData.messageContainerAppendSize.height;
+    }
+    if (self.messageData.direction == MsgDirectionIncoming) {
+        self.avatarView.hidden = !self.messageData.showAvatar;
+        [self.avatarView mas_remakeConstraints:^(MASConstraintMaker *make) {
+          if (self.messageData.showCheckBox) {
+              make.leading.mas_equalTo(self.selectedIcon.mas_trailing).mas_offset(cellLayout.avatarInsets.left);
+          } else {
+              make.leading.mas_equalTo(self.contentView.mas_leading).mas_offset(cellLayout.avatarInsets.left);
+          }
+          make.top.mas_equalTo(cellLayout.avatarInsets.top);
+          make.size.mas_equalTo(cellLayout.avatarSize);
+        }];
+
+        [self.container mas_remakeConstraints:^(MASConstraintMaker *make) {
+          make.leading.mas_equalTo(self.avatarView.mas_trailing).mas_offset(cellLayout.messageInsets.left);
+          make.top.mas_equalTo(self.nameLabel.mas_bottom).mas_offset(cellLayout.messageInsets.top);
+          make.width.mas_equalTo(contentWidth);
+          make.height.mas_equalTo(contentHeight);
+        }];
+
+        CGRect indicatorFrame = self.indicator.frame;
+        [self.indicator mas_remakeConstraints:^(MASConstraintMaker *make) {
+          make.leading.mas_equalTo(self.container.mas_trailing).mas_offset(8);
+          make.centerY.mas_equalTo(self.container.mas_centerY);
+          make.size.mas_equalTo(indicatorFrame.size);
+        }];
+        self.retryView.frame = self.indicator.frame;
+        self.readReceiptLabel.hidden = YES;
+    } else {
+        if (!self.messageData.showAvatar) {
+            cellLayout.avatarSize = CGSizeZero;
+        } 
+        [self.avatarView mas_remakeConstraints:^(MASConstraintMaker *make) {
+          make.trailing.mas_equalTo(self.contentView.mas_trailing).mas_offset(-cellLayout.avatarInsets.right);
+          make.top.mas_equalTo(cellLayout.avatarInsets.top);
+          make.size.mas_equalTo(cellLayout.avatarSize);
+        }];
+        [self.container mas_remakeConstraints:^(MASConstraintMaker *make) {
+          make.trailing.mas_equalTo(self.avatarView.mas_leading).mas_offset(-cellLayout.messageInsets.right);
+          make.top.mas_equalTo(self.nameLabel.mas_bottom).mas_offset(cellLayout.messageInsets.top);
+          make.width.mas_equalTo(contentWidth);
+          make.height.mas_equalTo(contentHeight);
+        }];
+
+        CGRect indicatorFrame = self.indicator.frame;
+        [self.indicator mas_remakeConstraints:^(MASConstraintMaker *make) {
+          make.trailing.mas_equalTo(self.container.mas_leading).mas_offset(-8);
+          make.centerY.mas_equalTo(self.container.mas_centerY);
+          make.size.mas_equalTo(indicatorFrame.size);
+        }];
+
+        self.retryView.frame = self.indicator.frame;
+
+        [self.readReceiptLabel sizeToFit];
+        [self.readReceiptLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
+          make.bottom.mas_equalTo(self.container.mas_bottom);
+          make.trailing.mas_equalTo(self.container.mas_leading).mas_offset(-8);
+          make.size.mas_equalTo(self.readReceiptLabel.frame.size);
+        }];
+    }
+    
+    [self.headBgView mas_remakeConstraints:^(MASConstraintMaker *make) {
+        make.centerX.equalTo(self.avatarView);
+        make.centerY.equalTo(self.avatarView);
+        make.width.height.equalTo(@40.0);
+    }];
+    self.headBgView.headImgWidth = 40.0;
+
+    if (!self.messageModifyRepliesButton.isHidden) {
+        self.messageModifyRepliesButton.mm_sizeToFit();
+        CGFloat repliesBtnTextWidth = self.messageModifyRepliesButton.frame.size.width;
+        [self.messageModifyRepliesButton mas_remakeConstraints:^(MASConstraintMaker *make) {
+          if (isInComing) {
+              make.leading.mas_equalTo(self.container.mas_leading);
+          } else {
+              make.trailing.mas_equalTo(self.container.mas_trailing);
+          }
+          make.top.mas_equalTo(self.container.mas_bottom);
+          make.size.mas_equalTo(CGSizeMake(repliesBtnTextWidth + 10, 30));
+        }];
+    }
+
+
+    // according to apple super should be called at end of method
+    [super updateConstraints];
+}
+
+- (void)layoutSubviews {
+    [super layoutSubviews];
+}
+
+- (void)setupRAC {
+    @weakify(self);
+    [RACObserve(self, readReceiptLabel.text) subscribeNext:^(id _Nullable x) {
+      @strongify(self);
+      if ([self shouldHighlightReadReceiptLabel]) {
+          self.readReceiptLabel.textColor = TIMCommonDynamicColor(@"chat_message_read_status_text_color", @"#147AFF");
+      } else {
+          self.readReceiptLabel.textColor = TIMCommonDynamicColor(@"chat_message_read_status_text_gray_color", @"#BBBBBB");
+      }
+    }];
+}
+
+- (void)prepareForReuse {
+    [super prepareForReuse];
+    /**
+     * In the future, any UI problems caused by reuse can be solved by coding here.
+     */
+
+    /**
+     * Once the message is reused, it means that a new message is about to appear, and the label content is changed to empty string.
+     */
+    _readReceiptLabel.text = @"";
+    _readReceiptLabel.hidden = YES;
+}
+
+#pragma mark - Public
+- (void)fillWithData:(TUIMessageCellData *)data {
+    [super fillWithData:data];
+    self.messageData = data;
+
+    [self loadAvatar:data];
+
+    if (self.messageData.showName) {
+        _nameLabel.hidden = NO;
+    } else {
+        _nameLabel.hidden = YES;
+    }
+
+    if (self.messageData.showCheckBox) {
+        _selectedIcon.hidden = NO;
+        _selectedView.hidden = NO;
+    } else {
+        _selectedIcon.hidden = YES;
+        _selectedView.hidden = YES;
+    }
+
+    if ([TUIConfig defaultConfig].avatarType == TAvatarTypeRounded) {
+        self.avatarView.layer.masksToBounds = YES;
+        self.avatarView.layer.cornerRadius = data.cellLayout.avatarSize.height / 2;
+    } else if ([TUIConfig defaultConfig].avatarType == TAvatarTypeRadiusCorner) {
+        self.avatarView.layer.masksToBounds = YES;
+        self.avatarView.layer.cornerRadius = [TUIConfig defaultConfig].avatarCornerRadius;
+    }
+
+    self.nameLabel.text = data.senderName;
+    
+    if (data.direction == MsgDirectionIncoming) {
+        self.nameLabel.textColor = self.class.incommingNameColor;
+        self.nameLabel.font = self.class.incommingNameFont;
+    } else {
+        self.nameLabel.textColor = self.class.outgoingNameColor;
+        self.nameLabel.font = self.class.outgoingNameFont;
+    }
+
+
+    self.retryView.image = [UIImage imageNamed:TUIChatImagePath(@"msg_error")];
+
+    if (data.status == Msg_Status_Fail) {
+        [_indicator stopAnimating];
+        _readReceiptLabel.hidden = YES;
+        self.retryView.hidden = NO;
+    } else {
+        if (data.status == Msg_Status_Sending_2) {
+            [_indicator startAnimating];
+            _readReceiptLabel.hidden = YES;
+        } else if (data.status == Msg_Status_Succ) {
+            [_indicator stopAnimating];
+            /**
+             * The message is sent successfully, indicating that the indicator and error are no longer displayed on the label, and the read receipt label can be
+             * displayed.
+             */
+            if (self.messageData.showReadReceipt && self.messageData.direction == MsgDirectionOutgoing && self.messageData.innerMessage.needReadReceipt &&
+                (self.messageData.innerMessage.userID || self.messageData.innerMessage.groupID) &&
+                ![self.messageData isKindOfClass:TUISystemMessageCellData.class]) {
+                [self updateReadLabelText];
+                _readReceiptLabel.hidden = NO;
+            }
+        } else if (data.status == Msg_Status_Sending) {
+            [_indicator startAnimating];
+            _readReceiptLabel.hidden = YES;
+        }
+        self.retryView.hidden = YES;
+    }
+
+    self.messageModifyRepliesButton.hidden = !data.showMessageModifyReplies;
+    if (data.showMessageModifyReplies) {
+        NSString *title = [NSString stringWithFormat:@"%ld%@", data.messageModifyReplies.count, TIMCommonLocalizableString(TUIKitRepliesNum)];
+        [self.messageModifyRepliesButton setTitle:title forState:UIControlStateNormal];
+        [self.messageModifyRepliesButton sizeToFit];
+        [self.messageModifyRepliesButton setNeedsUpdateConstraints];
+        [self.messageModifyRepliesButton updateConstraintsIfNeeded];
+        [self.messageModifyRepliesButton layoutIfNeeded];
+    }
+
+    NSString *imageName = (data.showCheckBox && data.selected) ? TIMCommonImagePath(@"icon_select_selected") : TIMCommonImagePath(@"icon_select_normal");
+    self.selectedIcon.image = [UIImage imageNamed:imageName];
+
+    _timeLabel.text = [TUITool convertDateToStr:data.innerMessage.timestamp];
+    [_timeLabel sizeToFit];
+    _timeLabel.hidden = !data.showMessageTime;
+
+    /**
+     * Text highlighting - asynchronous operations are here to keep the order of execution consistent with subclasses
+     */
+    __weak typeof(self) weakSelf = self;
+    dispatch_async(dispatch_get_main_queue(), ^{
+      [weakSelf highlightWhenMatchKeyword:data.highlightKeyword];
+    });
+    
+    if(![data isKindOfClass:[TUISystemMessageCellData class]]){
+        if(data.headdress.length > 0){
+            self.headBgView.hidden = NO;
+            self.headBgView.isLiving = NO;
+            self.headBgView.effectType = data.headgearType;
+            self.headBgView.imgUrlStr = data.headdress;
+        }
+        else{
+            self.headBgView.hidden = YES;
+        }
+    }
+    else{
+        self.headBgView.hidden = YES;
+    }
+    
+    // tell constraints they need updating
+    [self setNeedsUpdateConstraints];
+
+    // update constraints now so we can animate the change
+    [self updateConstraintsIfNeeded];
+
+    [self layoutIfNeeded];
+
+}
+
+- (void)loadAvatar:(TUIMessageCellData *)data {
+    [self.avatarView setImage:DefaultAvatarImage];
+    @weakify(self);
+    [[[RACObserve(data, avatarUrl) takeUntil:self.rac_prepareForReuseSignal] ignore:nil] subscribeNext:^(NSURL *url) {
+      @strongify(self);
+      [self.avatarView sd_setImageWithURL:url placeholderImage:DefaultAvatarImage];
+    }];
+
+    if (data.isUseMsgReceiverAvatar) {
+        NSString *userId = @"";
+        if ([data.innerMessage.sender isEqualToString:V2TIMManager.sharedInstance.getLoginUser]) {
+            userId = data.innerMessage.userID;
+        } else {
+            userId = V2TIMManager.sharedInstance.getLoginUser;
+        }
+
+        [V2TIMManager.sharedInstance getUsersInfo:@[ userId?:@"" ]
+                                             succ:^(NSArray<V2TIMUserFullInfo *> *infoList) {
+                                               @strongify(self);
+                                               V2TIMUserFullInfo *info = infoList.firstObject;
+                                               if (info && [data isEqual:self.messageData]) {
+                                                   data.avatarUrl = [NSURL URLWithString:info.faceURL];
+                                                   [self.avatarView sd_setImageWithURL:data.avatarUrl placeholderImage:DefaultAvatarImage];
+                                               }
+                                             }
+                                             fail:^(int code, NSString *desc){
+
+                                             }];
+    }
+}
+
+- (void)highlightWhenMatchKeyword:(NSString *)keyword {
+    static NSString *const key = @"highlightAnimation";
+    if (keyword && keyword.length) {
+        if (self.highlightAnimating) {
+            return;
+        }
+        self.highlightAnimating = YES;
+        CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"backgroundColor"];
+        animation.repeatCount = 3;
+        animation.values = @[
+            (id)[[UIColor orangeColor] colorWithAlphaComponent:0.2].CGColor,
+            (id)[[UIColor orangeColor] colorWithAlphaComponent:0.5].CGColor,
+            (id)[[UIColor orangeColor] colorWithAlphaComponent:0.2].CGColor,
+        ];
+        animation.duration = 0.5;
+        animation.removedOnCompletion = YES;
+        animation.delegate = self;
+        [self.highlightAnimateView.layer addAnimation:animation forKey:key];
+    } else {
+        [self.highlightAnimateView.layer removeAnimationForKey:key];
+    }
+}
+
+- (void)updateReadLabelText {
+    if (self.messageData.innerMessage.groupID.length > 0) {
+        // group message
+        NSString *text = TIMCommonLocalizableString(Unread);
+        if (self.messageData.messageReceipt == nil) {
+            // haven't received the message receipt yet
+            return;
+        }
+        NSInteger readCount = self.messageData.messageReceipt.readCount;
+        NSInteger unreadCount = self.messageData.messageReceipt.unreadCount;
+        if (unreadCount == 0) {
+            // show "All read"
+            text = TIMCommonLocalizableString(TUIKitMessageReadAllRead);
+        } else if (readCount > 0) {
+            // show "x read"
+            text = [NSString stringWithFormat:@"%ld %@", (long)readCount, TIMCommonLocalizableString(TUIKitMessageReadPartRead)];
+        }
+        self.readReceiptLabel.text = text;
+    } else {
+        // c2c message
+        BOOL isPeerRead = self.messageData.messageReceipt.isPeerRead;
+        NSString *text = isPeerRead ? TIMCommonLocalizableString(TUIKitMessageReadC2CRead) : TIMCommonLocalizableString(TUIKitMessageReadC2CUnRead);
+        self.readReceiptLabel.text = text;
+    }
+    
+    [self.readReceiptLabel sizeToFit];
+    [self.readReceiptLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
+      make.bottom.mas_equalTo(self.container.mas_bottom);
+      make.trailing.mas_equalTo(self.container.mas_leading).mas_offset(-8);
+      make.size.mas_equalTo(self.readReceiptLabel.frame.size);
+    }];
+    self.readReceiptLabel.textColor = [self shouldHighlightReadReceiptLabel] ? TIMCommonDynamicColor(@"chat_message_read_status_text_color", @"#147AFF")
+                                                                             : TIMCommonDynamicColor(@"chat_message_read_status_text_gray_color", @"#BBBBBB");
+}
+
+- (UIView *)highlightAnimateView {
+    return self.container;
+}
+
+#pragma mark - TUIMessageCellProtocol
++ (CGFloat)getEstimatedHeight:(TUIMessageCellData *)data {
+    return 60.f;
+}
+
++ (CGFloat)getHeight:(TUIMessageCellData *)data withWidth:(CGFloat)width {
+    CGFloat height = 0;
+    if (data.showName) height += kScale375(20);
+    if (data.showMessageModifyReplies) height += kScale375(22);
+    
+    if (data.messageContainerAppendSize.height > 0) {
+        height += data.messageContainerAppendSize.height;
+    }
+    
+    CGSize containerSize = [self getContentSize:data];
+    height += containerSize.height;
+    height += data.cellLayout.messageInsets.top;
+    height += data.cellLayout.messageInsets.bottom;
+    
+    if (height < 55) height = 55;
+    return height;
+}
+
++ (CGSize)getContentSize:(TUIMessageCellData *)data {
+    return CGSizeZero;
+}
+
+#pragma mark - Private
+- (void)animationDidStart:(CAAnimation *)anim {
+    self.highlightAnimating = YES;
+}
+
+- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
+    self.highlightAnimating = NO;
+}
+
+#pragma mark-- Event
+- (void)onLongPress:(UIGestureRecognizer *)recognizer {
+    if ([recognizer isKindOfClass:[UILongPressGestureRecognizer class]] && recognizer.state == UIGestureRecognizerStateBegan) {
+        if (_delegate && [_delegate respondsToSelector:@selector(onLongPressMessage:)]) {
+            [_delegate onLongPressMessage:self];
+        }
+    }
+}
+
+- (void)onRetryMessage:(UIGestureRecognizer *)recognizer {
+    if (_messageData.status == Msg_Status_Fail)
+        if (_delegate && [_delegate respondsToSelector:@selector(onRetryMessage:)]) {
+            [_delegate onRetryMessage:self];
+        }
+}
+
+- (void)onSelectMessage:(UIGestureRecognizer *)recognizer {
+    if (_delegate && [_delegate respondsToSelector:@selector(onSelectMessage:)]) {
+        [_delegate onSelectMessage:self];
+    }
+}
+
+- (void)onSelectMessageAvatar:(UIGestureRecognizer *)recognizer {
+    if (_delegate && [_delegate respondsToSelector:@selector(onSelectMessageAvatar:)]) {
+        [_delegate onSelectMessageAvatar:self];
+    }
+}
+
+- (void)onLongSelectMessageAvatar:(UIGestureRecognizer *)recognizer {
+    if (_delegate && [_delegate respondsToSelector:@selector(onLongSelectMessageAvatar:)]) {
+        [_delegate onLongSelectMessageAvatar:self];
+    }
+}
+
+- (void)onSelectReadReceipt:(UITapGestureRecognizer *)gesture {
+    if (![self shouldHighlightReadReceiptLabel]) {
+        return;
+    }
+    if (_delegate && [_delegate respondsToSelector:@selector(onSelectReadReceipt:)]) {
+        [_delegate onSelectReadReceipt:self.messageData];
+    }
+}
+
+- (void)onJumpToRepliesDetailPage:(UIButton *)btn {
+    NSLog(@"click onJumpToRepliesDetailPage");
+    NSLog(@"%@", self.messageData.messageModifyReplies);
+
+    if (_delegate && [_delegate respondsToSelector:@selector(onJumpToRepliesDetailPage:)]) {
+        [_delegate onJumpToRepliesDetailPage:self.messageData];
+    }
+}
+- (BOOL)shouldHighlightReadReceiptLabel {
+    if (self.messageData.innerMessage.groupID.length == 0) {
+        return ![self.readReceiptLabel.text isEqualToString:TIMCommonLocalizableString(TUIKitMessageReadC2CRead)];
+    } else {
+        return ![self.readReceiptLabel.text isEqualToString:TIMCommonLocalizableString(TUIKitMessageReadAllRead)];
+    }
+}
+
+- (UIFont *)fontWithSize:(CGFloat)size {
+    static NSCache *fontCache;
+    if (fontCache == nil) {
+        fontCache = [[NSCache alloc] init];
+    }
+    UIFont *font = [fontCache objectForKey:@(size)];
+    if (font == nil) {
+        font = [UIFont systemFontOfSize:size];
+        [fontCache setObject:font forKey:@(size)];
+    }
+    return font;
+}
+
+- (void)notifyBottomContainerReadyOfData:(TUIMessageCellData *)cellData {
+    // Override by subclass.
+}
+
+@end
+
+
+@implementation TUIMessageCell (TUILayoutConfiguration)
+
+static UIColor *gOutgoingNameColor;
+
++ (UIColor *)outgoingNameColor {
+    if (!gOutgoingNameColor) {
+        gOutgoingNameColor = [UIColor d_systemGrayColor];
+    }
+    return gOutgoingNameColor;
+}
+
++ (void)setOutgoingNameColor:(UIColor *)outgoingNameColor {
+    gOutgoingNameColor = outgoingNameColor;
+}
+
+static UIFont *gOutgoingNameFont;
+
++ (UIFont *)outgoingNameFont {
+    if (!gOutgoingNameFont) {
+        gOutgoingNameFont = [UIFont systemFontOfSize:14];
+    }
+    return gOutgoingNameFont;
+}
+
++ (void)setOutgoingNameFont:(UIFont *)outgoingNameFont {
+    gOutgoingNameFont = outgoingNameFont;
+}
+
+static UIColor *gIncommingNameColor;
+
++ (UIColor *)incommingNameColor {
+    if (!gIncommingNameColor) {
+        gIncommingNameColor = [UIColor d_systemGrayColor];
+    }
+    return gIncommingNameColor;
+}
+
++ (void)setIncommingNameColor:(UIColor *)incommingNameColor {
+    gIncommingNameColor = incommingNameColor;
+}
+
+static UIFont *gIncommingNameFont;
+
++ (UIFont *)incommingNameFont {
+    if (!gIncommingNameFont) {
+        gIncommingNameFont = [UIFont systemFontOfSize:14];
+    }
+    return gIncommingNameFont;
+}
+
++ (void)setIncommingNameFont:(UIFont *)incommingNameFont {
+    gIncommingNameFont = incommingNameFont;
+}
+
+- (MOHeadNormalView *)headBgView{
+    if(!_headBgView){
+        _headBgView = [[MOHeadNormalView alloc] init];
+    }
+    return _headBgView;
+}
+
+@end

+ 20 - 0
ThirdParty/TUIKit/TIMCommon/BaseCell/TUISecurityStrikeView.h

@@ -0,0 +1,20 @@
+//
+//  TUISecurityStrikeView.h
+//  TIMCommon
+//
+//  Created by wyl on 2023/10/11.
+//  Copyright © 2023 Tencent. All rights reserved.
+
+#import <UIKit/UIKit.h>
+
+NS_ASSUME_NONNULL_BEGIN
+#define  kTUISecurityStrikeViewTopLineMargin 14.5
+#define  kTUISecurityStrikeViewTopLineToBottom 28
+@interface TUISecurityStrikeView : UIView
+@property(nonatomic, strong) UIView * topLine;
+@property(nonatomic, strong) UILabel * textLabel;
+
++ (UIImage *)changeImageColorWith:(UIColor *)color image:(UIImage *)image alpha:(CGFloat)alpha;
+@end
+
+NS_ASSUME_NONNULL_END

+ 73 - 0
ThirdParty/TUIKit/TIMCommon/BaseCell/TUISecurityStrikeView.m

@@ -0,0 +1,73 @@
+//
+//  TUISecurityStrikeView.m
+//  TIMCommon
+//
+//  Created by wyl on 2023/10/11.
+//  Copyright © 2023 Tencent. All rights reserved.
+
+#import "TUISecurityStrikeView.h"
+#import <TIMCommon/TIMDefine.h>
+
+@implementation TUISecurityStrikeView
+
+- (instancetype)initWithFrame:(CGRect)frame {
+    self = [super initWithFrame:frame];
+    if (self){
+        [self setupView];
+    }
+    return self;
+}
++ (BOOL)requiresConstraintBasedLayout {
+    return YES;
+}
+
+- (void)setupView {
+    self.topLine = [[UIView alloc] initWithFrame:CGRectZero];
+    self.topLine.backgroundColor = TUIDynamicColor(@"", TUIThemeModuleTIMCommon, @"#E5C7C7");
+    [self addSubview:self.topLine];
+    
+    self.textLabel = [[UILabel alloc] initWithFrame:CGRectZero];
+    [self addSubview:self.textLabel];
+    self.textLabel.font = [UIFont systemFontOfSize:14];
+    self.textLabel.text = TIMCommonLocalizableString(TUIKitMessageTypeSecurityStrike);
+    self.textLabel.textColor = TUIDynamicColor(@"", TUIThemeModuleTIMCommon, @"#DA2222");
+    self.textLabel.numberOfLines = 0;
+    self.textLabel.textAlignment = isRTL()?NSTextAlignmentRight:NSTextAlignmentLeft;
+
+}
+// this is Apple's recommended place for adding/updating constraints
+- (void)updateConstraints {
+    [super updateConstraints];
+    [self.topLine mas_remakeConstraints:^(MASConstraintMaker *make) {
+        make.top.mas_equalTo(kTUISecurityStrikeViewTopLineMargin);
+        make.leading.mas_equalTo(10);
+        make.trailing.mas_equalTo(-10);
+        make.height.mas_equalTo(0.5);
+    }];
+    
+    [self.textLabel sizeToFit];
+    [self.textLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
+        make.leading.mas_equalTo(10);
+        make.bottom.mas_equalTo(-11);
+        make.width.mas_equalTo(self);
+    }];
+
+}
+
++ (UIImage *)changeImageColorWith:(UIColor *)color image:(UIImage *)image alpha:(CGFloat)alpha {
+    UIGraphicsBeginImageContextWithOptions(image.size, NO, image.scale);
+    CGContextRef context = UIGraphicsGetCurrentContext();
+    CGContextTranslateCTM(context, 0, image.size.height);
+    CGContextScaleCTM(context, 1.0, -1.0);
+    CGContextSetAlpha(context, alpha);
+    CGContextSetBlendMode(context, kCGBlendModeNormal);
+    CGRect rect = CGRectMake(0, 0, image.size.width, image.size.height);
+    CGContextClipToMask(context, rect, image.CGImage);
+    [color setFill];
+    CGContextFillRect(context, rect);
+    UIImage*newImage = UIGraphicsGetImageFromCurrentImageContext();
+    UIGraphicsEndImageContext();
+    return newImage;
+}
+@end
+

+ 30 - 0
ThirdParty/TUIKit/TIMCommon/BaseCell/TUISystemMessageCell.h

@@ -0,0 +1,30 @@
+
+//  Created by Tencent on 2023/06/09.
+//  Copyright © 2023 Tencent. All rights reserved.
+/**
+ *
+ *  This file declares the TUISystemMessageCell class, which is responsible for displaying system messages.
+ *  The system message unit is responsible for displaying special messages from the system. Such messages are usually white on a gray background and centered.
+ */
+#import "TUIMessageCell.h"
+#import "TUISystemMessageCellData.h"
+
+/**
+ * 【Module name】 TUISystemMessageCell
+ * 【Function description】System message unit
+ *  - It is used to display the system messages. Common system messages include: recall-message, group-member-change-message, group-created and
+ * group-diss-message, etc.
+ *  - System messages are typically used to display notifications from apps that are sent by the system, not from any user.
+ */
+@interface TUISystemMessageCell : TUIMessageCell
+
+/**
+ *
+ *  The label of display system message content, such as "You recalled a message.".
+ */
+@property(readonly) UILabel *messageLabel;
+
+@property(readonly) TUISystemMessageCellData *systemData;
+
+- (void)fillWithData:(TUISystemMessageCellData *)data;
+@end

+ 107 - 0
ThirdParty/TUIKit/TIMCommon/BaseCell/TUISystemMessageCell.m

@@ -0,0 +1,107 @@
+//
+//  TUISystemMessageCell.m
+//  UIKit
+//
+//  Created by annidyfeng on 2019/5/30.
+//  Copyright © 2023 Tencent. All rights reserved.
+//
+
+#import "TUISystemMessageCell.h"
+#import <TIMCommon/TIMDefine.h>
+#import <TUICore/NSString+TUIUtil.h>
+
+@interface TUISystemMessageCell ()
+@property(nonatomic, strong) UILabel *messageLabel;
+@property TUISystemMessageCellData *systemData;
+@end
+
+@implementation TUISystemMessageCell
+
+- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
+    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
+    if (self) {
+        _messageLabel = [[UILabel alloc] init];
+        _messageLabel.textAlignment = NSTextAlignmentCenter;
+        _messageLabel.numberOfLines = 0;
+        _messageLabel.backgroundColor = [UIColor clearColor];
+        _messageLabel.layer.cornerRadius = 3;
+        [_messageLabel.layer setMasksToBounds:YES];
+        [self.container addSubview:_messageLabel];
+        self.backgroundColor = [UIColor clearColor];
+        self.contentView.backgroundColor = [UIColor clearColor];
+    }
+    return self;
+}
+
++ (BOOL)requiresConstraintBasedLayout {
+    return YES;
+}
+
+// this is Apple's recommended place for adding/updating constraints
+- (void)updateConstraints {
+    [super updateConstraints];
+    
+    [self.container mas_remakeConstraints:^(MASConstraintMaker *make) {
+        make.center.mas_equalTo(self.contentView);
+        make.size.mas_equalTo(self.contentView);
+    }];
+    [self.messageLabel sizeToFit];
+    if(self.messageLabel.superview) {
+        [self.messageLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
+            make.center.mas_equalTo(self.container);
+            make.leading.trailing.mas_equalTo(self.container);
+        }];
+    }
+}
+
+- (void)fillWithData:(TUISystemMessageCellData *)data;
+{
+    [super fillWithData:data];
+    self.systemData = data;
+    
+    self.messageLabel.textColor = TUISystemMessageCellData.textColor ? : data.contentColor;
+    self.messageLabel.font = TUISystemMessageCellData.textFont ? : data.contentFont;
+    self.messageLabel.backgroundColor = TUISystemMessageCellData.textBackgroundColor ? : [UIColor clearColor];
+    self.messageLabel.attributedText = data.attributedString;
+    
+    self.nameLabel.hidden = YES;
+    self.avatarView.hidden = YES;
+    self.retryView.hidden = YES;
+    [self.indicator stopAnimating];
+    
+    // tell constraints they need updating
+    [self setNeedsUpdateConstraints];
+    // update constraints now so we can animate the change
+    [self updateConstraintsIfNeeded];
+    [self layoutIfNeeded];
+}
+
+- (void)layoutSubviews {
+    [super layoutSubviews];
+}
+
+
+#pragma mark - TUIMessageCellProtocol
++ (CGFloat)getEstimatedHeight:(TUIMessageCellData *)data {
+    return 42.f;
+}
+
++ (CGFloat)getHeight:(TUIMessageCellData *)data withWidth:(CGFloat)width {
+    return [self getContentSize:data].height + kScale375(16);
+}
+
++ (CGSize)getContentSize:(TUIMessageCellData *)data {
+    NSAssert([data isKindOfClass:TUISystemMessageCellData.class], @"data must be kind of TUISystemMessageCellData");
+    TUISystemMessageCellData *systemCellData = (TUISystemMessageCellData *)data;
+    
+    static CGSize maxSystemSize;
+    if (CGSizeEqualToSize(maxSystemSize, CGSizeZero)) {
+        maxSystemSize = CGSizeMake(Screen_Width, MAXFLOAT);
+    }
+    CGSize size = [systemCellData.attributedString.string textSizeIn:maxSystemSize font:systemCellData.contentFont];
+    size.height += 10;
+    size.width += 16;
+    return size;
+}
+
+@end

+ 90 - 0
ThirdParty/TUIKit/TIMCommon/BaseCellData/NSString+TUIEmoji.h

@@ -0,0 +1,90 @@
+//
+//  NSString+TUIEmoji.h
+//  TUIChat
+//
+//  Created by harvy on 2021/11/15.
+//  Copyright © 2023 Tencent. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+#import <UIKit/UIKit.h>
+#import "TIMDefine.h"
+NS_ASSUME_NONNULL_BEGIN
+
+#define kSplitStringResultKey @"result"
+#define kSplitStringTextKey @"text"
+#define kSplitStringTextIndexKey @"textIndex"
+
+@interface NSString (TUIEmoji)
+
+/**
+ * Localize the emoji text in the current text and get the localized text
+ * eg: The original text was @"你好, [大哭]"
+ *    - If it is currently in English, this method converts the text to @"Hello,[Cry]"
+ *    - If the current is Chinese, this method converts the text to @"你好,[大哭]"
+ */
+- (NSString *)getLocalizableStringWithFaceContent;
+
+/**
+ * Internationalize the emoji text in the current text and get the internationalized text. The internationalized text of the emoji is Chinese
+ */
+- (NSString *)getInternationalStringWithfaceContent;
+
+/**
+ *
+ * Get the formatted emoticon text (after the image and text are mixed) The emoticon is stored in the NSTextAttachment object and cannot carry parameters
+ */
+- (NSMutableAttributedString *)getFormatEmojiStringWithFont:(UIFont *)textFont
+                                             emojiLocations:(nullable NSMutableArray<NSDictionary<NSValue *, NSAttributedString *> *> *)emojiLocations;
+
+/**
+ *
+ * Get the formatted emoji (after the image and text are mixed together) The emoji is stored in the TUIEmojiTextAttachment object, which can carry parameters.
+ * For example: the original text is @"Hello,[cry]", then this method turns the text into @"Hello,😭"
+ */
+- (NSMutableAttributedString *)getAdvancedFormatEmojiStringWithFont:(UIFont *)textFont
+                                                          textColor:(UIColor *)textColor
+                                                     emojiLocations:(nullable NSMutableArray<NSDictionary<NSValue *, NSAttributedString *> *> *)emojiLocations;
+
+- (NSString *)getEmojiImagePath;
+
+- (UIImage *)getEmojiImage;
+
+/**
+ * Split string using both emoji and @user. For instance,
+ * Origin string is @"hello[Grin]world, @user1 see you!", and users is @[@"user1"];
+ * Return value is:
+ * @{
+ *    kSplitStringResultKey:    @[@"hello", @"[Grin]", @"world, ", @"user1 ", @"see you!"],
+ *    kSplitStringTextKey:      @[@"hello", @"world, ", @"see you!"],
+ *    kSplitStringTextIndexKey: @[@0, @2, @4]
+ * }
+ * kSplitStringResultKey's value contains all elements after spliting.
+ * kSplitStringTextKey'value contains all text elements in the split result, excluding emojis and @user infos.
+ * kSplitStringTextIndexKey'value contains the location of text in split result.
+ */
+- (NSDictionary *)splitTextByEmojiAndAtUsers:(NSArray *_Nullable)users;
+
+/**
+ * Replace the element in array, whose index is in index with the corresponding value in replaceDict.
+ * For instance,
+ * array is         @[@"hello", @"[Grin]", @"world, ", @"user1 ", @"see you!"]
+ * index is         @[@0, @2, @4]
+ * replaceDict is   @{@"hello":@"你好", @"world":@"世界", @"see you!":@"再见!"}
+ * Return value is  @"你好[Grin]世界, @user1 再见!"
+ */
++ (NSString *)replacedStringWithArray:(NSArray *)array index:(NSArray *)index replaceDict:(NSDictionary *)replaceDict;
+
+@end
+
+@interface NSAttributedString (EmojiExtension)
+
+/**
+ *   @"你好,😭""  ->  @"你好,[大哭]"
+ *   @"Hello,😭"  ->  @"Hello,[Cry]"
+ */
+- (NSString *)tui_getPlainString;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 637 - 0
ThirdParty/TUIKit/TIMCommon/BaseCellData/NSString+TUIEmoji.m

@@ -0,0 +1,637 @@
+//
+//  NSString+TUIEmoji.m
+//  TUIChat
+//
+//  Created by harvy on 2021/11/15.
+//  Copyright © 2023 Tencent. All rights reserved.
+//
+
+#import "NSString+TUIEmoji.h"
+#import "TIMConfig.h"
+
+@implementation NSString (TUIEmoji)
+
++ (NSString *)getRegex_emoji {
+    
+    NSString *regex_emoji = @"\\[[a-zA-Z0-9_\\u4e00-\\u9fa5]+\\]";  // match emoji
+
+    return regex_emoji;
+}
+- (NSString *)getLocalizableStringWithFaceContent {
+    NSString *content = self;
+    NSString *regex_emoji = [self.class getRegex_emoji];  // match emoji
+    NSError *error = nil;
+    NSRegularExpression *re = [NSRegularExpression regularExpressionWithPattern:regex_emoji options:NSRegularExpressionCaseInsensitive error:&error];
+    if (re) {
+        NSArray *resultArray = [re matchesInString:content options:0 range:NSMakeRange(0, content.length)];
+        TUIFaceGroup *group = [TIMConfig defaultConfig].faceGroups[0];
+        NSMutableArray *waitingReplaceM = [NSMutableArray array];
+        for (NSTextCheckingResult *match in resultArray) {
+            NSRange range = [match range];
+            NSString *subStr = [content substringWithRange:range];
+            for (TUIFaceCellData *face in group.faces) {
+                if ([face.name isEqualToString:subStr]) {
+                    [waitingReplaceM
+                        addObject:@{@"range" : NSStringFromRange(range), @"localizableStr" : face.localizableName.length ? face.localizableName : face.name}];
+                    break;
+                }
+            }
+        }
+
+        if (waitingReplaceM.count) {
+            /**
+             * Replace from back to front, otherwise it will cause positional problems
+             */
+            for (int i = (int)waitingReplaceM.count - 1; i >= 0; i--) {
+                NSRange range = NSRangeFromString(waitingReplaceM[i][@"range"]);
+                NSString *localizableStr = waitingReplaceM[i][@"localizableStr"];
+                content = [content stringByReplacingCharactersInRange:range withString:localizableStr];
+            }
+        }
+    }
+    return content;
+}
+
+- (NSString *)getInternationalStringWithfaceContent {
+    NSString *content = self;
+    NSString *regex_emoji = [self.class getRegex_emoji];
+    NSError *error = nil;
+    NSRegularExpression *re = [NSRegularExpression regularExpressionWithPattern:regex_emoji options:NSRegularExpressionCaseInsensitive error:&error];
+    if (re) {
+        NSMutableDictionary *faceDict = [NSMutableDictionary dictionary];
+        TUIFaceGroup *group = [TIMConfig defaultConfig].faceGroups[0];
+        for (TUIFaceCellData *face in group.faces) {
+            NSString *key = face.localizableName ?: face.name;
+            NSString *value = face.name ?: @"";
+            faceDict[key] = value;
+        }
+
+        NSArray *resultArray = [re matchesInString:content options:0 range:NSMakeRange(0, content.length)];
+        NSMutableArray *waitingReplaceM = [NSMutableArray array];
+        for (NSTextCheckingResult *match in resultArray) {
+            NSRange range = [match range];
+            NSString *subStr = [content substringWithRange:range];
+            [waitingReplaceM addObject:@{@"range" : NSStringFromRange(range), @"localizableStr" : faceDict[subStr] ?: subStr}];
+        }
+
+        if (waitingReplaceM.count != 0) {
+            /**
+             * Replace from back to front, otherwise it will cause positional problems
+             */
+            for (int i = (int)waitingReplaceM.count - 1; i >= 0; i--) {
+                NSRange range = NSRangeFromString(waitingReplaceM[i][@"range"]);
+                NSString *localizableStr = waitingReplaceM[i][@"localizableStr"];
+                content = [content stringByReplacingCharactersInRange:range withString:localizableStr];
+            }
+        }
+    }
+    return content;
+}
+
+- (NSMutableAttributedString *)getFormatEmojiStringWithFont:(UIFont *)textFont
+                                             emojiLocations:(nullable NSMutableArray<NSDictionary<NSValue *, NSAttributedString *> *> *)emojiLocations {
+    /**
+     * First determine whether the text exists
+     */
+    if (self.length == 0) {
+        NSLog(@"getFormatEmojiStringWithFont failed , current text is nil");
+        return [[NSMutableAttributedString alloc] initWithString:@""];
+    }
+    /**
+     * 1. Create a mutable attributed string
+     */
+    NSMutableAttributedString *attributeString = [[NSMutableAttributedString alloc] initWithString:self];
+    if ([TIMConfig defaultConfig].faceGroups.count == 0) {
+        [attributeString addAttribute:NSFontAttributeName value:textFont range:NSMakeRange(0, attributeString.length)];
+        return attributeString;
+    }
+
+    /**
+     * 2.Match strings with regular expressions
+     */
+    NSError *error = nil;
+    static NSRegularExpression *re = nil;
+    if (re == nil) {
+        NSString *regex_emoji = [self.class getRegex_emoji];
+        re = [NSRegularExpression regularExpressionWithPattern:regex_emoji options:NSRegularExpressionCaseInsensitive error:&error];
+    }
+    if (!re) {
+        NSLog(@"%@", [error localizedDescription]);
+        return attributeString;
+    }
+
+    NSArray *resultArray = [re matchesInString:self options:0 range:NSMakeRange(0, self.length)];
+
+    TUIFaceGroup *group = [TIMConfig defaultConfig].faceGroups[0];
+
+    /**
+     * 3.Getting all emotes and locations
+     * - Used to store the dictionary, the dictionary stores the image and the corresponding location of the image
+     */
+    NSMutableArray *imageArray = [NSMutableArray arrayWithCapacity:resultArray.count];
+    /**
+     * Replace the image with the corresponding image according to the matching range
+     */
+    for (NSTextCheckingResult *match in resultArray) {
+        /**
+         * Get the range in the array element
+         */
+        NSRange range = [match range];
+        /**
+         * Get the corresponding value in the original string
+         */
+        NSString *subStr = [self substringWithRange:range];
+
+        for (TUIFaceCellData *face in group.faces) {
+            if ([face.name isEqualToString:subStr]) {
+                /**
+                 * - Create a new NSTextAttachment to store our image
+                 */
+                TUIEmojiTextAttachment *emojiTextAttachment = [[TUIEmojiTextAttachment alloc] init];
+                emojiTextAttachment.faceCellData = face;
+
+                NSString *localizableFaceName =  face.name;
+
+                // Set tag and image
+                emojiTextAttachment.emojiTag = localizableFaceName;
+                emojiTextAttachment.image = [[TUIImageCache sharedInstance] getFaceFromCache:face.path];
+                
+                // Set emoji size
+                emojiTextAttachment.emojiSize = kTIMDefaultEmojiSize;
+                NSAttributedString *str = [NSAttributedString attributedStringWithAttachment:emojiTextAttachment];
+
+                /**
+                 * - Convert attachments to mutable strings to replace emoji text in source strings
+                 */
+                NSAttributedString *imageStr = [NSAttributedString attributedStringWithAttachment:emojiTextAttachment];
+                /**
+                 * - Save the picture and the corresponding position of the picture into the dictionary
+                 */
+                NSMutableDictionary *imageDic = [NSMutableDictionary dictionaryWithCapacity:2];
+                [imageDic setObject:imageStr forKey:@"image"];
+                [imageDic setObject:[NSValue valueWithRange:range] forKey:@"range"];
+                /**
+                 * - Store dictionary in array
+                 */
+                [imageArray addObject:imageDic];
+                break;
+            }
+        }
+    }
+
+    /**
+     * 4.Replace from back to front, otherwise it will cause positional problems
+     */
+    NSMutableArray *locations = [NSMutableArray array];
+    for (int i = (int)imageArray.count - 1; i >= 0; i--) {
+        NSRange originRange;
+        [imageArray[i][@"range"] getValue:&originRange];
+
+        /**
+         * Store location information
+         */
+        NSAttributedString *originStr = [attributeString attributedSubstringFromRange:originRange];
+        NSAttributedString *currentStr = imageArray[i][@"image"];
+        [locations insertObject:@[ [NSValue valueWithRange:originRange], originStr, currentStr ] atIndex:0];
+
+        // Replace
+        [attributeString replaceCharactersInRange:originRange withAttributedString:currentStr];
+    }
+
+    /**
+     * 5.Getting the position information of the converted string of emoji
+     */
+    NSInteger offsetLocation = 0;
+    for (NSArray *obj in locations) {
+        NSArray *location = (NSArray *)obj;
+        NSRange originRange = [(NSValue *)location[0] rangeValue];
+        NSAttributedString *originStr = location[1];
+        NSAttributedString *currentStr = location[2];
+        NSRange currentRange;
+        currentRange.location = originRange.location + offsetLocation;
+        currentRange.length = currentStr.length;
+        offsetLocation += currentStr.length - originStr.length;
+        [emojiLocations addObject:@{[NSValue valueWithRange:currentRange] : originStr}];
+    }
+
+    NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
+    paragraphStyle.lineBreakMode = NSLineBreakByCharWrapping;
+    [attributeString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0, attributeString.length)];
+    [attributeString addAttribute:NSFontAttributeName value:textFont range:NSMakeRange(0, attributeString.length)];
+      
+    return attributeString;
+}
+
+- (NSString *)getEmojiImagePath {
+    TUIFaceGroup *group = [TIMConfig defaultConfig].faceGroups[0];
+
+    NSString *loaclName = [self getLocalizableStringWithFaceContent];
+    for (TUIFaceCellData *face in group.faces) {
+        if ([face.localizableName isEqualToString:loaclName]) {
+            return face.path;
+        }
+    }
+    return nil;
+}
+
+- (UIImage *)getEmojiImage {
+    TUIFaceGroup *group = [TIMConfig defaultConfig].faceGroups[0];
+
+    for (TUIFaceCellData *face in group.faces) {
+        if ([face.name isEqualToString:self]) {
+            return [[TUIImageCache sharedInstance] getFaceFromCache:face.path];
+        }
+    }
+    return nil;
+}
+
+- (NSMutableAttributedString *)getAdvancedFormatEmojiStringWithFont:(UIFont *)textFont
+                                                          textColor:(UIColor *)textColor
+                                                     emojiLocations:(nullable NSMutableArray<NSDictionary<NSValue *, NSAttributedString *> *> *)emojiLocations {
+    if (self.length == 0) {
+        NSLog(@"getAdvancedFormatEmojiStringWithFont failed , current text is nil");
+        return [[NSMutableAttributedString alloc] initWithString:@""];
+    }
+    NSMutableAttributedString *attributeString = [[NSMutableAttributedString alloc] initWithString:self];
+    if ([TIMConfig defaultConfig].faceGroups.count == 0) {
+        [attributeString addAttribute:NSFontAttributeName value:textFont range:NSMakeRange(0, attributeString.length)];
+        return attributeString;
+    }
+
+    NSString *regex_emoji = [self.class getRegex_emoji];
+
+    NSError *error = nil;
+    NSRegularExpression *re = [NSRegularExpression regularExpressionWithPattern:regex_emoji options:NSRegularExpressionCaseInsensitive error:&error];
+    if (error) {
+        NSLog(@"%@", [error localizedDescription]);
+        return attributeString;
+    }
+
+    NSArray *resultArray = [re matchesInString:self options:0 range:NSMakeRange(0, self.length)];
+
+    TUIFaceGroup *group = [TIMConfig defaultConfig].faceGroups[0];
+
+    NSMutableArray *imageArray = [NSMutableArray arrayWithCapacity:resultArray.count];
+
+    for (NSTextCheckingResult *match in resultArray) {
+        NSRange range = [match range];
+
+        NSString *subStr = [self substringWithRange:range];
+
+        for (TUIFaceCellData *face in group.faces) {
+            if ([face.name isEqualToString:subStr] || [face.localizableName isEqualToString:subStr]) {
+                TUIEmojiTextAttachment *emojiTextAttachment = [[TUIEmojiTextAttachment alloc] init];
+                emojiTextAttachment.faceCellData = face;
+
+                // Set tag and image
+                emojiTextAttachment.emojiTag = face.name;
+                emojiTextAttachment.image = [[TUIImageCache sharedInstance] getFaceFromCache:face.path];
+
+                // Set emoji size
+                emojiTextAttachment.emojiSize = kTIMDefaultEmojiSize;
+
+                NSAttributedString *imageStr = [NSAttributedString attributedStringWithAttachment:emojiTextAttachment];
+
+                NSMutableDictionary *imageDic = [NSMutableDictionary dictionaryWithCapacity:2];
+                [imageDic setObject:imageStr forKey:@"image"];
+                [imageDic setObject:[NSValue valueWithRange:range] forKey:@"range"];
+
+                [imageArray addObject:imageDic];
+                break;
+            }
+        }
+    }
+
+    NSMutableArray *locations = [NSMutableArray array];
+    for (int i = (int)imageArray.count - 1; i >= 0; i--) {
+        NSRange originRange;
+        [imageArray[i][@"range"] getValue:&originRange];
+
+        NSAttributedString *originStr = [attributeString attributedSubstringFromRange:originRange];
+        NSAttributedString *currentStr = imageArray[i][@"image"];
+        [locations insertObject:@[ [NSValue valueWithRange:originRange], originStr, currentStr ] atIndex:0];
+
+        [attributeString replaceCharactersInRange:originRange withAttributedString:currentStr];
+    }
+
+    NSInteger offsetLocation = 0;
+    for (NSArray *obj in locations) {
+        NSArray *location = (NSArray *)obj;
+        NSRange originRange = [(NSValue *)location[0] rangeValue];
+        NSAttributedString *originStr = location[1];
+        NSAttributedString *currentStr = location[2];
+        NSRange currentRange;
+        currentRange.location = originRange.location + offsetLocation;
+        currentRange.length = currentStr.length;
+        offsetLocation += currentStr.length - originStr.length;
+        [emojiLocations addObject:@{[NSValue valueWithRange:currentRange] : originStr}];
+    }
+
+    [attributeString addAttribute:NSFontAttributeName value:textFont range:NSMakeRange(0, attributeString.length)];
+    [attributeString addAttribute:NSForegroundColorAttributeName value:textColor range:NSMakeRange(0, attributeString.length)];
+
+    return attributeString;
+}
+
+/**
+ * Steps:
+ * 1. Match @user infos in string.
+ * 2. Split origin string into array(A) by @user info's ranges.
+ * 3. Iterate the array(A) to match emoji one by one.
+ * 4. Add all parsed elements(emoji, @user, pure text) into result.
+ * 5. Process the text and textIndex by the way.
+ * 6. Encapsulate all arrays in a dict and return it.
+ */
+- (NSDictionary *)splitTextByEmojiAndAtUsers:(NSArray *_Nullable)users {
+    if (self.length == 0) {
+        return nil;
+    }
+    NSMutableArray *result = [NSMutableArray new];
+
+    /// Find @user info's ranges in string.
+    NSMutableArray *atUsers = [NSMutableArray new];
+    for (NSString *user in users) {
+        /// Add an whitespace after the user's name due to the special format of @ content.
+        NSString *atUser = [NSString stringWithFormat:@"@%@ ", user];
+        [atUsers addObject:atUser];
+    }
+    NSArray *atUserRanges = [self rangeOfAtUsers:atUsers inString:self];
+
+    /// Split text using @user info's ranges.
+    NSArray *splitResult = [self splitArrayWithRanges:atUserRanges inString:self];
+    NSMutableArray *splitArrayByAtUser = splitResult.firstObject;
+    NSSet *atUserIndex = splitResult.lastObject;
+
+    /// Iterate the split array after finding @user, aimed to match emoji.
+    NSInteger k = -1;
+    NSMutableArray *textIndexArray = [NSMutableArray new];
+    for (int i = 0; i < splitArrayByAtUser.count; i++) {
+        NSString *str = splitArrayByAtUser[i];
+        if ([atUserIndex containsObject:@(i)]) {
+            /// str is @user info.
+            [result addObject:str];
+            k += 1;
+        } else {
+            /// str is not @user info, try to parse emoji in the same way as above.
+            NSArray *emojiRanges = [self matchTextByEmoji:str];
+            splitResult = [self splitArrayWithRanges:emojiRanges inString:str];
+            NSMutableArray *splitArrayByEmoji = splitResult.firstObject;
+            NSSet *emojiIndex = splitResult.lastObject;
+            for (int j = 0; j < splitArrayByEmoji.count; j++) {
+                NSString *tmp = splitArrayByEmoji[j];
+                [result addObject:tmp];
+                k += 1;
+                if (![emojiIndex containsObject:@(j)]) {
+                    /// str is text.
+                    [textIndexArray addObject:@(k)];
+                }
+            }
+        }
+    }
+
+    NSMutableArray *textArray = [NSMutableArray new];
+    for (NSNumber *n in textIndexArray) {
+        [textArray addObject:result[[n integerValue]]];
+    }
+
+    NSDictionary *dict = @{kSplitStringResultKey : result, kSplitStringTextKey : textArray, kSplitStringTextIndexKey : textIndexArray};
+    return dict;
+}
+
+/// Find all ranges of @user in string.
+- (NSArray *)rangeOfAtUsers:(NSArray *)atUsers inString:(NSString *)string {
+    /// Find all positions of character "@".
+    NSString *tmp = nil;
+    NSMutableIndexSet *atIndex = [NSMutableIndexSet new];
+    for (int i = 0; i < [string length]; i++) {
+        tmp = [string substringWithRange:NSMakeRange(i, 1)];
+        if ([tmp isEqualToString:@"@"]) {
+            [atIndex addIndex:i];
+        }
+    }
+
+    /// Match @user with "@" position.
+    NSMutableArray *result = [NSMutableArray new];
+    for (NSString *user in atUsers) {
+        [atIndex enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
+          if (string.length >= user.length && idx <= string.length - user.length) {
+              NSRange range = NSMakeRange(idx, user.length);
+              if ([[string substringWithRange:range] isEqualToString:user]) {
+                  [result addObject:[NSValue valueWithRange:range]];
+                  [atIndex removeIndex:idx];
+                  *stop = YES;
+              }
+          }
+        }];
+    }
+    return result;
+}
+
+/// Split string into multi substrings by given ranges.
+/// Return value's structure is [result, indexes], in which indexs means position of content within ranges located in result after spliting.
+- (NSArray *)splitArrayWithRanges:(NSArray *)ranges inString:(NSString *)string {
+    if (ranges.count == 0) {
+        return @[ @[ string ], @[] ];
+    }
+    if (string.length == 0) {
+        return nil;
+    }
+
+    /// Ascending sort.
+    NSArray *sortedRanges = [ranges sortedArrayUsingComparator:^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2) {
+      NSRange range1 = [obj1 rangeValue];
+      NSRange range2 = [obj2 rangeValue];
+      if (range1.location < range2.location) {
+          return (NSComparisonResult)NSOrderedAscending;
+      } else if (range1.location > range2.location) {
+          return (NSComparisonResult)NSOrderedDescending;
+      } else {
+          return (NSComparisonResult)NSOrderedSame;
+      }
+    }];
+
+    NSMutableArray *result = [NSMutableArray new];
+    NSMutableSet *indexes = [NSMutableSet new];
+    NSInteger prev = 0;
+    NSInteger i = 0;
+    NSInteger j = -1;
+    while (i < sortedRanges.count) {
+        NSRange cur = [sortedRanges[i] rangeValue];
+        NSString *str = nil;
+        if (cur.location > prev) {
+            /// Add the str in [prev, cur.location).
+            str = [string substringWithRange:NSMakeRange(prev, cur.location - prev)];
+            [result addObject:str];
+            j += 1;
+        }
+
+        /// Add the str in cur range.
+        str = [string substringWithRange:cur];
+        [result addObject:str];
+        j += 1;
+        [indexes addObject:@(j)];
+
+        /// Update prev to support calculation of next round.
+        prev = cur.location + cur.length;
+
+        /// Text exists after the last emoji.
+        if (i == sortedRanges.count - 1 && prev < string.length - 1) {
+            NSString *last = [string substringWithRange:NSMakeRange(prev, string.length - prev)];
+            [result addObject:last];
+        }
+
+        i++;
+    }
+
+    return @[ result, indexes ];
+}
+
+/// Match text by emoji, return the matched ranges
+- (NSArray *)matchTextByEmoji:(NSString *)text {
+    NSMutableArray *result = [NSMutableArray new];
+
+    /// TUIKit qq emoji.
+    NSString *regexOfCustomEmoji = [self.class getRegex_emoji];
+    NSError *error = nil;
+    NSRegularExpression *re = [NSRegularExpression regularExpressionWithPattern:regexOfCustomEmoji options:NSRegularExpressionCaseInsensitive error:&error];
+    if (error) {
+        NSLog(@"re match custom emoji failed, error: %@", [error localizedDescription]);
+        return nil;
+    }
+    NSArray *matchResult = [re matchesInString:text options:0 range:NSMakeRange(0, text.length)];
+    for (NSTextCheckingResult *match in matchResult) {
+        NSString *substring = [text substringWithRange:match.range];
+        TUIFaceGroup *group = [TIMConfig defaultConfig].faceGroups[0];
+        for (TUIFaceCellData *face in group.faces) {
+            if ([face.name isEqualToString:substring] || [face.localizableName isEqualToString:substring]) {
+                [result addObject:[NSValue valueWithRange:match.range]];
+                break;
+            }
+        }
+    }
+
+    /// Unicode emoji.
+    NSString *regexOfUnicodeEmoji = [NSString unicodeEmojiReString];
+    re = [NSRegularExpression regularExpressionWithPattern:regexOfUnicodeEmoji options:NSRegularExpressionCaseInsensitive error:&error];
+    if (error) {
+        NSLog(@"re match universal emoji failed, error: %@", [error localizedDescription]);
+        return [result copy];
+    }
+    matchResult = [re matchesInString:text options:0 range:NSMakeRange(0, text.length)];
+    for (NSTextCheckingResult *match in matchResult) {
+        [result addObject:[NSValue valueWithRange:match.range]];
+    }
+
+    return [result copy];
+}
+
++ (NSString *)replacedStringWithArray:(NSArray *)array index:(NSArray *)indexArray replaceDict:(NSDictionary *)replaceDict {
+    if (replaceDict == nil) {
+        return nil;
+    }
+    NSMutableArray *mutableArray = [array mutableCopy];
+    for (NSNumber *value in indexArray) {
+        NSInteger i = [value integerValue];
+        if (i < 0 || i > mutableArray.count - 1) {
+            continue;
+        }
+        if (replaceDict[mutableArray[i]]) {
+            mutableArray[i] = replaceDict[mutableArray[i]];
+        }
+    }
+    return [mutableArray componentsJoinedByString:@""];
+}
+
+/**
+ * Regex of unicode emoji, refer to https://unicode.org/reports/tr51/#EBNF_and_Regex
+ * Regex exression is like:
+ \p{ri} \p{ri}
+ | \p{Emoji}
+   ( \p{EMod}
+   | \x{FE0F} \x{20E3}?
+   | [\x{E0020}-\x{E007E}]+ \x{E007F}
+   )?
+   (\x{200D}
+     ( \p{ri} \p{ri}
+     | \p{Emoji}
+       ( \p{EMod}
+       | \x{FE0F} \x{20E3}?
+       | [\x{E0020}-\x{E007E}]+ \x{E007F}
+       )?
+     )
+   )*
+ */
++ (NSString *)unicodeEmojiReString {
+    NSString *ri = @"[\U0001F1E6-\U0001F1FF]";
+
+    /// \u0023(#), \u002A(*), \u0030(keycap 0), \u0039(keycap 9), \u00A9(©), \u00AE(®) couldn't be added to NSString directly, need to transform a little bit.
+    NSString *unsupport = [NSString stringWithFormat:@"%C|%C|[%C-%C]|", 0x0023, 0x002A, 0x0030, 0x0039];
+    NSString *support =
+        @"\U000000A9|\U000000AE|\u203C|\u2049|\u2122|\u2139|[\u2194-\u2199]|[\u21A9-\u21AA]|[\u231A-\u231B]|\u2328|\u23CF|[\u23E9-\u23EF]|[\u23F0-\u23F3]|["
+        @"\u23F8-\u23FA]|\u24C2|[\u25AA-\u25AB]|\u25B6|\u25C0|[\u25FB-\u25FE]|[\u2600-\u2604]|\u260E|\u2611|[\u2614-\u2615]|\u2618|\u261D|\u2620|[\u2622-"
+        @"\u2623]|\u2626|\u262A|[\u262E-\u262F]|[\u2638-\u263A]|\u2640|\u2642|[\u2648-\u264F]|[\u2650-\u2653]|\u265F|\u2660|\u2663|[\u2665-\u2666]|\u2668|"
+        @"\u267B|[\u267E-\u267F]|[\u2692-\u2697]|\u2699|[\u269B-\u269C]|[\u26A0-\u26A1]|\u26A7|[\u26AA-\u26AB]|[\u26B0-\u26B1]|[\u26BD-\u26BE]|[\u26C4-\u26C5]|"
+        @"\u26C8|[\u26CE-\u26CF]|\u26D1|[\u26D3-\u26D4]|[\u26E9-\u26EA]|[\u26F0-\u26F5]|[\u26F7-\u26FA]|\u26FD|\u2702|\u2705|[\u2708-\u270D]|\u270F|\u2712|"
+        @"\u2714|\u2716|\u271D|\u2721|\u2728|[\u2733-\u2734]|\u2744|\u2747|\u274C|\u274E|[\u2753-\u2755]|\u2757|[\u2763-\u2764]|[\u2795-\u2797]|\u27A1|\u27B0|"
+        @"\u27BF|[\u2934-\u2935]|[\u2B05-\u2B07]|[\u2B1B-\u2B1C]|\u2B50|\u2B55|\u3030|\u303D|\u3297|\u3299|\U0001F004|\U0001F0CF|[\U0001F170-\U0001F171]|["
+        @"\U0001F17E-\U0001F17F]|\U0001F18E|[\U0001F191-\U0001F19A]|[\U0001F1E6-\U0001F1FF]|[\U0001F201-\U0001F202]|\U0001F21A|\U0001F22F|[\U0001F232-"
+        @"\U0001F23A]|[\U0001F250-\U0001F251]|[\U0001F300-\U0001F30F]|[\U0001F310-\U0001F31F]|[\U0001F320-\U0001F321]|[\U0001F324-\U0001F32F]|[\U0001F330-"
+        @"\U0001F33F]|[\U0001F340-\U0001F34F]|[\U0001F350-\U0001F35F]|[\U0001F360-\U0001F36F]|[\U0001F370-\U0001F37F]|[\U0001F380-\U0001F38F]|[\U0001F390-"
+        @"\U0001F393]|[\U0001F396-\U0001F397]|[\U0001F399-\U0001F39B]|[\U0001F39E-\U0001F39F]|[\U0001F3A0-\U0001F3AF]|[\U0001F3B0-\U0001F3BF]|[\U0001F3C0-"
+        @"\U0001F3CF]|[\U0001F3D0-\U0001F3DF]|[\U0001F3E0-\U0001F3EF]|\U0001F3F0|[\U0001F3F3-\U0001F3F5]|[\U0001F3F7-\U0001F3FF]|[\U0001F400-\U0001F40F]|["
+        @"\U0001F410-\U0001F41F]|[\U0001F420-\U0001F42F]|[\U0001F430-\U0001F43F]|[\U0001F440-\U0001F44F]|[\U0001F450-\U0001F45F]|[\U0001F460-\U0001F46F]|["
+        @"\U0001F470-\U0001F47F]|[\U0001F480-\U0001F48F]|[\U0001F490-\U0001F49F]|[\U0001F4A0-\U0001F4AF]|[\U0001F4B0-\U0001F4BF]|[\U0001F4C0-\U0001F4CF]|["
+        @"\U0001F4D0-\U0001F4DF]|[\U0001F4E0-\U0001F4EF]|[\U0001F4F0-\U0001F4FF]|[\U0001F500-\U0001F50F]|[\U0001F510-\U0001F51F]|[\U0001F520-\U0001F52F]|["
+        @"\U0001F530-\U0001F53D]|[\U0001F549-\U0001F54E]|[\U0001F550-\U0001F55F]|[\U0001F560-\U0001F567]|\U0001F56F|\U0001F570|[\U0001F573-\U0001F57A]|"
+        @"\U0001F587|[\U0001F58A-\U0001F58D]|\U0001F590|[\U0001F595-\U0001F596]|[\U0001F5A4-\U0001F5A5]|\U0001F5A8|[\U0001F5B1-\U0001F5B2]|\U0001F5BC|["
+        @"\U0001F5C2-\U0001F5C4]|[\U0001F5D1-\U0001F5D3]|[\U0001F5DC-\U0001F5DE]|\U0001F5E1|\U0001F5E3|\U0001F5E8|\U0001F5EF|\U0001F5F3|[\U0001F5FA-\U0001F5FF]"
+        @"|[\U0001F600-\U0001F60F]|[\U0001F610-\U0001F61F]|[\U0001F620-\U0001F62F]|[\U0001F630-\U0001F63F]|[\U0001F640-\U0001F64F]|[\U0001F650-\U0001F65F]|["
+        @"\U0001F660-\U0001F66F]|[\U0001F670-\U0001F67F]|[\U0001F680-\U0001F68F]|[\U0001F690-\U0001F69F]|[\U0001F6A0-\U0001F6AF]|[\U0001F6B0-\U0001F6BF]|["
+        @"\U0001F6C0-\U0001F6C5]|[\U0001F6CB-\U0001F6CF]|[\U0001F6D0-\U0001F6D2]|[\U0001F6D5-\U0001F6D7]|[\U0001F6DD-\U0001F6DF]|[\U0001F6E0-\U0001F6E5]|"
+        @"\U0001F6E9|[\U0001F6EB-\U0001F6EC]|\U0001F6F0|[\U0001F6F3-\U0001F6FC]|[\U0001F7E0-\U0001F7EB]|\U0001F7F0|[\U0001F90C-\U0001F90F]|[\U0001F910-"
+        @"\U0001F91F]|[\U0001F920-\U0001F92F]|[\U0001F930-\U0001F93A]|[\U0001F93C-\U0001F93F]|[\U0001F940-\U0001F945]|[\U0001F947-\U0001F94C]|[\U0001F94D-"
+        @"\U0001F94F]|[\U0001F950-\U0001F95F]|[\U0001F960-\U0001F96F]|[\U0001F970-\U0001F97F]|[\U0001F980-\U0001F98F]|[\U0001F990-\U0001F99F]|[\U0001F9A0-"
+        @"\U0001F9AF]|[\U0001F9B0-\U0001F9BF]|[\U0001F9C0-\U0001F9CF]|[\U0001F9D0-\U0001F9DF]|[\U0001F9E0-\U0001F9EF]|[\U0001F9F0-\U0001F9FF]|[\U0001FA70-"
+        @"\U0001FA74]|[\U0001FA78-\U0001FA7C]|[\U0001FA80-\U0001FA86]|[\U0001FA90-\U0001FA9F]|[\U0001FAA0-\U0001FAAC]|[\U0001FAB0-\U0001FABA]|[\U0001FAC0-"
+        @"\U0001FAC5]|[\U0001FAD0-\U0001FAD9]|[\U0001FAE0-\U0001FAE7]|[\U0001FAF0-\U0001FAF6]";
+    NSString *emoji = [NSString stringWithFormat:@"[%@%@]", unsupport, support];
+
+    /// Construct regex of emoji by the rules above.
+    NSString *eMod = @"[\U0001F3FB-\U0001F3FF]";
+
+    NSString *variationSelector = @"\uFE0F";
+    NSString *keycap = @"\u20E3";
+    NSString *tags = @"[\U000E0020-\U000E007E]";
+    NSString *termTag = @"\U000E007F";
+    NSString *zwj = @"\u200D";
+
+    NSString *riSequence = [NSString stringWithFormat:@"[%@][%@]", ri, ri];
+    NSString *element = [NSString stringWithFormat:@"[%@]([%@]|%@%@?|[%@]+%@)?", emoji, eMod, variationSelector, keycap, tags, termTag];
+
+    NSString *regexEmoji = [NSString stringWithFormat:@"%@|%@(%@(%@|%@))*", riSequence, element, zwj, riSequence, element];
+    return regexEmoji;
+}
+
+@end
+
+@implementation NSAttributedString (EmojiExtension)
+
+- (NSString *)tui_getPlainString {
+    NSMutableString *plainString = [NSMutableString stringWithString:self.string];
+    __block NSUInteger base = 0;
+
+    [self enumerateAttribute:NSAttachmentAttributeName
+                     inRange:NSMakeRange(0, self.length)
+                     options:0
+                  usingBlock:^(id value, NSRange range, BOOL *stop) {
+                    if (value && [value isKindOfClass:[TUIEmojiTextAttachment class]]) {
+                        [plainString replaceCharactersInRange:NSMakeRange(range.location + base, range.length)
+                                                   withString:((TUIEmojiTextAttachment *)value).emojiTag];
+                        base += ((TUIEmojiTextAttachment *)value).emojiTag.length - 1;
+                    }
+                  }];
+
+    return plainString;
+}
+
+@end

+ 30 - 0
ThirdParty/TUIKit/TIMCommon/BaseCellData/TUIBubbleMessageCellData.h

@@ -0,0 +1,30 @@
+
+//  Created by Tencent on 2023/06/09.
+//  Copyright © 2023 Tencent. All rights reserved.
+/**
+ *
+ *  - This file declares the TUIBubbleMessageCellData class.
+ *  - This class inherits from TUIMessageCellData and is used to store a series of data and information required by the bubble message unit.
+ *  - This class is used as the base class for the data source of the bubble message. When you want to implement a custom bubble message,
+ *   you also need to make the data source of the corresponding message inherit from this class.
+ *
+ */
+#import "TUIMessageCellData.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ *
+ * 【Module name】TUIBubbleMessageCellData
+ * 【Function description】Bubble message data source.
+ *  - Bubble messages, the most common type of messages that contain text and emoji characters, will be your most common type of message in most cases.
+ *  - The Bubble Message data source (hereinafter referred to as the data source) is responsible for storing various information required to render the Bubble
+ * Message UI.
+ *  - The data source implements a series of business logic that can provide the required information to the Bubble Message UI.
+ *  - Both TUIFileMessageCellData and TUIVoiceMessageCellData inherit from this class and implement the UI of bubble messages.
+ */
+@interface TUIBubbleMessageCellData : TUIMessageCellData
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 15 - 0
ThirdParty/TUIKit/TIMCommon/BaseCellData/TUIBubbleMessageCellData.m

@@ -0,0 +1,15 @@
+//
+//  TUIBubbleMessageCellData.m
+//  TXIMSDK_TUIKit_iOS
+//
+//  Created by annidyfeng on 2019/5/30.
+//  Copyright © 2023 Tencent. All rights reserved.
+//
+
+#import "TUIBubbleMessageCellData.h"
+#import <TIMCommon/TIMDefine.h>
+#import <TUICore/TUIThemeManager.h>
+
+@implementation TUIBubbleMessageCellData
+
+@end

+ 283 - 0
ThirdParty/TUIKit/TIMCommon/BaseCellData/TUIMessageCellData.h

@@ -0,0 +1,283 @@
+
+//  Created by Tencent on 2023/06/09.
+//  Copyright © 2023 Tencent. All rights reserved.
+/**
+ *
+ * This file declares the TUIMessageCellData class.
+ * - The "message unit" data source, as the parent class of various detailed data sources, provides basic templates for the properties and behaviors of various
+ * "message unit" data sources.
+ * - The "data source class" in this document is the base class for all message data, and each type of data source inherits from this class or its subclasses.
+ * - When you want to customize the message, you need to inherit the data source of the customized message from this class or a subclass of this class.
+ */
+#import <TIMCommon/TIMCommonModel.h>
+#import <TIMCommon/TIMDefine.h>
+#import "TUIMessageCellLayout.h"
+@class TUIRelationUserModel;
+
+NS_ASSUME_NONNULL_BEGIN
+
+typedef void (^TDownloadProgress)(NSInteger curSize, NSInteger totalSize);
+typedef void (^TDownloadResponse)(int code, NSString *desc, NSString *path);
+
+/**
+ *  The definition of message status
+ */
+typedef NS_ENUM(NSUInteger, TMsgStatus) {
+    Msg_Status_Init,       //  message initial
+    Msg_Status_Sending,    //  message sending
+    Msg_Status_Sending_2,  //  message sending, recommended
+    Msg_Status_Succ,       //  message sent successfully
+    Msg_Status_Fail,       //  Failed to send message
+};
+
+/**
+ *
+ *  The definition of message direction
+ *  Message direction affects UI styles such as bubble icons, bubble positions, etc.
+ */
+typedef NS_ENUM(NSUInteger, TMsgDirection) {
+    MsgDirectionIncoming,
+    MsgDirectionOutgoing,
+};
+
+/**
+ *
+ *  The source of message
+ *  Different display logic can be done according to the source of the message.
+ */
+typedef NS_ENUM(NSUInteger, TMsgSource) {
+    Msg_Source_Unkown = 0,    // 未知
+    Msg_Source_OnlinePush,    // Messages actively pushed in the background
+    Msg_Source_GetHistory,    // SDK actively requests historical messages pulled from the background
+};
+
+/**
+ * 【Module name】TUIMessageCellData
+ * 【Function description】The data source of the chat message unit cooperates with the message controller to realize the business logic of message sending and
+ * receiving.
+ *  - It is used to store various data and information required for message management and logic implementation. Including a series of data such as message
+ * status, message sender ID and avatar.
+ *  - The chat information data unit integrates and calls the IM SDK, and can implement the business logic of the message through the interface provided by the
+ * SDK.
+ */
+@interface TUIMessageCellData : TUICommonCellData
+/**
+ *  Getting cellData according to message
+ */
++ (TUIMessageCellData *)getCellData:(V2TIMMessage *)message;
+
+/**
+ * Getting the display string according to the message
+ */
++ (NSString *)getDisplayString:(V2TIMMessage *)message;
+
+/**
+ * Class to get the layout of the message reply custom reference and its data
+ */
+- (Class)getReplyQuoteViewDataClass;
+- (Class)getReplyQuoteViewClass;
+
+/**
+ *  Message unique id
+ */
+@property(nonatomic, strong) NSString *msgID;
+
+/**
+ *  Message sender ID
+ */
+@property(nonatomic, strong) NSString *identifier;
+
+/**
+ *  Message display sender name
+ */
+@property(nonatomic, strong, readonly) NSString *senderName;
+
+/**
+ *  Sender's avatar url
+ */
+@property(nonatomic, strong) NSURL *__nullable avatarUrl;
+
+/**
+ *  Sender's avatar
+ */
+@property(nonatomic, strong) UIImage *__nullable avatarImage __attribute__((deprecated("not supported")));
+
+@property (nonatomic, strong) NSString *headdress;//头饰链接
+@property (nonatomic, assign) NSInteger headgearType;//头饰 - 头饰类型(0=无资源文件,1=动态webp,2=SVGA,3=VAP)
+
+/**
+ * Whether to use the receiver's avatar, default is NO
+ */
+@property(nonatomic, assign) BOOL isUseMsgReceiverAvatar;
+
+/**
+ *
+ *  The flag of showing name
+ *  - In 1 vs 1 chat, the nickname is not displayed in the message by default.
+ *  - In group chat, the nickname is displayed for messages sent by other users in the group.
+ *  - YES: showing nickname;  NO: hidden nickname
+ */
+@property(nonatomic, assign) BOOL showName;
+
+/**
+ *  Display user avatar
+ */
+@property(nonatomic, assign) BOOL showAvatar;
+
+/**
+ *  Whether the current message is the same as the sender of the next message
+ */
+@property(nonatomic, assign) BOOL sameToNextMsgSender;
+
+/**
+ *
+ * The flag of showing message multiple selection
+ * - In the message list, the selection button is not displayed by default. When you long press the message to pop up the multi-select button and click it, the
+ * message list becomes multi-selectable.
+ * - YES: Enable multiple selection, multiple selection views are displayed; NO: Disable multiple selection, the default view is displayed.
+ */
+@property(nonatomic, assign) BOOL showCheckBox;
+
+/**
+ * The flag of selected
+ */
+@property(nonatomic, assign) BOOL selected;
+
+/**
+ * The user list in at message
+ */
+@property(nonatomic, strong) NSMutableArray<NSString *> *atUserList;
+
+/**
+ * Message direction
+ * - Message direction affects UI styles such as bubble icons, bubble positions, etc.
+ */
+@property(nonatomic, assign) TMsgDirection direction;
+
+/**
+ * Message status
+ */
+@property(nonatomic, assign) TMsgStatus status;
+
+/**
+ * Message source
+ */
+@property(nonatomic, assign) TMsgSource source;
+
+/**
+ * IMSDK message
+ * The Message object provided by IM SDK. Contains various member functions for obtaining message information, including obtaining priority, obtaining element
+ * index, obtaining offline message configuration information, etc. For details, please refer to
+ * TXIMSDK__Plus_iOS\Frameworks\ImSDK_Plus.framework\Headers\V2TIMMessage.h
+ */
+@property(nonatomic, strong) V2TIMMessage *innerMessage;
+
+/**
+ *  Message unit layout
+ *  It includes UI information such as message margins, bubble padding, avatar margins, and avatar size.
+ *  For details, please refer to Section\Chat\CellLayout\TUIMessageCellLayout.h
+ */
+@property(nonatomic, strong) TUIMessageCellLayout *cellLayout;
+
+/**
+ * The flag of whether showing read receipts.
+ */
+@property(nonatomic, assign) BOOL showReadReceipt;
+
+/**
+ * The flag of  whether showing message time.
+ */
+@property(nonatomic, assign) BOOL showMessageTime;
+
+/**
+ * The flag of whether showing the button which indicated how many people modiffied.
+ */
+@property(nonatomic, assign) BOOL showMessageModifyReplies;
+/**
+ * Highlight keywords, when the keyword is not empty, it will be highlighted briefly, mainly used in message search scenarios.
+ */
+@property(nonatomic, copy) NSString *__nullable highlightKeyword;
+
+/**
+ * Message read receipt
+ */
+@property(nonatomic, strong) V2TIMMessageReceipt *messageReceipt;
+
+/**
+ * List of Reply Messages for the current message
+ */
+@property(nonatomic, strong) NSArray *messageModifyReplies;
+
+@property(nonatomic, assign) CGSize messageContainerAppendSize;
+
+/// Size for bottom container.
+@property(nonatomic, assign) CGSize bottomContainerSize;
+
+/// Placeholder data, to be replaced after data preparation is completed.
+@property(nonatomic, strong) TUIMessageCellData* _Nullable placeHolderCellData;
+
+/// Video transcoding progress
+@property(nonatomic, assign) CGFloat videoTranscodingProgress;
+
+/// If cell content can be forwarded.
+- (BOOL)canForward;
+
+- (BOOL)canLongPress;
+
+- (BOOL)shouldHide;
+
+/// Custom cell refresh when message modified
+- (BOOL)customReloadCellWithNewMsg:(V2TIMMessage *)newMessage;
+
+/**
+ *  Initialize the message unit according to the message direction (receive/sent)
+ *  - In addition to the initialization of basic messages, it also includes setting direction variables, nickname fonts, etc. according to the direction.
+ *  - Also provides inheritable behavior for subclasses.
+ */
+- (instancetype)initWithDirection:(TMsgDirection)direction NS_DESIGNATED_INITIALIZER;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+@property(nonatomic, assign) CGSize msgStatusSize;
+
+/**
+ * TUIChat supports batch retrieval of user information except for the message sender's nickname.
+ * You can override the requestForAdditionalUserInfo method in your custom TUIMessageCellData to return the user IDs which you want to retrieve, and directly use the additionalUserInfoResult property in your custom TUIMessageCell to render the UI as needed.
+ * After TUIChat retrieves the information, it will assign it to the additionalUserInfoResult property and asynchronously refresh your cell.
+ */
+- (NSArray<NSString *> *)requestForAdditionalUserInfo;
+@property(nonatomic, strong) NSDictionary<NSString *, TUIRelationUserModel *> *additionalUserInfoResult;
+
+@end
+
+NS_ASSUME_NONNULL_END
+
+/**
+ * 【Module name】TUIMessageCellDataFileUploadProtocol
+ * 【Function description】File type message, unified upload (send) progress field
+ */
+@protocol TUIMessageCellDataFileUploadProtocol <NSObject>
+
+@required
+/**
+ *  The progress of uploading (sending)
+ */
+@property(nonatomic, assign) NSUInteger uploadProgress;
+
+@end
+
+@protocol TUIMessageCellDataFileDownloadProtocol <NSObject>
+
+@required
+/**
+ *  The progress of downloading (receving)
+ */
+@property(nonatomic, assign) NSUInteger downladProgress;
+
+/**
+ *  The flag of whether is downloading
+ *  YES: downloading; NO: not download
+ */
+@property(nonatomic, assign) BOOL isDownloading;
+
+@end

+ 207 - 0
ThirdParty/TUIKit/TIMCommon/BaseCellData/TUIMessageCellData.m

@@ -0,0 +1,207 @@
+//
+//  TUIMessageCellData.m
+//  TXIMSDK_TUIKit_iOS
+//
+//  Created by annidyfeng on 2019/5/21.
+//  Copyright © 2023 Tencent. All rights reserved.
+//
+
+#import "TUIMessageCellData.h"
+#import <TIMCommon/TIMDefine.h>
+
+@interface TUIMessageCellData ()
+
+@end
+
+@implementation TUIMessageCellData
+{
+    NSString *_msgID;
+    NSString *_identifier;
+    NSURL *_avatarUrl;
+}
+
++ (TUIMessageCellData *)getCellData:(V2TIMMessage *)message {
+    return nil;
+}
+
++ (NSString *)getDisplayString:(V2TIMMessage *)message {
+    return nil;
+}
+
+- (Class)getReplyQuoteViewDataClass {
+    return nil;
+}
+
+- (Class)getReplyQuoteViewClass {
+    return nil;
+}
+
+- (instancetype)initWithDirection:(TMsgDirection)direction {
+    self = [super init];
+    if (self) {
+        _direction = direction;
+        _status = Msg_Status_Init;
+        _source = Msg_Source_Unkown;
+        _showReadReceipt = YES;
+        _sameToNextMsgSender = NO;
+        _showAvatar = YES;
+        _cellLayout = [self cellLayout:direction];
+        _additionalUserInfoResult = @{};
+    }
+    return self;
+}
+
+- (TUIMessageCellLayout *)cellLayout:(TMsgDirection)direction {
+    if (direction == MsgDirectionIncoming) {
+        return [TUIMessageCellLayout incommingMessageLayout];
+    } else {
+        return [TUIMessageCellLayout outgoingMessageLayout];
+    }
+}
+
+- (void)setMsgID:(NSString *)msgID {
+    _msgID = msgID;
+}
+
+- (NSString *)msgID {
+    if (_msgID) {
+        return _msgID;
+    }
+    if (self.innerMessage) {
+        return self.innerMessage.msgID;
+    }
+    return nil;
+}
+
+- (void)setIdentifier:(NSString *)identifier {
+    _identifier = identifier;
+}
+
+- (NSString *)identifier {
+    if (_identifier) {
+        return _identifier;
+    }
+    if (self.innerMessage) {
+        return self.innerMessage.sender;
+    }
+    return nil;
+}
+
+- (NSString *)senderName {
+    if (self.innerMessage) {
+        
+        if(self.innerMessage.localCustomData){
+            NSDictionary *dict = [self dictionaryFromData:self.innerMessage.localCustomData];
+            NSString *nickName = [TUIMessageCellData objectOrNilForKey:@"showName" fromDictionary:dict];
+            if(nickName.length > 0){
+                return nickName;
+            }
+        }
+        
+        return self.innerMessage.nameCard ? : (self.innerMessage.friendRemark ? : (self.innerMessage.nickName ? : self.innerMessage.sender));
+    }
+    return nil;
+}
+
+- (void)setAvatarUrl:(NSURL *)avatarUrl {
+    _avatarUrl = avatarUrl;
+}
+
+- (NSDictionary *)dictionaryFromData:(NSData *)data {
+    NSError *error = nil;
+    NSDictionary *dictionary = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error];
+    if (error) {
+        NSLog(@"Error converting NSData to NSDictionary: %@", error.localizedDescription);
+        return nil;
+    }
+    return dictionary;
+}
+
++ (id)objectOrNilForKey:(id)aKey fromDictionary:(NSDictionary *)dict
+{
+    id object = [dict objectForKey:aKey];
+    return [object isEqual:[NSNull null]] ? nil : object;
+}
+
+- (NSURL *)avatarUrl {
+    if (_avatarUrl) {
+        return _avatarUrl;
+    }
+    if (self.innerMessage) {
+        
+        if(self.innerMessage.localCustomData){
+            NSDictionary *dict = [self dictionaryFromData:self.innerMessage.localCustomData];
+            NSString *avatarStr = [TUIMessageCellData objectOrNilForKey:@"avatarURL" fromDictionary:dict];
+            if(avatarStr.length > 0){
+                return [NSURL URLWithString:avatarStr];
+            }
+        }
+        
+        return [NSURL URLWithString:self.innerMessage.faceURL];;
+    }
+    return nil;
+}
+
+- (NSInteger)headgearType{
+    if (self.innerMessage){
+        if(self.innerMessage.localCustomData){
+            NSDictionary *dict = [self dictionaryFromData:self.innerMessage.localCustomData];
+            NSInteger type = [[TUIMessageCellData objectOrNilForKey:@"headgearType" fromDictionary:dict] integerValue];
+            return type;
+        }
+    }
+    return 0;
+}
+
+- (NSString *)headdress{
+    if (self.innerMessage){
+        if(self.innerMessage.localCustomData){
+            NSDictionary *dict = [self dictionaryFromData:self.innerMessage.localCustomData];
+            NSString *headdressStr = [TUIMessageCellData objectOrNilForKey:@"headdress" fromDictionary:dict];
+            return headdressStr;
+        }
+    }
+    return @"";
+}
+
+- (BOOL)canForward {
+    return YES;
+}
+
+- (BOOL)canLongPress {
+    return YES;
+}
+
+- (BOOL)shouldHide {
+    return NO;
+}
+
+- (BOOL)customReloadCellWithNewMsg:(V2TIMMessage *)newMessage {
+    return NO;
+}
+
+- (CGSize)msgStatusSize {
+    if (self.showReadReceipt && self.innerMessage.needReadReceipt &&
+        (self.innerMessage.userID || self.innerMessage.groupID)) {
+        if (self.direction == MsgDirectionOutgoing) {
+            return CGSizeMake(54, 14);
+        } else {
+            return CGSizeMake(38, 14);
+        }
+    }
+    else {
+        //The community type does not require read receipt markers, only the time is needed.
+        return CGSizeMake(26, 14);
+    }
+
+}
+
+- (NSDictionary *)messageModifyUserInfos {
+  return self.additionalUserInfoResult;
+}
+
+- (NSArray<NSString *> *)requestForAdditionalUserInfo {
+  return @[];
+}
+
+@end

+ 110 - 0
ThirdParty/TUIKit/TIMCommon/BaseCellData/TUIMessageCellLayout.h

@@ -0,0 +1,110 @@
+
+//  Created by Tencent on 2023/06/09.
+//  Copyright © 2023 Tencent. All rights reserved.
+
+#import <Foundation/Foundation.h>
+@import UIKit;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ *【Module Name】TUIMessageCellLayout
+ *【Function description】The layout of message unit
+ * - UI layouts for implementing various message units (text, voice, video, images, emoticons, etc.).
+ * - When you want to adjust the interface layout in TUIKit, you can modify the corresponding properties in this layout.
+ */
+@interface TUIMessageCellLayout : NSObject
+
+/**
+ * The insets of message
+ */
+@property(nonatomic, assign) UIEdgeInsets messageInsets;
+
+/**
+ * The insets of bubble content.
+ */
+@property(nonatomic, assign) UIEdgeInsets bubbleInsets;
+
+/**
+ * The insets of avatar
+ */
+@property(nonatomic, assign) UIEdgeInsets avatarInsets;
+
+/**
+ * The size of avatar
+ */
+@property(nonatomic, assign) CGSize avatarSize;
+
+/////////////////////////////////////////////////////////////////////////////////
+//                      Text Message Layout
+/////////////////////////////////////////////////////////////////////////////////
+
+/**
+ *  Getting text message (receive) layout
+ */
++ (TUIMessageCellLayout *)incommingTextMessageLayout;
+
+/**
+ *  Getting text message (send) layout
+ */
++ (TUIMessageCellLayout *)outgoingTextMessageLayout;
+
+/////////////////////////////////////////////////////////////////////////////////
+//                      Voice Message Layout
+/////////////////////////////////////////////////////////////////////////////////
+/**
+ *  Getting voice message (receive) layout
+ */
++ (TUIMessageCellLayout *)incommingVoiceMessageLayout;
+
+/**
+ *  Getting voice message (send) layout
+ */
++ (TUIMessageCellLayout *)outgoingVoiceMessageLayout;
+
+/////////////////////////////////////////////////////////////////////////////////
+//                      System Message Layout
+/////////////////////////////////////////////////////////////////////////////////
+/**
+ *  Getting system message layout
+ */
++ (TUIMessageCellLayout *)systemMessageLayout;
+
+/////////////////////////////////////////////////////////////////////////////////
+//                      Image Message Layout
+/////////////////////////////////////////////////////////////////////////////////
+
+/**
+ *  Getting Image message layout
+ */
++ (TUIMessageCellLayout *)incommingImageMessageLayout;
++ (TUIMessageCellLayout *)outgoingImageMessageLayout;
+
+/////////////////////////////////////////////////////////////////////////////////
+//                      Video Message Layout
+/////////////////////////////////////////////////////////////////////////////////
+
+/**
+ *  Getting video message layout
+ */
++ (TUIMessageCellLayout *)incommingVideoMessageLayout;
++ (TUIMessageCellLayout *)outgoingVideoMessageLayout;
+
+
+
+/////////////////////////////////////////////////////////////////////////////////
+//                     Other Message Layout
+/////////////////////////////////////////////////////////////////////////////////
+/**
+ *  Getting receive message layout
+ */
++ (TUIMessageCellLayout *)incommingMessageLayout;
+
+/**
+ *  Getting send message layout
+ */
++ (TUIMessageCellLayout *)outgoingMessageLayout;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 160 - 0
ThirdParty/TUIKit/TIMCommon/BaseCellData/TUIMessageCellLayout.m

@@ -0,0 +1,160 @@
+//
+//  TUIMessageCellLayout.m
+//  TXIMSDK_TUIKit_iOS
+//
+//  Created by annidyfeng on 2019/5/21.
+//  Copyright © 2023 Tencent. All rights reserved.
+//
+
+#import "TUIMessageCellLayout.h"
+#import <TIMCommon/TIMDefine.h>
+
+@implementation TUIMessageCellLayout
+
+- (instancetype)init:(BOOL)isIncomming {
+    self = [super init];
+    if (self) {
+        self.avatarSize = CGSizeMake(40, 40);
+        if (isIncomming) {
+            self.avatarInsets = (UIEdgeInsets){
+                .left = 8,
+                .top = 3,
+                .bottom = 1,
+            };
+            self.messageInsets = (UIEdgeInsets){
+                .top = 3,
+                .bottom = 17,
+                .left = 8,
+            };
+        } else {
+            self.avatarInsets = (UIEdgeInsets){
+                .right = 8,
+                .top = 3,
+                .bottom = 1,
+            };
+            self.messageInsets = (UIEdgeInsets){
+                .top = 3,
+                .bottom = 17,
+                .right = 8,
+            };
+        }
+    }
+    return self;
+}
+
+static TUIMessageCellLayout *gIncommingMessageLayout;
+
++ (TUIMessageCellLayout *)incommingMessageLayout {
+    if (!gIncommingMessageLayout) {
+        gIncommingMessageLayout = [[TUIMessageCellLayout alloc] init:YES];
+    }
+    return gIncommingMessageLayout;
+}
+
+static TUIMessageCellLayout *gOutgoingMessageLayout;
+
++ (TUIMessageCellLayout *)outgoingMessageLayout {
+    if (!gOutgoingMessageLayout) {
+        gOutgoingMessageLayout = [[TUIMessageCellLayout alloc] init:NO];
+    }
+    return gOutgoingMessageLayout;
+}
+
+#pragma Text CellLayout
+
+static TUIMessageCellLayout *gIncommingTextMessageLayout;
+
++ (TUIMessageCellLayout *)incommingTextMessageLayout {
+    if (!gIncommingTextMessageLayout) {
+        gIncommingTextMessageLayout = [[TUIMessageCellLayout alloc] init:YES];
+        gIncommingTextMessageLayout.bubbleInsets = (UIEdgeInsets){.top = 10.5, .bottom = 10.5, .left = 16, .right = 16};
+    }
+    return gIncommingTextMessageLayout;
+}
+
+static TUIMessageCellLayout *gOutgingTextMessageLayout;
+
++ (TUIMessageCellLayout *)outgoingTextMessageLayout {
+    if (!gOutgingTextMessageLayout) {
+        gOutgingTextMessageLayout = [[TUIMessageCellLayout alloc] init:NO];
+        gOutgingTextMessageLayout.bubbleInsets = (UIEdgeInsets){.top = 10.5, .bottom = 10.5, .left = 16, .right = 16};
+    }
+    return gOutgingTextMessageLayout;
+}
+
+#pragma Voice CellLayout
+
+static TUIMessageCellLayout *gIncommingVoiceMessageLayout;
+
++ (TUIMessageCellLayout *)incommingVoiceMessageLayout {
+    if (!gIncommingVoiceMessageLayout) {
+        gIncommingVoiceMessageLayout = [[TUIMessageCellLayout alloc] init:YES];
+        gIncommingVoiceMessageLayout.bubbleInsets = (UIEdgeInsets){.top = 12, .bottom = 12, .left = 16, .right = 16};
+    }
+    return gIncommingVoiceMessageLayout;
+}
+
+static TUIMessageCellLayout *gOutgingVoiceMessageLayout;
+
++ (TUIMessageCellLayout *)outgoingVoiceMessageLayout {
+    if (!gOutgingVoiceMessageLayout) {
+        gOutgingVoiceMessageLayout = [[TUIMessageCellLayout alloc] init:NO];
+        gOutgingVoiceMessageLayout.bubbleInsets = (UIEdgeInsets){.top = 14, .bottom = 20, .left = 22, .right = 20};
+    }
+    return gOutgingVoiceMessageLayout;
+}
+
+#pragma System CellLayout
+
+static TUIMessageCellLayout *gSystemMessageLayout;
+
++ (TUIMessageCellLayout *)systemMessageLayout {
+    if (!gSystemMessageLayout) {
+        gSystemMessageLayout = [[TUIMessageCellLayout alloc] init:YES];
+        gSystemMessageLayout.messageInsets = (UIEdgeInsets){.top = 5, .bottom = 5};
+    }
+    return gSystemMessageLayout;
+}
+
+#pragma Image CellLayout
+
+static TUIMessageCellLayout *gIncommingImageMessageLayout;
++ (TUIMessageCellLayout *)incommingImageMessageLayout {
+    if (!gIncommingImageMessageLayout) {
+        gIncommingImageMessageLayout = [[TUIMessageCellLayout alloc] init:YES];
+        gIncommingImageMessageLayout.bubbleInsets = (UIEdgeInsets){.top = 0, .bottom = 0, .left = 0 ,.right = 0};
+    }
+    return gIncommingImageMessageLayout;
+}
+
+static TUIMessageCellLayout *gOutgoingImageMessageLayout;
++ (TUIMessageCellLayout *)outgoingImageMessageLayout {
+    if (!gOutgoingImageMessageLayout) {
+        gOutgoingImageMessageLayout = [[TUIMessageCellLayout alloc] init:NO];
+        gOutgoingImageMessageLayout.bubbleInsets = (UIEdgeInsets){.top = 0, .bottom = 0, .left = 0 ,.right = 0};
+    }
+    return gOutgoingImageMessageLayout;
+}
+
+#pragma Video CellLayout
+static TUIMessageCellLayout *gIncommingVideoMessageLayout;
+
++ (TUIMessageCellLayout *)incommingVideoMessageLayout {
+    if (!gIncommingVideoMessageLayout) {
+        gIncommingVideoMessageLayout = [[TUIMessageCellLayout alloc] init:YES];
+        gIncommingVideoMessageLayout.bubbleInsets = (UIEdgeInsets){.top = 0, .bottom = 0, .left = 0 ,.right = 0};
+    }
+    return gIncommingVideoMessageLayout;
+}
+
+static TUIMessageCellLayout *gOutgoingVideoMessageLayout;
+
++ (TUIMessageCellLayout *)outgoingVideoMessageLayout {
+    if (!gOutgoingVideoMessageLayout) {
+        gOutgoingVideoMessageLayout = [[TUIMessageCellLayout alloc] init:NO];
+        gOutgoingVideoMessageLayout.bubbleInsets = (UIEdgeInsets){.top = 0, .bottom = 0, .left = 0 ,.right = 0};
+    }
+    return gOutgoingVideoMessageLayout;
+}
+
+@end

+ 24 - 0
ThirdParty/TUIKit/TIMCommon/BaseCellData/TUIRelationUserModel.h

@@ -0,0 +1,24 @@
+//
+//  TUIRelationUserModel.h
+//  TIMCommon
+//
+//  Created by wyl on 2023/12/5.
+//  Copyright © 2023 Tencent. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface TUIRelationUserModel : NSObject
+@property(nonatomic, copy) NSString *userID;
+@property(nonatomic, copy) NSString *nickName;
+@property(nonatomic, copy) NSString *faceURL;
+@property(nonatomic, copy) NSString *friendRemark;
+@property(nonatomic, copy) NSString *nameCard;
+
+- (NSString *)getDisplayName;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 26 - 0
ThirdParty/TUIKit/TIMCommon/BaseCellData/TUIRelationUserModel.m

@@ -0,0 +1,26 @@
+//
+//  TUIRelationUserModel.m
+//  TIMCommon
+//
+//  Created by wyl on 2023/12/5.
+//  Copyright © 2023 Tencent. All rights reserved.
+//
+
+#import "TUIRelationUserModel.h"
+#import <TIMCommon/TIMDefine.h>
+
+@implementation TUIRelationUserModel
+
+- (NSString *)getDisplayName {
+    if (IS_NOT_EMPTY_NSSTRING(self.nameCard)) {
+        return self.nameCard;
+    } else if (IS_NOT_EMPTY_NSSTRING(self.friendRemark)) {
+        return self.friendRemark;
+    } else if (IS_NOT_EMPTY_NSSTRING(self.nickName)) {
+        return self.nickName;
+    } else {
+        return self.userID;
+    }
+    return @"";
+}
+@end

+ 73 - 0
ThirdParty/TUIKit/TIMCommon/BaseCellData/TUISystemMessageCellData.h

@@ -0,0 +1,73 @@
+
+//  Created by Tencent on 2023/06/09.
+//  Copyright © 2023 Tencent. All rights reserved.
+/**
+ * This file declares the TUISystemMessageCellData class.
+ * This class inherits from TUIMessageCellData and is used to store a series of data and information required by the system message unit.
+ */
+#import "TUIMessageCellData.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+typedef NS_ENUM(NSInteger, TUISystemMessageType) {
+    TUISystemMessageTypeUnknown = 0,
+    TUISystemMessageTypeDate = 1,
+};
+
+/**
+ * 【Module name】TUISystemMessageCellData
+ * 【Function description】The datasource of system message unit.
+ */
+@interface TUISystemMessageCellData : TUIMessageCellData
+
+/**
+ *  The content of system message, such as "You recalled a message.".
+ */
+@property(nonatomic, strong) NSString *content;
+
+/**
+ *  The flag of whether supporting re-edit.
+ */
+@property(nonatomic, assign) BOOL supportReEdit;
+
+/**
+ *  Mutable string
+ *  The recalled message can be re-edited within 2 minutes, which is displayed here based on attributedString.
+ */
+@property(nonatomic, strong, nullable) NSMutableAttributedString *attributedString;
+
+/**
+ *  The font of label which displays the system message content.
+ */
+@property(nonatomic, strong, nullable) UIFont *contentFont;
+
+/**
+ *  The color of label which displays the system message content.
+ */
+@property(nonatomic, strong, nullable) UIColor *contentColor;
+
+/**
+ * The type of system message type, default is TUISystemMessageTypeUnknown
+ */
+@property(nonatomic, assign) TUISystemMessageType type;
+
+@property(nonatomic, strong) NSArray<NSString *> *replacedUserIDList;
+
+/**
+ *  The font of label which displays the system message content.
+ */
+@property(nonatomic, class) UIFont *textFont;
+
+/**
+ *  The color of label which displays the system message content.
+ */
+@property(nonatomic, class) UIColor *textColor;
+
+/**
+ *  The background color of label which displays the system message content.
+ */
+@property(nonatomic, class) UIColor *textBackgroundColor;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 97 - 0
ThirdParty/TUIKit/TIMCommon/BaseCellData/TUISystemMessageCellData.m

@@ -0,0 +1,97 @@
+//
+//  TUISystemMessageCellData.m
+//  TXIMSDK_TUIKit_iOS
+//
+//  Created by annidyfeng on 2019/5/21.
+//  Copyright © 2023 Tencent. All rights reserved.
+//
+
+#import "TUISystemMessageCellData.h"
+#import <TIMCommon/TIMDefine.h>
+#import "TUIRelationUserModel.h"
+
+
+@implementation TUISystemMessageCellData
+
+- (instancetype)initWithDirection:(TMsgDirection)direction {
+    self = [super initWithDirection:direction];
+    if (self) {
+        self.showAvatar = NO;
+        _contentFont = [UIFont systemFontOfSize:13];
+        _contentColor = [UIColor d_systemGrayColor];
+        self.cellLayout = [TUIMessageCellLayout systemMessageLayout];
+    }
+    return self;
+}
+
+- (NSMutableAttributedString *)attributedString {
+    __block BOOL forceRefresh = NO;
+    [self.additionalUserInfoResult enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, TUIRelationUserModel * _Nonnull obj, BOOL * _Nonnull stop) {
+      NSString *str = [NSString stringWithFormat:@"{%@}", key];
+      NSString *showName = obj.userID;
+      if (obj.nameCard.length > 0) {
+          showName = obj.nameCard;
+      } else if (obj.friendRemark.length > 0) {
+          showName = obj.friendRemark;
+      } else if (obj.nickName.length > 0) {
+          showName = obj.nickName;
+      }
+      if ([self.content containsString:str]) {
+        self.content = [self.content stringByReplacingOccurrencesOfString:str withString:showName];
+        forceRefresh = YES;
+      }
+    }];
+  
+    if (forceRefresh || (_attributedString == nil && self.content.length > 0)) {
+        NSMutableAttributedString *attributeString = [[NSMutableAttributedString alloc] initWithString:self.content];
+        NSDictionary *attributeDict = @{NSForegroundColorAttributeName : [UIColor d_systemGrayColor]};
+        [attributeString setAttributes:attributeDict range:NSMakeRange(0, attributeString.length)];
+        if (self.supportReEdit) {
+            NSString *reEditStr = TIMCommonLocalizableString(TUIKitMessageTipsReEditMessage);
+            [attributeString appendAttributedString:[[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@" %@", reEditStr]]];
+            NSDictionary *attributeDict = @{NSForegroundColorAttributeName : [UIColor d_systemBlueColor]};
+            [attributeString setAttributes:attributeDict range:NSMakeRange(self.content.length + 1, reEditStr.length)];
+            [attributeString addAttribute:NSUnderlineStyleAttributeName
+                                    value:[NSNumber numberWithInteger:NSUnderlineStyleNone]
+                                    range:NSMakeRange(self.content.length + 1, reEditStr.length)];
+        }
+        _attributedString = attributeString;
+    }
+
+    return _attributedString;
+}
+
+- (NSArray<NSString *> *)requestForAdditionalUserInfo {
+  NSMutableArray *result = [NSMutableArray arrayWithArray:[super requestForAdditionalUserInfo]];
+  
+  if (self.replacedUserIDList) {
+    [result addObjectsFromArray:self.replacedUserIDList];
+  }
+  return result;
+}
+
+static UIFont *gTextFont;
++ (void)setTextFont:(UIFont *)textFont {
+    gTextFont = textFont;
+}
++ (UIFont *)textFont {
+    return gTextFont;
+}
+
+static UIColor *gTextColor;
++ (void)setTextColor:(UIColor *)textColor {
+    gTextColor = textColor;
+}
++ (UIColor *)textColor {
+    return gTextColor;
+}
+
+static UIColor *gTextBackgroundColor;
++ (void)setTextBackgroundColor:(UIColor *)textBackgroundColor {
+    gTextBackgroundColor = textBackgroundColor;
+}
++ (UIColor *)textBackgroundColor {
+    return gTextBackgroundColor;
+}
+
+@end

+ 17 - 0
ThirdParty/TUIKit/TIMCommon/CommonModel/NSTimer+TUISafe.h

@@ -0,0 +1,17 @@
+//
+//  NSTimer+TUISafe.h
+//  TUICore
+//
+//  Created by wyl on 2022/7/5.
+//  Copyright © 2023 Tencent. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface NSTimer (TUISafe)
++ (NSTimer *)tui_scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
+@end
+
+NS_ASSUME_NONNULL_END

+ 21 - 0
ThirdParty/TUIKit/TIMCommon/CommonModel/NSTimer+TUISafe.m

@@ -0,0 +1,21 @@
+//
+//  NSTimer+TUISafe.m
+//  TUICore
+//
+//  Created by wyl on 2022/7/5.
+//  Copyright © 2023 Tencent. All rights reserved.
+//
+
+#import "NSTimer+TUISafe.h"
+
+@implementation NSTimer (TUISafe)
++ (NSTimer *)tui_scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block {
+    return [self scheduledTimerWithTimeInterval:interval target:self selector:@selector(tui_callBlock:) userInfo:[block copy] repeats:repeats];
+}
+
++ (void)tui_callBlock:(NSTimer *)timer {
+    void (^block)(NSTimer *timer) = timer.userInfo;
+    !block ?: block(timer);
+}
+
+@end

+ 27 - 0
ThirdParty/TUIKit/TIMCommon/CommonModel/TIMCommonMediator.h

@@ -0,0 +1,27 @@
+//
+//  TIMCommonMediator.h
+//  TUIEmojiPlugin
+//
+//  Created by cologne on 2023/11/14.
+//  Copyright © 2023 Tencent. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface TIMCommonMediator : NSObject
+
++ (instancetype)share;
+
+///  Protocol : Class
+/// Register Protocol : Class
+- (void)registerService:(Protocol *)service class:(Class)cls;
+
+///  Protocol  [Class new]
+/// get  [class new]  by Protocol 
+- (id)getObject:(Protocol *)service;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 41 - 0
ThirdParty/TUIKit/TIMCommon/CommonModel/TIMCommonMediator.m

@@ -0,0 +1,41 @@
+//
+//  TIMCommonMediator.m
+//  TUIEmojiPlugin
+//
+//  Created by cologne on 2023/11/14.
+//  Copyright © 2023 Tencent. All rights reserved.
+//
+
+#import "TIMCommonMediator.h"
+
+@interface TIMCommonMediator()
+@property (nonatomic, strong) NSMutableDictionary *map;
+@end
+
+@implementation TIMCommonMediator
+
++ (instancetype)share {
+    static TIMCommonMediator *mediator = nil;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        mediator = [TIMCommonMediator new];
+        mediator.map = [NSMutableDictionary new];
+    });
+    return mediator;
+}
+
+- (void)registerService:(Protocol *)service class:(Class)cls {
+    if (!service || !cls) return;
+    self.map[NSStringFromProtocol(service)] = cls;
+}
+
+- (id)getObject:(Protocol *)service {
+    if (!service) return nil;
+    Class cls = self.map[NSStringFromProtocol(service)];
+    id obj = [cls new];
+    if ([obj conformsToProtocol:service]) {
+        return obj;
+    }
+    return nil;
+}
+@end

+ 587 - 0
ThirdParty/TUIKit/TIMCommon/CommonModel/TIMCommonModel.h

@@ -0,0 +1,587 @@
+//
+//  TIMCommonModel.h
+//  TIMCommon
+//
+//  Created by cologne on 2023/3/14.
+//  Copyright © 2023 Tencent. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+#import <TUICore/TUICommonModel.h>
+#import "TIMDefine.h"
+
+NS_ASSUME_NONNULL_BEGIN
+////////////////////////////////////////////////////////////////////////////////
+//
+//                           TUIPopView
+//
+/////////////////////////////////////////////////////////////////////////////////
+@class TUIPopView;
+@protocol TUIPopViewDelegate <NSObject>
+- (void)popView:(TUIPopView *)popView didSelectRowAtIndex:(NSInteger)index;
+@end
+
+@interface TUIPopView : UIView
+@property(nonatomic, strong) UITableView *tableView;
+@property(nonatomic, assign) CGPoint arrowPoint;
+@property(nonatomic, weak) id<TUIPopViewDelegate> delegate;
+- (void)setData:(NSMutableArray *)data;
+- (void)showInWindow:(UIWindow *)window;
+@end
+
+@interface TUIPopCellData : NSObject
+@property(nonatomic, strong) UIImage *image;
+@property(nonatomic, strong) NSString *title;
+@end
+
+@interface TUIPopCell : UITableViewCell
+@property(nonatomic, strong) UIImageView *image;
+@property(nonatomic, strong) UILabel *title;
++ (CGFloat)getHeight;
+- (void)setData:(TUIPopCellData *)data;
+@end
+
+/////////////////////////////////////////////////////////////////////////////////
+//
+//                           TUIModifyView
+//
+/////////////////////////////////////////////////////////////////////////////////
+@class TUIModifyView;
+@protocol TUIModifyViewDelegate <NSObject>
+- (void)modifyView:(TUIModifyView *)modifyView didModiyContent:(NSString *)content;
+@end
+
+@interface TUIModifyViewData : NSObject
+@property(nonatomic, strong) NSString *title;
+@property(nonatomic, strong) NSString *content;
+@property(nonatomic, strong) NSString *desc;
+@property(nonatomic, assign) BOOL enableNull;
+@end
+
+@interface TUIModifyView : UIView
+@property(nonatomic, strong) UIView *container;
+@property(nonatomic, strong) UILabel *title;
+@property(nonatomic, strong) UITextField *content;
+@property(nonatomic, strong) UILabel *descLabel;
+@property(nonatomic, strong) UIButton *confirm;
+@property(nonatomic, strong) UIView *hLine;
+@property(nonatomic, weak) id<TUIModifyViewDelegate> delegate;
+- (void)setData:(TUIModifyViewData *)data;
+- (void)showInWindow:(UIWindow *)window;
+@end
+
+/////////////////////////////////////////////////////////////////////////////////
+//
+//                           TUINaviBarIndicatorView
+//
+/////////////////////////////////////////////////////////////////////////////////
+@interface TUINaviBarIndicatorView : UIView
+
+@property(nonatomic, strong) UIActivityIndicatorView *indicator;
+
+@property(nonatomic, strong) UILabel *label;
+
+@property(nonatomic, assign) CGFloat maxLabelLength;
+
+- (void)setTitle:(NSString *)title;
+
+- (void)startAnimating;
+
+- (void)stopAnimating;
+
+@end
+
+/////////////////////////////////////////////////////////////////////////////////
+//
+//                           TUICommonCell & data
+//
+/////////////////////////////////////////////////////////////////////////////////
+
+@interface TUICommonCellData : NSObject
+@property(strong) NSString *reuseId;
+@property(nonatomic, assign) SEL cselector;
+@property(nonatomic, strong) NSDictionary *ext;
+- (CGFloat)heightOfWidth:(CGFloat)width;
+- (CGFloat)estimatedHeight;
+@end
+
+@interface TUICommonTableViewCell : UITableViewCell
+
+@property(readonly) TUICommonCellData *data;
+@property UIColor *colorWhenTouched;
+@property BOOL changeColorWhenTouched;
+
+- (void)fillWithData:(TUICommonCellData *)data;
+
+@end
+
+/////////////////////////////////////////////////////////////////////////////////
+//
+//                           TUICommonTextCell & data
+//
+/////////////////////////////////////////////////////////////////////////////////
+@interface TUICommonTextCellData : TUICommonCellData
+
+@property NSString *key;
+@property NSString *value;
+@property BOOL showAccessory;
+@property UIColor *keyColor;
+@property UIColor *valueColor;
+@property BOOL enableMultiLineValue;
+
+@property(nonatomic, assign) UIEdgeInsets keyEdgeInsets;
+
+@end
+
+@interface TUICommonTextCell : TUICommonTableViewCell
+@property UILabel *keyLabel;
+@property UILabel *valueLabel;
+@property(readonly) TUICommonTextCellData *textData;
+
+- (void)fillWithData:(TUICommonTextCellData *)data;
+
+@end
+
+/////////////////////////////////////////////////////////////////////////////////
+//
+//                           TUICommonSwitchCell & data
+//
+/////////////////////////////////////////////////////////////////////////////////
+@interface TUICommonSwitchCellData : TUICommonCellData
+
+@property NSString *title;
+@property NSString *desc;
+@property(getter=isOn) BOOL on;
+@property CGFloat margin;
+@property SEL cswitchSelector;
+
+@property(nonatomic, assign) BOOL displaySeparatorLine;
+
+@property(nonatomic, assign) BOOL disableChecked;
+
+@end
+
+@interface TUICommonSwitchCell : TUICommonTableViewCell
+@property UILabel *titleLabel;  // main title label
+@property UILabel *descLabel;   // detail title label below the main title label, used for explaining details
+@property UISwitch *switcher;
+
+@property(readonly) TUICommonSwitchCellData *switchData;
+
+- (void)fillWithData:(TUICommonSwitchCellData *)data;
+
+@end
+
+/////////////////////////////////////////////////////////////////////////////////
+//
+//                           TUIButtonCell & data
+//
+/////////////////////////////////////////////////////////////////////////////////
+typedef enum : NSUInteger {
+    ButtonGreen,
+    ButtonWhite,
+    ButtonRedText,
+    ButtonBule,
+} TUIButtonStyle;
+
+@interface TUIButtonCellData : TUICommonCellData
+@property(nonatomic, strong) NSString *title;
+@property SEL cbuttonSelector;
+@property TUIButtonStyle style;
+@property(nonatomic, strong) UIColor *textColor;
+@property(nonatomic, assign) BOOL hideSeparatorLine;
+@end
+
+@interface TUIButtonCell : TUICommonTableViewCell
+@property(nonatomic, strong) UIButton *button;
+@property TUIButtonCellData *buttonData;
+
+- (void)fillWithData:(TUIButtonCellData *)data;
+@end
+
+/////////////////////////////////////////////////////////////////////////////////
+//
+//                             TUIGroupPendencyCell & data
+//
+/////////////////////////////////////////////////////////////////////////////////
+#define TUIGroupPendencyCellData_onPendencyChanged @"TUIGroupPendencyCellData_onPendencyChanged"
+
+@interface TUIGroupPendencyCellData : TUICommonCellData
+
+@property(nonatomic, strong) NSString *groupId;
+
+@property(nonatomic, strong) NSString *fromUser;
+
+@property(nonatomic, strong) NSString *toUser;
+
+@property(readonly) V2TIMGroupApplication *pendencyItem;
+
+@property NSURL *avatarUrl;
+
+@property NSString *title;
+
+/**
+ *  The joining group introduction of the requester. Such as "Xiao Ming applied to join the group".
+ */
+@property NSString *requestMsg;
+
+/**
+ *  Agree or Not
+ *  YES: Agree;  NO: Indicates that the current request was not granted, but does not mean that the request has been denied.
+ */
+@property BOOL isAccepted;
+
+/**
+ *
+ *  Refuse or Not
+ *  YES: Refuse; NO: Indicates that the current request is not denied, but does not mean that the request has been granted.
+ */
+@property BOOL isRejectd;
+@property SEL cbuttonSelector;
+
+- (instancetype)initWithPendency:(V2TIMGroupApplication *)args;
+
+typedef void (^TUIGroupPendencyCellDataSuccessCallback)(void);
+typedef void (^TUIGroupPendencyCellDataFailureCallback)(int code, NSString *msg);
+
+- (void)agreeWithSuccess:(TUIGroupPendencyCellDataSuccessCallback)success
+                 failure:(TUIGroupPendencyCellDataFailureCallback)failure;
+
+- (void)rejectWithSuccess:(TUIGroupPendencyCellDataSuccessCallback)success
+                  failure:(TUIGroupPendencyCellDataFailureCallback)failure;
+- (void)accept;
+- (void)reject;
+
+@end
+
+@interface TUIGroupPendencyCell : TUICommonTableViewCell
+
+@property UIImageView *avatarView;
+
+@property UILabel *titleLabel;
+
+@property UILabel *addWordingLabel;
+
+@property UIButton *agreeButton;
+
+@property TUIGroupPendencyCellData *pendencyData;
+
+- (void)fillWithData:(TUIGroupPendencyCellData *)pendencyData;
+
+@end
+
+/////////////////////////////////////////////////////////////////////////////////
+//
+//                             TUIFaceCell & data
+//
+/////////////////////////////////////////////////////////////////////////////////
+
+/**
+ * 【Module name】 TUIFaceCellData
+ * 【Function description]】The name and local storage path of the stored emoticon.
+ */
+@interface TUIFaceCellData : NSObject
+
+/**
+ * The name of emoticon
+ */
+@property(nonatomic, strong) NSString *name;
+
+/**
+ * The localized name of the emoticon (the attribute used for internationalization, if it is empty or the length is 0, the name is displayed by default)
+ */
+@property(nonatomic, copy) NSString *localizableName;
+
+/**
+ * The storage path of the emoticon cached locally.
+ */
+@property(nonatomic, strong) NSString *path;
+@end
+
+/**
+ * 【Module name】TUIFaceCell
+ * 【Function description】 Store the image of the emoticon, and initialize the Cell according to TUIFaceCellData.
+ *  In the emoticon view, TUIFaceCell is the unit displayed on the interface.
+ */
+@interface TUIFaceCell : UICollectionViewCell
+
+/**
+ *  The image view for displaying emoticon
+ */
+@property(nonatomic, strong) UIImageView *face;
+@property(nonatomic, strong) UIImage *staicImage;
+@property(nonatomic, strong) UIImage *gifImage;
+@property(nonatomic, copy) void(^longPressCallback)(UILongPressGestureRecognizer *recognizer);
+- (void)setData:(TUIFaceCellData *)data;
+@end
+
+/////////////////////////////////////////////////////////////////////////////////
+//
+//                          TUIFaceGroup
+//
+/////////////////////////////////////////////////////////////////////////////////
+
+/**
+ * 【Module name】 TUIFaceGroup
+ * 【Function description】 It is used to realize the grouping of emoticon, which is convenient for users to browse and select under different emoticon themes.
+ *  This class stores the index of each emoticon group, so that FaceView can locate each emoticon group.
+ *  At the same time, this class stores the path of all emoticon pictures in an emoticon group, and provides data such as the number of lines, the number of
+ * emoticons in each line, etc., to locate specific emoticons
+ */
+@interface TUIFaceGroup : NSObject
+
+/**
+ *  Index of emoticons group, begining with zero.
+ */
+@property(nonatomic, assign) int groupIndex;
+
+/**
+ *  The resource path of the entire expression group
+ */
+@property(nonatomic, strong) NSString *groupPath;
+
+/**
+ *  The number of lines of emoticons in the emoticon group
+ */
+@property(nonatomic, assign) int rowCount;
+
+/**
+ *  The number of emoticons contained in each line
+ */
+@property(nonatomic, assign) int itemCountPerRow;
+
+@property(nonatomic, strong) NSMutableArray *faces;
+
+@property(nonatomic, strong) NSDictionary *facesMap;
+
+/**
+ *  The flag of indicating whether to display the delete button
+ *  When set to YES, FaceView will display a "delete" icon in the lower right corner of the emoticon view. Clicking the icon can delete the entered emoticon
+ * directly without evoking the keyboard.
+ */
+@property(nonatomic, assign) BOOL needBackDelete;
+
+/**
+ *  The path to the cover image of the emoticon group
+ */
+@property(nonatomic, strong) NSString *menuPath;
+
+@property(nonatomic, strong) TUIFaceGroup *recentGroup;
+
+@property(nonatomic, assign) BOOL isNeedAddInInputBar;
+
+@property(nonatomic, copy) NSString *groupName;
+
+@end
+
+@interface TUIEmojiTextAttachment : NSTextAttachment
+
+@property(nonatomic, strong) TUIFaceCellData *faceCellData;
+
+@property(nonatomic, copy) NSString *emojiTag;
+
+@property(nonatomic, assign) CGSize emojiSize;  // For emoji image size
+
+@end
+
+/////////////////////////////////////////////////////////////////////////////////
+//
+// TUIUnReadView
+//
+/////////////////////////////////////////////////////////////////////////////////
+@interface TUIUnReadView : UIView
+
+/**
+ * The label of displaying unread message count
+ */
+@property(nonatomic, strong) UILabel *unReadLabel;
+
+/**
+ * Set the unread message count
+ */
+- (void)setNum:(NSInteger)num;
+@end
+
+/////////////////////////////////////////////////////////////////////////////////
+//
+//                          TUIConversationPin
+//
+/////////////////////////////////////////////////////////////////////////////////
+extern NSString *kTopConversationListChangedNotification;
+
+@interface TUIConversationPin : NSObject
+
++ (instancetype)sharedInstance;
+
+/**
+ * 
+ * Getting the list of pinned conversations
+ */
+- (NSArray *)topConversationList;
+
+/**
+ * 
+ * Pin the conversation
+ */
+- (void)addTopConversation:(NSString *)conv callback:(void (^__nullable)(BOOL success, NSString *__nullable errorMessage))callback;
+/**
+ * 
+ * Remove pinned conversations
+ */
+- (void)removeTopConversation:(NSString *)conv callback:(void (^__nullable)(BOOL success, NSString *__nullable errorMessage))callback;
+@end
+
+/////////////////////////////////////////////////////////////////////////////////
+//
+//                          TUICommonContactSelectCellData
+//
+/////////////////////////////////////////////////////////////////////////////////
+@interface TUICommonContactSelectCellData : TUICommonCellData
+
+@property(nonatomic, strong) NSString *identifier;
+@property(nonatomic, strong) NSString *title;
+@property(nonatomic, strong) NSURL *avatarUrl;
+@property(nonatomic, strong) UIImage *avatarImage;
+
+@property(nonatomic, getter=isSelected) BOOL selected;
+@property(nonatomic, getter=isEnabled) BOOL enabled;
+
+@end
+
+/////////////////////////////////////////////////////////////////////////////////
+//
+//                          TUICommonContactListPickerCell
+//
+/////////////////////////////////////////////////////////////////////////////////
+@interface TUICommonContactListPickerCell : UICollectionViewCell
+
+@property UIImageView *avatar;
+
+@end
+
+/////////////////////////////////////////////////////////////////////////////////
+//
+//                          TUIContactListPickerOnCancel
+//
+/////////////////////////////////////////////////////////////////////////////////
+typedef void (^TUIContactListPickerOnCancel)(TUICommonContactSelectCellData *data);
+
+@interface TUIContactListPicker : UIControl
+
+@property(nonatomic, strong, readonly) UIButton *accessoryBtn;
+@property(nonatomic, strong) NSArray<TUICommonContactSelectCellData *> *selectArray;
+@property(nonatomic, copy) TUIContactListPickerOnCancel onCancel;
+
+@end
+
+/////////////////////////////////////////////////////////////////////////////////
+//
+//                             TUIProfileCardCell & vc
+//
+/////////////////////////////////////////////////////////////////////////////////
+@class TUIProfileCardCell;
+@protocol TUIProfileCardDelegate <NSObject>
+- (void)didTapOnAvatar:(TUIProfileCardCell *)cell;
+@end
+
+@interface TUIProfileCardCellData : TUICommonCellData
+@property(nonatomic, strong) UIImage *avatarImage;
+@property(nonatomic, strong) NSURL *avatarUrl;
+@property(nonatomic, strong) NSString *name;
+@property(nonatomic, strong) NSString *identifier;
+@property(nonatomic, strong) NSString *signature;
+@property(nonatomic, strong) UIImage *genderIconImage;
+@property(nonatomic, strong) NSString *genderString;
+@property BOOL showAccessory;
+@property BOOL showSignature;
+@end
+
+@interface TUIProfileCardCell : TUICommonTableViewCell
+@property(nonatomic, strong) UIImageView *avatar;
+@property(nonatomic, strong) UILabel *name;
+@property(nonatomic, strong) UILabel *identifier;
+@property(nonatomic, strong) UILabel *signature;
+@property(nonatomic, strong) UIImageView *genderIcon;
+@property(nonatomic, strong) TUIProfileCardCellData *cardData;
+@property(nonatomic, weak) id<TUIProfileCardDelegate> delegate;
+- (void)fillWithData:(TUIProfileCardCellData *)data;
+@end
+
+@interface TUIAvatarViewController : UIViewController
+
+@property(nonatomic, strong) TUIProfileCardCellData *avatarData;
+
+@end
+
+typedef NS_ENUM(NSUInteger, TUISelectAvatarType) {
+    TUISelectAvatarTypeUserAvatar,
+    TUISelectAvatarTypeGroupAvatar,
+    TUISelectAvatarTypeCover,
+    TUISelectAvatarTypeConversationBackGroundCover,
+};
+
+@interface TUISelectAvatarCardItem : NSObject
+@property(nonatomic, strong) NSString *posterUrlStr;
+@property(nonatomic, assign) BOOL isSelect;
+@property(nonatomic, copy) NSString *fullUrlStr;
+@property(nonatomic, assign) BOOL isDefaultBackgroundItem;
+@property(nonatomic, assign) BOOL isGroupGridAvatar;
+@property(nonatomic, copy) NSString *createGroupType;
+@property(nonatomic, strong) UIImage *cacheGroupGridAvatarImage;
+@end
+
+@interface TUISelectAvatarController : UIViewController
+@property(nonatomic, copy) void (^selectCallBack)(NSString *urlStr);
+@property(nonatomic, assign) TUISelectAvatarType selectAvatarType;
+@property(nonatomic, copy) NSString *profilFaceURL;
+@property(nonatomic, strong) UIImage *cacheGroupGridAvatarImage;
+@property(nonatomic, copy) NSString *createGroupType;
+@end
+
+/////////////////////////////////////////////////////////////////////////////////
+//
+//                             TUICommonAvatarCell & Data
+//
+/////////////////////////////////////////////////////////////////////////////////
+@interface TUICommonAvatarCellData : TUICommonCellData
+;
+@property(nonatomic, strong) NSString *key;
+@property(nonatomic, strong) NSString *value;
+@property BOOL showAccessory;
+@property(nonatomic, strong) UIImage *avatarImage;
+@property(nonatomic, strong) NSURL *avatarUrl;
+
+@end
+
+@interface TUICommonAvatarCell : TUICommonTableViewCell
+@property UILabel *keyLabel;
+@property UILabel *valueLabel;
+@property UIImageView *avatar;
+@property(readonly) TUICommonAvatarCellData *avatarData;
+
+- (void)fillWithData:(TUICommonAvatarCellData *)avatarData;
+
+@end
+
+/////////////////////////////////////////////////////////////////////////////////
+//
+//                             TUIConversationGroupItem
+//
+/////////////////////////////////////////////////////////////////////////////////
+extern NSUInteger kConversationMarkStarType;
+@interface TUIConversationGroupItem : NSObject
+@property(nonatomic, strong) NSString *groupName;
+@property(nonatomic, assign) NSInteger unreadCount;
+@property(nonatomic, assign) NSInteger groupIndex;
+@property(nonatomic, assign) BOOL isShow;
+@property(nonatomic, strong) UIButton *groupBtn;
+@end
+
+
+@interface TUISendMessageAppendParams : NSObject
+@property (nonatomic, assign) BOOL isSendPushInfo;
+@property (nonatomic, assign) BOOL isOnlineUserOnly;
+@property (nonatomic, assign) V2TIMMessagePriority priority;
++ (instancetype)defaultConfig;
+@end
+NS_ASSUME_NONNULL_END

+ 2437 - 0
ThirdParty/TUIKit/TIMCommon/CommonModel/TIMCommonModel.m

@@ -0,0 +1,2437 @@
+//
+//  TIMCommonModel.m
+//  TIMCommon
+//
+//  Created by cologne on 2023/3/14.
+//  Copyright © 2023 Tencent. All rights reserved.
+//
+
+#import "TIMCommonModel.h"
+
+/////////////////////////////////////////////////////////////////////////////////
+//
+//                           TUIPopView
+//
+/////////////////////////////////////////////////////////////////////////////////
+
+@interface TUIPopView () <UITableViewDelegate, UITableViewDataSource, UIGestureRecognizerDelegate>
+@property(nonatomic, strong) NSMutableArray *data;
+@end
+
+@implementation TUIPopView
+
+- (id)initWithFrame:(CGRect)frame {
+    self = [super initWithFrame:frame];
+    if (self) {
+        [self setupViews];
+    }
+    return self;
+}
+
+- (void)setData:(NSMutableArray *)data {
+    _data = data;
+    [_tableView reloadData];
+}
+
+- (void)showInWindow:(UIWindow *)window {
+    [window addSubview:self];
+    __weak typeof(self) ws = self;
+    self.alpha = 0;
+    [UIView animateWithDuration:0.25
+                          delay:0
+                        options:UIViewAnimationOptionCurveEaseOut
+                     animations:^{
+                       ws.alpha = 1;
+                     }
+                     completion:nil];
+}
+
+- (void)setupViews {
+    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTap:)];
+    tap.delegate = self;
+    [self addGestureRecognizer:tap];
+    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(onTap:)];
+    [self addGestureRecognizer:pan];
+
+    self.backgroundColor = [UIColor clearColor];
+    CGSize arrowSize = TUIPopView_Arrow_Size;
+    _tableView = [[UITableView alloc] initWithFrame:CGRectMake(self.frame.origin.x, self.frame.origin.y + arrowSize.height, self.frame.size.width,
+                                                               self.frame.size.height - arrowSize.height)];
+    self.frame = [UIScreen mainScreen].bounds;
+    _tableView.delegate = self;
+    _tableView.dataSource = self;
+    _tableView.backgroundColor = TUIDemoDynamicColor(@"pop_bg_color", @"#FFFFFF");
+    _tableView.tableFooterView = [[UIView alloc] init];
+    _tableView.scrollEnabled = NO;
+    _tableView.layer.cornerRadius = 5.0;
+    [self addSubview:_tableView];
+}
+
+- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
+    return _data.count;
+}
+
+- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
+    return [TUIPopCell getHeight];
+}
+
+- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
+    TUIPopCell *cell = [tableView dequeueReusableCellWithIdentifier:TUIPopCell_ReuseId];
+    if (!cell) {
+        cell = [[TUIPopCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:TUIPopCell_ReuseId];
+    }
+    [cell setData:_data[indexPath.row]];
+    if (indexPath.row == _data.count - 1) {
+        cell.separatorInset = UIEdgeInsetsMake(0, self.bounds.size.width, 0, 0);
+    }
+    return cell;
+}
+
+- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
+    [tableView deselectRowAtIndexPath:indexPath animated:NO];
+    if (_delegate && [_delegate respondsToSelector:@selector(popView:didSelectRowAtIndex:)]) {
+        [_delegate popView:self didSelectRowAtIndex:indexPath.row];
+    }
+    [self hide];
+}
+
+- (void)drawRect:(CGRect)rect {
+    [[UIColor whiteColor] set];
+
+    CGSize arrowSize = TUIPopView_Arrow_Size;
+    UIBezierPath *arrowPath = [[UIBezierPath alloc] init];
+    [arrowPath moveToPoint:_arrowPoint];
+    [arrowPath addLineToPoint:CGPointMake(_arrowPoint.x + arrowSize.width * 0.5, _arrowPoint.y + arrowSize.height)];
+    [arrowPath addLineToPoint:CGPointMake(_arrowPoint.x - arrowSize.width * 0.5, _arrowPoint.y + arrowSize.height)];
+    [arrowPath closePath];
+    [arrowPath fill];
+}
+
+- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
+    if ([NSStringFromClass([touch.view class]) isEqualToString:@"UITableViewCellContentView"]) {
+        return NO;
+    }
+    return YES;
+}
+
+- (void)onTap:(UIGestureRecognizer *)recognizer {
+    [self hide];
+}
+
+- (void)hide {
+    __weak typeof(self) ws = self;
+    self.alpha = 1;
+    [UIView animateWithDuration:0.25
+        delay:0
+        options:UIViewAnimationOptionCurveEaseOut
+        animations:^{
+          ws.alpha = 0;
+        }
+        completion:^(BOOL finished) {
+          if ([ws superview]) {
+              [ws removeFromSuperview];
+          }
+        }];
+}
+@end
+
+@implementation TUIPopCellData
+@end
+
+@implementation TUIPopCell
+
+- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
+    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
+    if (self) {
+        [self setupViews];
+    }
+    return self;
+}
+
+- (void)setupViews {
+    self.backgroundColor = [UIColor clearColor];
+
+    _image = [[UIImageView alloc] init];
+    _image.contentMode = UIViewContentModeScaleAspectFit;
+    [self addSubview:_image];
+
+    _title = [[UILabel alloc] init];
+    _title.font = [UIFont systemFontOfSize:15];
+    _title.textColor = TUIDemoDynamicColor(@"pop_text_color", @"#444444");
+    _title.numberOfLines = 0;
+    [self addSubview:_title];
+
+    [self setSeparatorInset:UIEdgeInsetsMake(0, TUIPopCell_Padding, 0, 0)];
+}
+
+- (void)layoutSubviews {
+    CGFloat headHeight = TUIPopCell_Height - 2 * TUIPopCell_Padding;
+    self.image.frame = CGRectMake(TUIPopCell_Padding, TUIPopCell_Padding, headHeight, headHeight);
+    self.image.center = CGPointMake(self.image.center.x, self.contentView.center.y);
+
+    CGFloat titleWidth = self.frame.size.width - 2 * TUIPopCell_Padding - TUIPopCell_Margin - _image.frame.size.width;
+    self.title.frame =
+        CGRectMake(_image.frame.origin.x + _image.frame.size.width + TUIPopCell_Margin, TUIPopCell_Padding, titleWidth, self.contentView.bounds.size.height);
+    self.title.center = CGPointMake(self.title.center.x, self.contentView.center.y);
+    
+    if (isRTL()) {
+        [self.image resetFrameToFitRTL];
+        [self.title resetFrameToFitRTL];
+    }
+}
+
+- (void)setData:(TUIPopCellData *)data {
+    _image.image = data.image;
+    _title.text = data.title;
+}
+
++ (CGFloat)getHeight {
+    return TUIPopCell_Height;
+}
+@end
+
+/////////////////////////////////////////////////////////////////////////////////
+//
+//                           TUIModifyView
+//
+/////////////////////////////////////////////////////////////////////////////////
+#define kContainerWidth Screen_Width
+#define kContainerHeight kContainerWidth * 3 / 4
+
+@implementation TUIModifyViewData
+- (instancetype)init {
+    if (self = [super init]) {
+        self.enableNull = NO;
+    }
+    return self;
+}
+@end
+
+@interface TUIModifyView () <UITextFieldDelegate, UIGestureRecognizerDelegate>
+@property(nonatomic, assign) BOOL keyboardShowing;
+@property(nonatomic, strong) TUIModifyViewData *data;
+@property(nonatomic, strong) UIButton *closeBtn;
+@end
+
+@implementation TUIModifyView
+- (id)init {
+    self = [super init];
+    if (self) {
+        [self setupViews];
+    }
+    return self;
+}
+
+- (void)setupViews {
+    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
+    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardDidHide:) name:UIKeyboardDidHideNotification object:nil];
+    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillChangeFrame:) name:UIKeyboardWillChangeFrameNotification object:nil];
+
+    self.frame = [UIScreen mainScreen].bounds;
+    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTap:)];
+    tap.delegate = self;
+    [self addGestureRecognizer:tap];
+
+    self.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.4];
+
+    _container = [[UIView alloc] initWithFrame:CGRectMake(0, Screen_Height, kContainerWidth, kContainerHeight)];
+    _container.backgroundColor = TUIContactDynamicColor(@"group_modify_container_view_bg_color", @"#FFFFFF");
+    _container.layer.cornerRadius = 8;
+    [_container.layer setMasksToBounds:YES];
+    [self addSubview:_container];
+
+    CGFloat buttonHeight = 46;
+    CGFloat titleHeight = 63;
+
+    _title = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, _container.frame.size.width, titleHeight)];
+    _title.font = [UIFont fontWithName:@"PingFangSC-Medium" size:17];
+    _title.textColor = TUIContactDynamicColor(@"group_modify_title_color", @"#000000");
+    _title.textAlignment = NSTextAlignmentCenter;
+    [_container addSubview:_title];
+
+    _hLine = [[UIView alloc] initWithFrame:CGRectMake(0, CGRectGetMaxY(_title.frame), kContainerWidth, TLine_Heigh)];
+    _hLine.backgroundColor = TIMCommonDynamicColor(@"separator_color", @"#E4E5E9");
+    [_container addSubview:_hLine];
+
+    CGFloat contentMargin = 20;
+    CGFloat contentWidth = _container.frame.size.width - 2 * contentMargin;
+    CGFloat contentY = CGRectGetMaxY(_hLine.frame) + 17;
+    CGFloat contentheight = 40;
+    _content = [[UITextField alloc] initWithFrame:CGRectMake(contentMargin, contentY, contentWidth, contentheight)];
+    _content.textAlignment = isRTL()?NSTextAlignmentRight:NSTextAlignmentLeft;
+    _content.delegate = self;
+    _content.backgroundColor = TUIContactDynamicColor(@"group_modify_input_bg_color", @"#F5F5F5");
+    _content.textColor = TUIContactDynamicColor(@"group_modify_input_text_color", @"#000000");
+    [_content setFont:[UIFont systemFontOfSize:16]];
+    [_content.layer setMasksToBounds:YES];
+    [_content.layer setCornerRadius:4.0f];
+    [_content setReturnKeyType:UIReturnKeyDone];
+    [_content addTarget:self action:@selector(textChanged) forControlEvents:UIControlEventEditingChanged];
+    CGRect leftviewFrame = _content.frame;
+    leftviewFrame.size.width = 16;
+    UIView *leftview = [[UIView alloc] initWithFrame:leftviewFrame];
+    _content.leftView = leftview;
+    _content.leftViewMode = UITextFieldViewModeAlways;
+    CGRect rightviewFrame = _content.frame;
+    rightviewFrame.size.width = 16;
+    rightviewFrame.origin.x = rightviewFrame.size.width - 16;
+    UIView *rightView = [[UIView alloc] initWithFrame:rightviewFrame];
+    _content.rightView = rightView;
+    _content.rightViewMode = UITextFieldViewModeAlways;
+
+    [_container addSubview:_content];
+
+    _descLabel = [[UILabel alloc] initWithFrame:CGRectMake(_content.frame.origin.x, CGRectGetMaxY(_content.frame) + 17, contentWidth, 20)];
+    _descLabel.textColor = TUIContactDynamicColor(@"group_modify_desc_color", @"#888888");
+    _descLabel.font = [UIFont systemFontOfSize:13.0];
+    _descLabel.numberOfLines = 0;
+    _descLabel.text = @"desc";
+    [_container addSubview:_descLabel];
+
+    _confirm = [[UIButton alloc] initWithFrame:CGRectMake(_content.frame.origin.x, CGRectGetMaxY(_descLabel.frame) + 30, contentWidth, buttonHeight)];
+    [_confirm setTitle:TIMCommonLocalizableString(Confirm) forState:UIControlStateNormal];
+    [_confirm setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
+    _confirm.titleLabel.font = [UIFont systemFontOfSize:15];
+    _confirm.layer.cornerRadius = 8;
+    _confirm.layer.masksToBounds = YES;
+    _confirm.imageView.contentMode = UIViewContentModeScaleToFill;
+    [self enableConfirmButton:self.data.enableNull];
+    [_confirm addTarget:self action:@selector(didConfirm:) forControlEvents:UIControlEventTouchUpInside];
+    [_container addSubview:_confirm];
+
+    _closeBtn = [[UIButton alloc] initWithFrame:CGRectMake(_container.frame.size.width - 24 - 20, 0, 24, 24)];
+    _closeBtn.mm__centerY(_title.mm_centerY);
+    [_closeBtn setImage:[UIImage imageNamed:TUIContactImagePath(@"ic_close_poppings")] forState:UIControlStateNormal];
+    [_closeBtn addTarget:self action:@selector(didCancel:) forControlEvents:UIControlEventTouchUpInside];
+    [_container addSubview:_closeBtn];
+}
+
+- (void)setData:(TUIModifyViewData *)data {
+    _title.text = data.title;
+    _content.text = data.content;
+    _descLabel.text = data.desc;
+    _data = data;
+
+    CGRect rect = [data.desc boundingRectWithSize:CGSizeMake(self.content.bounds.size.width, CGFLOAT_MAX)
+                                          options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
+                                       attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:13.0]}
+                                          context:nil];
+    CGRect frame = _descLabel.frame;
+    frame.size.height = rect.size.height;
+    _descLabel.frame = frame;
+
+    [self textChanged];
+}
+
+- (void)showInWindow:(UIWindow *)window {
+    [window addSubview:self];
+    [self layoutIfNeeded];
+    CGFloat height = CGRectGetMaxY(self.confirm.frame) + 50;
+
+    __weak typeof(self) ws = self;
+    [UIView animateWithDuration:0.25
+                          delay:0
+                        options:UIViewAnimationOptionCurveEaseOut
+                     animations:^{
+                       ws.container.frame = CGRectMake(0, Screen_Height - height, kContainerWidth, height);
+                     }
+                     completion:nil];
+}
+
+- (void)onTap:(UIGestureRecognizer *)recognizer {
+    [_content resignFirstResponder];
+
+    if (!self.keyboardShowing) {
+        [self hide];
+    }
+}
+
+- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
+    return [touch.view isEqual:self];
+}
+
+- (void)hide {
+    __weak typeof(self) ws = self;
+    self.alpha = 1;
+    [UIView animateWithDuration:0.25
+        delay:0
+        options:UIViewAnimationOptionCurveEaseOut
+        animations:^{
+          ws.alpha = 0;
+        }
+        completion:^(BOOL finished) {
+          [[NSNotificationCenter defaultCenter] removeObserver:ws];
+          if ([ws superview]) {
+              [ws removeFromSuperview];
+          }
+        }];
+}
+
+- (void)didCancel:(UIButton *)sender {
+    [self hide];
+}
+
+- (void)didConfirm:(UIButton *)sender {
+    if (_delegate && [_delegate respondsToSelector:@selector(modifyView:didModiyContent:)]) {
+        [_delegate modifyView:self didModiyContent:_content.text];
+    }
+    [self hide];
+}
+
+- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
+    if ([text isEqualToString:@"\n"]) {
+        [textView resignFirstResponder];
+        return NO;
+    }
+    return YES;
+}
+
+- (void)textChanged {
+    [self enableConfirmButton:(self.content.text.length || self.data.enableNull)];
+}
+
+- (void)keyboardWillChangeFrame:(NSNotification *)notification {
+    CGRect keyboardFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
+    self.keyboardShowing = keyboardFrame.size.height > 0;
+    [self animateContainer:keyboardFrame.size.height];
+}
+
+- (void)keyboardWillHide:(NSNotification *)notification {
+    [self animateContainer:0];
+}
+
+- (void)keyboardDidHide:(NSNotification *)notice {
+    self.keyboardShowing = NO;
+}
+
+- (void)animateContainer:(CGFloat)keyboardHeight {
+    CGFloat height = CGRectGetMaxY(self.confirm.frame) + 50;
+    CGRect frame = _container.frame;
+    frame.origin.y = Screen_Height - height - keyboardHeight;  //(self.frame.size.height - keyboardHeight - frame.size.height) * 0.5;
+    __weak typeof(self) ws = self;
+    [UIView animateWithDuration:0.3
+                          delay:0
+                        options:UIViewAnimationOptionCurveEaseOut
+                     animations:^{
+                       ws.container.frame = frame;
+                     }
+                     completion:nil];
+}
+
+- (void)enableConfirmButton:(BOOL)enable {
+    if (enable) {
+        _confirm.backgroundColor = TUIContactDynamicColor(@"group_modify_confirm_enable_bg_color", @"147AFF");
+        _confirm.enabled = YES;
+    } else {
+        _confirm.backgroundColor = [TUIContactDynamicColor(@"group_modify_confirm_enable_bg_color", @"147AFF") colorWithAlphaComponent:0.3];
+        _confirm.enabled = NO;
+    }
+}
+
+@end
+
+/////////////////////////////////////////////////////////////////////////////////
+//
+//                           TUINaviBarIndicatorView
+//
+/////////////////////////////////////////////////////////////////////////////////
+@implementation TUINaviBarIndicatorView
+- (id)init {
+    self = [super init];
+    if (self) {
+        [self setupViews];
+    }
+    return self;
+}
+
+- (void)setupViews {
+    _indicator = [[UIActivityIndicatorView alloc] initWithFrame:CGRectMake(0, 0, 20, 20)];
+    _indicator.center = CGPointMake(0, NavBar_Height * 0.5);
+    _indicator.activityIndicatorViewStyle = UIActivityIndicatorViewStyleGray;
+    [self addSubview:_indicator];
+
+    _label = [[UILabel alloc] init];
+    _label.backgroundColor = [UIColor clearColor];
+    _label.font = [UIFont boldSystemFontOfSize:17];
+    _label.textColor = TIMCommonDynamicColor(@"nav_title_text_color", @"#000000");
+    [self addSubview:_label];
+    _maxLabelLength = 150;
+}
+
+- (void)setTitle:(NSString *)title {
+    _label.textColor = TIMCommonDynamicColor(@"nav_title_text_color", @"#000000");
+    _label.text = title;
+    [self updateLayout];
+}
+
+- (void)updateLayout {
+    [_label sizeToFit];
+    CGSize labelSize = _label.bounds.size;  // [_label sizeThatFits:CGSizeMake(Screen_Width, NavBar_Height)];
+    CGFloat labelWidth = MIN(labelSize.width, _maxLabelLength);
+    CGFloat labelY = 0;
+    CGFloat labelX = _indicator.hidden ? 0 : (_indicator.frame.origin.x + _indicator.frame.size.width + TUINaviBarIndicatorView_Margin);
+    _label.frame = CGRectMake(labelX, labelY, labelWidth, NavBar_Height);
+    self.frame = CGRectMake(0, 0, labelX + labelWidth + TUINaviBarIndicatorView_Margin, NavBar_Height);
+    //    self.center = CGPointMake(Screen_Width * 0.5, NavBar_Height * 0.5);
+}
+
+- (void)startAnimating {
+    [_indicator startAnimating];
+}
+
+- (void)stopAnimating {
+    [_indicator stopAnimating];
+}
+@end
+
+/////////////////////////////////////////////////////////////////////////////////
+//
+//                             TUICommonCell & data
+//
+/////////////////////////////////////////////////////////////////////////////////
+@implementation TUICommonCellData
+
+- (CGFloat)heightOfWidth:(CGFloat)width {
+    return 60;
+}
+
+- (CGFloat)estimatedHeight {
+    return 60;
+}
+
+@end
+
+@interface TUICommonTableViewCell () <UIGestureRecognizerDelegate>
+@property TUICommonCellData *data;
+@property UITapGestureRecognizer *tapRecognizer;
+@end
+
+@implementation TUICommonTableViewCell
+
+- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
+    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
+    if (self) {
+        _tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapGesture:)];
+        _tapRecognizer.delegate = self;
+        _tapRecognizer.cancelsTouchesInView = NO;
+
+        self.backgroundColor = TIMCommonDynamicColor(@"form_bg_color", @"#FFFFFF");
+        self.contentView.backgroundColor = TIMCommonDynamicColor(@"form_bg_color", @"#FFFFFF");
+    }
+    return self;
+}
+
+- (void)tapGesture:(UIGestureRecognizer *)gesture {
+    if (self.data.cselector) {
+        UIViewController *vc = self.mm_viewController;
+        if ([vc respondsToSelector:self.data.cselector]) {
+            self.selected = YES;
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+            [vc performSelector:self.data.cselector withObject:self];
+#pragma clang diagnostic pop
+        }
+    }
+}
+
+- (void)fillWithData:(TUICommonCellData *)data {
+    self.data = data;
+    if (data.cselector) {
+        [self addGestureRecognizer:self.tapRecognizer];
+    } else {
+        [self removeGestureRecognizer:self.tapRecognizer];
+    }
+}
+
+@end
+
+/////////////////////////////////////////////////////////////////////////////////
+//
+//                             TUICommonTextCell & data
+//
+/////////////////////////////////////////////////////////////////////////////////
+@implementation TUICommonTextCellData
+- (instancetype)init {
+    self = [super init];
+    self.keyEdgeInsets = UIEdgeInsetsMake(0, 20, 0, 0);
+    return self;
+}
+
+- (CGFloat)heightOfWidth:(CGFloat)width {
+    CGFloat height = [super heightOfWidth:width];
+    if (self.enableMultiLineValue) {
+        NSString *str = self.value;
+        NSDictionary *attribute = @{NSFontAttributeName : [UIFont systemFontOfSize:16]};
+        CGSize size = [str boundingRectWithSize:CGSizeMake(280, 999)
+                                        options:NSStringDrawingTruncatesLastVisibleLine | NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
+                                     attributes:attribute
+                                        context:nil]
+                          .size;
+        height = size.height + 30;
+    }
+    return height;
+}
+
+@end
+
+@interface TUICommonTextCell ()
+@property TUICommonTextCellData *textData;
+@end
+
+@implementation TUICommonTextCell
+
+- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
+    if (self = [super initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:reuseIdentifier]) {
+        self.backgroundColor = TIMCommonDynamicColor(@"form_bg_color", @"#FFFFFF");
+        self.contentView.backgroundColor = TIMCommonDynamicColor(@"form_bg_color", @"#FFFFFF");
+
+        _keyLabel = [[UILabel alloc] init];
+        _keyLabel.textColor = TIMCommonDynamicColor(@"form_key_text_color", @"#444444");
+        _keyLabel.font = [UIFont systemFontOfSize:16.0];
+        [self.contentView addSubview:_keyLabel];
+        [_keyLabel setRtlAlignment:TUITextRTLAlignmentTrailing];
+        
+        _valueLabel = [[UILabel alloc] init];
+        [self.contentView addSubview:_valueLabel];
+        _valueLabel.textColor = TIMCommonDynamicColor(@"form_value_text_color", @"#000000");
+        _valueLabel.font = [UIFont systemFontOfSize:16.0];
+        [_valueLabel setRtlAlignment:TUITextRTLAlignmentTrailing];
+        self.selectionStyle = UITableViewCellSelectionStyleNone;
+    }
+    return self;
+}
+
+- (void)fillWithData:(TUICommonTextCellData *)textData {
+    [super fillWithData:textData];
+
+    self.textData = textData;
+    RAC(_keyLabel, text) = [RACObserve(textData, key) takeUntil:self.rac_prepareForReuseSignal];
+    RAC(_valueLabel, text) = [RACObserve(textData, value) takeUntil:self.rac_prepareForReuseSignal];
+
+    if (textData.showAccessory) {
+        self.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
+    } else {
+        self.accessoryType = UITableViewCellAccessoryNone;
+    }
+
+    if (self.textData.keyColor) {
+        self.keyLabel.textColor = self.textData.keyColor;
+    }
+
+    if (self.textData.valueColor) {
+        self.valueLabel.textColor = self.textData.valueColor;
+    }
+
+    if (self.textData.enableMultiLineValue) {
+        self.valueLabel.numberOfLines = 0;
+    } else {
+        self.valueLabel.numberOfLines = 1;
+    }
+    // tell constraints they need updating
+    [self setNeedsUpdateConstraints];
+
+    // update constraints now so we can animate the change
+    [self updateConstraintsIfNeeded];
+
+    [self layoutIfNeeded];
+}
+
++ (BOOL)requiresConstraintBasedLayout {
+    return YES;
+}
+
+// this is Apple's recommended place for adding/updating constraints
+- (void)updateConstraints {
+    [super updateConstraints];
+
+    [self.keyLabel sizeToFit];
+    [self.keyLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
+        make.size.mas_equalTo(self.keyLabel.frame.size);
+        make.leading.mas_equalTo(self.contentView).mas_offset(self.textData.keyEdgeInsets.left);
+        make.centerY.mas_equalTo(self.contentView);
+    }];
+    
+    [self.valueLabel sizeToFit];
+    [self.valueLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
+        make.leading.mas_equalTo(self.keyLabel.mas_trailing).mas_offset(10);
+        if (self.textData.showAccessory) {
+            make.trailing.mas_equalTo(self.contentView.mas_trailing).mas_offset(-10);
+        }
+        else {
+            make.trailing.mas_equalTo(self.contentView.mas_trailing).mas_offset(-20);
+        }
+        make.centerY.mas_equalTo(self.contentView);
+    }];
+}
+
+@end
+
+/////////////////////////////////////////////////////////////////////////////////
+//
+//                             TUICommonSwitchCell & data
+//
+/////////////////////////////////////////////////////////////////////////////////
+@implementation TUICommonSwitchCellData
+- (instancetype)init {
+    self = [super init];
+    _margin = 20;
+    return self;
+}
+
+- (CGFloat)heightOfWidth:(CGFloat)width {
+    CGFloat height = [super heightOfWidth:width];
+    if (self.desc.length > 0) {
+        NSString *str = self.desc;
+        NSDictionary *attribute = @{NSFontAttributeName : [UIFont systemFontOfSize:12]};
+        CGSize size = [str boundingRectWithSize:CGSizeMake(264, 999)
+                                        options:NSStringDrawingTruncatesLastVisibleLine | NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
+                                     attributes:attribute
+                                        context:nil]
+                          .size;
+        height += size.height + 10;
+    }
+    return height;
+}
+
+@end
+
+@interface TUICommonSwitchCell ()
+
+@property TUICommonSwitchCellData *switchData;
+@property(nonatomic, strong) UIView *leftSeparatorLine;
+
+@end
+
+@implementation TUICommonSwitchCell
+
+- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
+    if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
+        _titleLabel = [[UILabel alloc] init];
+        _titleLabel.textColor = TIMCommonDynamicColor(@"form_key_text_color", @"#444444");
+        _titleLabel.font = [UIFont systemFontOfSize:16];
+        [_titleLabel setRtlAlignment:TUITextRTLAlignmentLeading];
+        [self.contentView addSubview:_titleLabel];
+
+        _descLabel = [[UILabel alloc] init];
+        _descLabel.textColor = TIMCommonDynamicColor(@"group_modify_desc_color", @"#888888");
+        _descLabel.font = [UIFont systemFontOfSize:12];
+        _descLabel.numberOfLines = 0;
+        [_descLabel setRtlAlignment:TUITextRTLAlignmentLeading];
+        _descLabel.hidden = YES;
+        [self.contentView addSubview:_descLabel];
+
+        _switcher = [[UISwitch alloc] init];
+        _switcher.onTintColor = TIMCommonDynamicColor(@"common_switch_on_color", @"#147AFF");
+        self.accessoryView = _switcher;
+        [self.contentView addSubview:_switcher];
+        [_switcher addTarget:self action:@selector(switchClick) forControlEvents:UIControlEventValueChanged];
+
+        _leftSeparatorLine = [[UIView alloc] init];
+        _leftSeparatorLine.backgroundColor = [[UIColor grayColor] colorWithAlphaComponent:0.3];
+        [self.contentView addSubview:_leftSeparatorLine];
+
+        self.selectionStyle = UITableViewCellSelectionStyleNone;
+    }
+    return self;
+}
+
+- (void)layoutSubviews {
+    [super layoutSubviews];
+}
+
+- (void)fillWithData:(TUICommonSwitchCellData *)switchData {
+    [super fillWithData:switchData];
+
+    self.switchData = switchData;
+    _titleLabel.text = switchData.title;
+    [_switcher setOn:switchData.isOn];
+    _descLabel.text = switchData.desc;
+    
+    // tell constraints they need updating
+    [self setNeedsUpdateConstraints];
+
+    // update constraints now so we can animate the change
+    [self updateConstraintsIfNeeded];
+
+    [self layoutIfNeeded];
+
+}
+
++ (BOOL)requiresConstraintBasedLayout {
+    return YES;
+}
+
+// this is Apple's recommended place for adding/updating constraints
+- (void)updateConstraints {
+     
+    [super updateConstraints];
+    
+    if (self.switchData.disableChecked) {
+        _titleLabel.textColor = [UIColor grayColor];
+        _titleLabel.alpha = 0.4;
+        _switcher.alpha = 0.4;
+        self.userInteractionEnabled = NO;
+    } else {
+        _titleLabel.alpha = 1;
+        _switcher.alpha = 1;
+        _titleLabel.textColor = TIMCommonDynamicColor(@"form_key_text_color", @"#444444");
+        _switcher.onTintColor = TIMCommonDynamicColor(@"common_switch_on_color", @"#147AFF");
+        self.userInteractionEnabled = YES;
+    }
+
+    CGFloat leftMargin = 0;
+    CGFloat padding = 5;
+    if (self.switchData.displaySeparatorLine) {
+        _leftSeparatorLine.mm_width(10).mm_height(2).mm_left(self.switchData.margin).mm__centerY(self.contentView.mm_h / 2);
+        leftMargin = self.switchData.margin + _leftSeparatorLine.mm_w + padding;
+    } else {
+        _leftSeparatorLine.mm_width(0).mm_height(0);
+        leftMargin = self.switchData.margin;
+    }
+
+    if (self.switchData.desc.length > 0) {
+        _descLabel.text = self.switchData.desc;
+        _descLabel.hidden = NO;
+        NSString *str = self.switchData.desc;
+        NSDictionary *attribute = @{NSFontAttributeName : [UIFont systemFontOfSize:12]};
+        CGSize size = [str boundingRectWithSize:CGSizeMake(264, 999)
+                                        options:NSStringDrawingTruncatesLastVisibleLine | NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
+                                     attributes:attribute
+                                        context:nil]
+                          .size;
+        
+        [self.titleLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
+            make.width.mas_equalTo(size.width);
+            make.height.mas_equalTo(24);
+            make.leading.mas_equalTo(leftMargin);
+            make.top.mas_equalTo(12);
+        }];
+        [self.descLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
+            make.width.mas_equalTo(size.width);
+            make.height.mas_equalTo(size.height);
+            make.leading.mas_equalTo(self.titleLabel.mas_leading);
+            make.top.mas_equalTo(self.titleLabel.mas_bottom).mas_offset(2);
+        }];
+    } else {
+        _descLabel.text = @"";
+        [self.titleLabel sizeToFit];
+        [self.titleLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
+            make.size.mas_equalTo(self.titleLabel.frame.size);
+            make.leading.mas_equalTo(self.switchData.margin);
+            make.centerY.mas_equalTo(self.contentView);
+        }];
+    }
+}
+- (void)switchClick {
+    if (self.switchData.cswitchSelector) {
+        UIViewController *vc = self.mm_viewController;
+        if ([vc respondsToSelector:self.switchData.cswitchSelector]) {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+            [vc performSelector:self.switchData.cswitchSelector withObject:self];
+#pragma clang diagnostic pop
+        }
+    }
+}
+@end
+
+/////////////////////////////////////////////////////////////////////////////////
+//
+//                           TUIGroupPendencyCell & data
+//
+/////////////////////////////////////////////////////////////////////////////////
+@interface TUIGroupPendencyCellData ()
+@property V2TIMUserFullInfo *fromUserProfile;
+@property V2TIMGroupApplication *pendencyItem;
+@end
+
+@implementation TUIGroupPendencyCellData
+
+- (instancetype)initWithPendency:(V2TIMGroupApplication *)args {
+    self = [self init];
+
+    _pendencyItem = args;
+
+    _groupId = args.groupID;
+    _fromUser = args.fromUser;
+    _toUser = args.toUser;
+    if (args.fromUserNickName.length > 0) {
+        _title = args.fromUserNickName;
+    } else {
+        _title = args.fromUser;
+    }
+    _avatarUrl = [NSURL URLWithString:args.fromUserFaceUrl];
+    _requestMsg = args.requestMsg;
+    if (_requestMsg.length == 0) {
+        if (args.applicationType == V2TIM_GROUP_INVITE_APPLICATION_NEED_APPROVED_BY_ADMIN) {
+            _requestMsg = [NSString stringWithFormat:TIMCommonLocalizableString(TUIKitInviteJoinGroupFormat), _toUser];
+        } else {
+            _requestMsg = [NSString stringWithFormat:TIMCommonLocalizableString(TUIKitWhoRequestForJoinGroupFormat), _title];
+        }
+    }
+
+    return self;
+}
+
+- (void)accept {
+    [self agreeWithSuccess:nil failure:nil];
+}
+- (void)reject {
+    [self rejectWithSuccess:nil failure:nil];
+}
+
+
+- (void)agreeWithSuccess:(TUIGroupPendencyCellDataSuccessCallback)success
+                 failure:(TUIGroupPendencyCellDataFailureCallback)failure {
+    [[V2TIMManager sharedInstance] acceptGroupApplication:_pendencyItem
+        reason:TIMCommonLocalizableString(TUIKitAgreedByAdministor)
+        succ:^{
+              [TUITool makeToast:TIMCommonLocalizableString(Have_been_sent)];
+              [[NSNotificationCenter defaultCenter] postNotificationName:
+               TUIGroupPendencyCellData_onPendencyChanged object:nil];
+              if (success) {
+                  success();
+              }
+          ;
+        }
+        fail:^(int code, NSString *msg) {
+            [TUITool makeToastError:code msg:msg];
+            if (failure) {
+                failure(code,msg);
+            }
+        }];
+    self.isAccepted = YES;
+}
+
+- (void)rejectWithSuccess:(TUIGroupPendencyCellDataSuccessCallback)success
+                  failure:(TUIGroupPendencyCellDataFailureCallback)failure {
+    [[V2TIMManager sharedInstance] refuseGroupApplication:_pendencyItem
+        reason:TIMCommonLocalizableString(TUIkitDiscliedByAdministor)
+        succ:^{
+            [TUITool makeToast:TIMCommonLocalizableString(Have_been_sent)];
+            [[NSNotificationCenter defaultCenter] postNotificationName: TUIGroupPendencyCellData_onPendencyChanged object:nil];
+            if (success) {
+                success();
+            }
+        }
+        fail:^(int code, NSString *msg) {
+          [TUITool makeToastError:code msg:msg];
+            if (failure) {
+                failure(code,msg);
+            }
+        }];
+    self.isRejectd = YES;
+}
+@end
+
+@implementation TUIGroupPendencyCell
+
+- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
+    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
+
+    self.avatarView = [[UIImageView alloc] initWithImage:DefaultAvatarImage];
+    [self.contentView addSubview:self.avatarView];
+    self.avatarView.mm_width(54).mm_height(54).mm__centerY(38).mm_left(12);
+
+    self.titleLabel = [[UILabel alloc] initWithFrame:CGRectZero];
+    [self.contentView addSubview:self.titleLabel];
+    self.titleLabel.textColor = [UIColor darkTextColor];
+    self.titleLabel.mm_left(self.avatarView.mm_maxX + 12).mm_top(14).mm_height(20).mm_width(120);
+
+    self.addWordingLabel = [[UILabel alloc] initWithFrame:CGRectZero];
+    [self.contentView addSubview:self.addWordingLabel];
+    self.addWordingLabel.textColor = [UIColor lightGrayColor];
+    self.addWordingLabel.font = [UIFont systemFontOfSize:15];
+    self.addWordingLabel.mm_left(self.titleLabel.mm_x).mm_top(self.titleLabel.mm_maxY + 6).mm_height(15).mm_width(self.mm_w - self.titleLabel.mm_x - 80);
+
+    self.agreeButton = [UIButton buttonWithType:UIButtonTypeSystem];
+    self.accessoryView = self.agreeButton;
+    [self.agreeButton addTarget:self action:@selector(agreeClick) forControlEvents:UIControlEventTouchUpInside];
+
+    return self;
+}
+
+- (void)awakeFromNib {
+    [super awakeFromNib];
+    // Initialization code
+}
+
+- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
+    [super setSelected:selected animated:animated];
+
+    // Configure the view for the selected state
+}
+
+- (void)fillWithData:(TUIGroupPendencyCellData *)pendencyData {
+    [super fillWithData:pendencyData];
+
+    self.pendencyData = pendencyData;
+    self.titleLabel.text = pendencyData.title;
+    self.addWordingLabel.text = pendencyData.requestMsg;
+    self.avatarView.image = DefaultAvatarImage;
+    if (pendencyData.avatarUrl) {
+        [self.avatarView sd_setImageWithURL:pendencyData.avatarUrl placeholderImage:[UIImage imageNamed:TIMCommonImagePath(@"default_c2c_head")]];
+    }
+
+    @weakify(self);
+    [[RACObserve(pendencyData, isAccepted) takeUntil:self.rac_prepareForReuseSignal] subscribeNext:^(NSNumber *isAccepted) {
+      @strongify(self);
+      if ([isAccepted boolValue]) {
+          [self.agreeButton setTitle:TIMCommonLocalizableString(Agreed) forState:UIControlStateNormal];
+          self.agreeButton.enabled = NO;
+          [self.agreeButton setTitleColor:[UIColor lightGrayColor] forState:UIControlStateNormal];
+          self.agreeButton.layer.borderColor = [UIColor clearColor].CGColor;
+      }
+    }];
+    [[RACObserve(pendencyData, isRejectd) takeUntil:self.rac_prepareForReuseSignal] subscribeNext:^(NSNumber *isAccepted) {
+      @strongify(self);
+      if ([isAccepted boolValue]) {
+          [self.agreeButton setTitle:TIMCommonLocalizableString(Disclined) forState:UIControlStateNormal];
+          self.agreeButton.enabled = NO;
+          [self.agreeButton setTitleColor:[UIColor lightGrayColor] forState:UIControlStateNormal];
+          self.agreeButton.layer.borderColor = [UIColor clearColor].CGColor;
+      }
+    }];
+
+    if (!(pendencyData.isAccepted || pendencyData.isRejectd)) {
+        [self.agreeButton setTitle:TIMCommonLocalizableString(Agree) forState:UIControlStateNormal];
+        self.agreeButton.enabled = YES;
+        [self.agreeButton setTitleColor:[UIColor darkTextColor] forState:UIControlStateNormal];
+        self.agreeButton.layer.borderColor = [UIColor grayColor].CGColor;
+        self.agreeButton.layer.borderWidth = 1;
+    }
+    self.agreeButton.mm_sizeToFit().mm_width(self.agreeButton.mm_w + 20);
+}
+
+- (void)agreeClick {
+    if (self.pendencyData.cbuttonSelector) {
+        UIViewController *vc = self.mm_viewController;
+        if ([vc respondsToSelector:self.pendencyData.cbuttonSelector]) {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+            [vc performSelector:self.pendencyData.cbuttonSelector withObject:self];
+#pragma clang diagnostic pop
+        }
+    }
+}
+
+- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
+    if ((touch.view == self.agreeButton)) {
+        return NO;
+    }
+    return YES;
+}
+
+@end
+
+/////////////////////////////////////////////////////////////////////////////////
+//
+//                           TUIButtonCell & data
+//
+/////////////////////////////////////////////////////////////////////////////////
+@implementation TUIButtonCellData
+
+- (CGFloat)heightOfWidth:(CGFloat)width {
+    return TButtonCell_Height;
+}
+@end
+
+@implementation TUIButtonCell {
+    UIView *_line;
+}
+
+- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
+    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
+    if (self) {
+        [self setupViews];
+        self.changeColorWhenTouched = YES;
+    }
+    return self;
+}
+
+- (void)setupViews {
+    self.backgroundColor = TIMCommonDynamicColor(@"form_bg_color", @"#FFFFFF");
+    self.contentView.backgroundColor = TIMCommonDynamicColor(@"form_bg_color", @"#FFFFFF");
+
+    _button = [UIButton buttonWithType:UIButtonTypeCustom];
+    [_button.titleLabel setFont:[UIFont systemFontOfSize:18]];
+    [_button addTarget:self action:@selector(onClick:) forControlEvents:UIControlEventTouchUpInside];
+
+    [self.contentView addSubview:_button];
+
+    [self setSeparatorInset:UIEdgeInsetsMake(0, Screen_Width, 0, 0)];
+    [self setSelectionStyle:UITableViewCellSelectionStyleNone];
+    self.changeColorWhenTouched = YES;
+
+    _line = [[UIView alloc] initWithFrame:CGRectZero];
+    [self.contentView addSubview:_line];
+    _line.backgroundColor = TIMCommonDynamicColor(@"separator_color", @"#DBDBDB");
+}
+
+- (void)fillWithData:(TUIButtonCellData *)data {
+    [super fillWithData:data];
+    self.buttonData = data;
+    [_button setTitle:data.title forState:UIControlStateNormal];
+    switch (data.style) {
+        case ButtonGreen: {
+            [_button setTitleColor:TIMCommonDynamicColor(@"form_green_button_text_color", @"#FFFFFF") forState:UIControlStateNormal];
+            _button.backgroundColor = TIMCommonDynamicColor(@"form_green_button_bg_color", @"#232323");
+            [_button setBackgroundImage:[self imageWithColor:TIMCommonDynamicColor(@"form_green_button_highlight_bg_color", @"#179A1A")]
+                               forState:UIControlStateHighlighted];
+        } break;
+        case ButtonWhite: {
+            [_button setTitleColor:TIMCommonDynamicColor(@"form_white_button_text_color", @"#000000") forState:UIControlStateNormal];
+            _button.backgroundColor = TIMCommonDynamicColor(@"form_white_button_bg_color", @"#FFFFFF");
+        } break;
+        case ButtonRedText: {
+            [_button setTitleColor:TIMCommonDynamicColor(@"form_redtext_button_text_color", @"#FF0000") forState:UIControlStateNormal];
+            _button.backgroundColor = TIMCommonDynamicColor(@"form_redtext_button_bg_color", @"#FFFFFF");
+
+            break;
+        }
+        case ButtonBule: {
+            [_button.titleLabel setTextColor:TIMCommonDynamicColor(@"form_blue_button_text_color", @"#FFFFFF")];
+            _button.backgroundColor = TIMCommonDynamicColor(@"form_blue_button_bg_color", @"#1E90FF");
+            [_button setBackgroundImage:[self imageWithColor:TIMCommonDynamicColor(@"form_blue_button_highlight_bg_color", @"#1978D5")]
+                               forState:UIControlStateHighlighted];
+        } break;
+        default:
+            break;
+    }
+
+    if (data.textColor) {
+        [_button setTitleColor:data.textColor forState:UIControlStateNormal];
+    }
+
+    _line.hidden = data.hideSeparatorLine;
+}
+
+- (void)layoutSubviews {
+    [super layoutSubviews];
+    _button.mm_width(Screen_Width - 2 * TButtonCell_Margin).mm_height(self.mm_h - TButtonCell_Margin).mm_left(TButtonCell_Margin);
+
+    _line.mm_width(Screen_Width).mm_height(0.2).mm_left(20).mm_bottom(0);
+}
+
+- (void)onClick:(UIButton *)sender {
+    if (self.buttonData.cbuttonSelector) {
+        UIViewController *vc = self.mm_viewController;
+        if ([vc respondsToSelector:self.buttonData.cbuttonSelector]) {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+            [vc performSelector:self.buttonData.cbuttonSelector withObject:self];
+#pragma clang diagnostic pop
+        }
+    }
+}
+
+- (void)didAddSubview:(UIView *)subview {
+    [super didAddSubview:subview];
+    if (subview != self.contentView) {
+        [subview removeFromSuperview];
+    }
+}
+
+- (UIImage *)imageWithColor:(UIColor *)color {
+    CGRect rect = CGRectMake(0.0f, 0.0f, 1.0f, 1.0f);
+    UIGraphicsBeginImageContext(rect.size);
+    CGContextRef context = UIGraphicsGetCurrentContext();
+
+    CGContextSetFillColorWithColor(context, [color CGColor]);
+    CGContextFillRect(context, rect);
+
+    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
+    UIGraphicsEndImageContext();
+
+    return image;
+}
+
+@end
+
+/////////////////////////////////////////////////////////////////////////////////
+//
+//                             TUIFaceCell & data
+//
+/////////////////////////////////////////////////////////////////////////////////
+@implementation TUIFaceCellData
+@end
+
+@implementation TUIFaceCell
+- (id)initWithFrame:(CGRect)frame {
+    self = [super initWithFrame:frame];
+    if (self) {
+        [self setupViews];
+        [self defaultLayout];
+    }
+    return self;
+}
+
+- (void)setupViews {
+    _face = [[UIImageView alloc] init];
+    _face.contentMode = UIViewContentModeScaleAspectFill;
+    [self addSubview:_face];
+    UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(onLongPress:)];
+    [self addGestureRecognizer:longPress];
+    [self setUserInteractionEnabled:YES];
+}
+
+- (void)defaultLayout {
+    CGSize size = self.frame.size;
+    _face.frame = CGRectMake(0, 0, size.width, size.height);
+}
+#define kTUIFaceCellAllowDynamicImageShow 0
+- (void)setData:(TUIFaceCellData *)data {
+    if (!kTUIFaceCellAllowDynamicImageShow) {
+        UIImage * image = [[TUIImageCache sharedInstance] getFaceFromCache:data.path];
+        SDImageFormat imageFormat = [image sd_imageFormat];
+        if (SDImageFormatGIF == imageFormat ) {
+            self.gifImage = image;
+            if (image.images.count > 1) {
+                self.staicImage = image.images[0];
+            }
+        }
+        else {
+            self.staicImage = image;
+        }
+        
+        _face.image = self.staicImage;
+    }
+    else {
+        _face.image = [[TUIImageCache sharedInstance] getFaceFromCache:data.path];
+    }
+    [self defaultLayout];
+}
+
+- (void)onLongPress:(UILongPressGestureRecognizer *)longPress {
+    if (self.longPressCallback) {
+        self.longPressCallback(longPress);
+    }
+}
+
+@end
+
+/////////////////////////////////////////////////////////////////////////////////
+//
+//                          TUIFaceGroup
+//
+/////////////////////////////////////////////////////////////////////////////////
+@implementation TUIFaceGroup
+
+- (NSDictionary *)facesMap {
+    if (!_facesMap || (_facesMap.count != _faces.count )) {
+        NSMutableDictionary *faceDic = [NSMutableDictionary dictionaryWithCapacity:3];
+        if (_faces.count > 0) {
+            for (TUIFaceCellData *data in _faces) {
+                [faceDic setObject:data.path forKey:data.name];
+            }
+        }
+        _facesMap = [NSDictionary dictionaryWithDictionary:faceDic];
+    }
+    return _facesMap;
+}
+@end
+
+@implementation TUIEmojiTextAttachment
+
+- (CGRect)attachmentBoundsForTextContainer:(NSTextContainer *)textContainer
+                      proposedLineFragment:(CGRect)lineFrag
+                             glyphPosition:(CGPoint)position
+                            characterIndex:(NSUInteger)charIndex {
+    return CGRectMake( 0 , -0.4* lineFrag.size.height, kTIMDefaultEmojiSize.width , kTIMDefaultEmojiSize.height);
+}
+
+@end
+/////////////////////////////////////////////////////////////////////////////////
+//
+//                          TUIUnReadView
+//
+/////////////////////////////////////////////////////////////////////////////////
+@implementation TUIUnReadView
+- (id)init {
+    self = [super init];
+    if (self) {
+        [self setupViews];
+        [self defaultLayout];
+    }
+    return self;
+}
+
+- (void)setNum:(NSInteger)num {
+    NSString *unReadStr = [[NSNumber numberWithInteger:num] stringValue];
+    if (num > 99) {
+        unReadStr = @"99+";
+    }
+    _unReadLabel.text = unReadStr;
+    self.hidden = (num == 0 ? YES : NO);
+    [self defaultLayout];
+}
+
+- (void)setupViews {
+    _unReadLabel = [[UILabel alloc] init];
+    _unReadLabel.text = @"11";
+    _unReadLabel.font = [UIFont systemFontOfSize:10];
+    _unReadLabel.textColor = [UIColor whiteColor];
+    _unReadLabel.textAlignment = NSTextAlignmentCenter;
+    [_unReadLabel sizeToFit];
+    [self addSubview:_unReadLabel];
+
+    self.layer.cornerRadius = (_unReadLabel.frame.size.height + TUnReadView_Margin_TB * 2) / 2.0;
+    [self.layer masksToBounds];
+    self.backgroundColor = [UIColor redColor];
+    self.hidden = YES;
+}
+
+- (void)defaultLayout {
+    [_unReadLabel sizeToFit];
+    CGFloat width = _unReadLabel.frame.size.width + 2 * TUnReadView_Margin_LR;
+    CGFloat height = _unReadLabel.frame.size.height + 2 * TUnReadView_Margin_TB;
+    if (width < height) {
+        width = height;
+    }
+    self.bounds = CGRectMake(0, 0, width, height);
+    _unReadLabel.frame = self.bounds;
+}
+
+- (void)layoutSubviews {
+    [super layoutSubviews];
+    if (@available(iOS 11.0, *)) {
+        // Here is a workaround on iOS 11 UINavigationBarItem init with custom view, position issue
+        UIView *view = self;
+        while (![view isKindOfClass:[UINavigationBar class]] && [view superview] != nil) {
+            view = [view superview];
+            if ([view isKindOfClass:[UIStackView class]] && [view superview] != nil) {
+                CGFloat margin = 40.0f;
+                // margin = 4.0f;
+                [view.superview addConstraint:[NSLayoutConstraint constraintWithItem:view
+                                                                           attribute:NSLayoutAttributeLeading
+                                                                           relatedBy:NSLayoutRelationEqual
+                                                                              toItem:view.superview
+                                                                           attribute:NSLayoutAttributeLeading
+                                                                          multiplier:1.0
+                                                                            constant:margin]];
+                break;
+            }
+        }
+    }
+}
+
+@end
+
+/////////////////////////////////////////////////////////////////////////////////
+//
+//                          TUIConversationPin
+//
+/////////////////////////////////////////////////////////////////////////////////
+#define TOP_CONV_KEY @"TUIKIT_TOP_CONV_KEY"
+NSString *kTopConversationListChangedNotification = @"kTopConversationListChangedNotification";
+
+@implementation TUIConversationPin
++ (instancetype)sharedInstance {
+    static TUIConversationPin *instance;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+      instance = [TUIConversationPin new];
+    });
+    return instance;
+}
+
+- (NSArray *)topConversationList {
+#ifndef SDKPlaceTop
+#define SDKPlaceTop
+#endif
+#ifdef SDKPlaceTop
+    return @[];
+#else
+    NSArray *list = [[NSUserDefaults standardUserDefaults] objectForKey:TOP_CONV_KEY];
+    if ([list isKindOfClass:[NSArray class]]) {
+        return list;
+    }
+    return @[];
+#endif
+}
+
+- (void)addTopConversation:(NSString *)conv callback:(void (^)(BOOL success, NSString *errorMessage))callback {
+#ifndef SDKPlaceTop
+#define SDKPlaceTop
+#endif
+#ifdef SDKPlaceTop
+    [V2TIMManager.sharedInstance pinConversation:conv
+        isPinned:YES
+        succ:^{
+          if (callback) {
+              callback(YES, nil);
+          }
+        }
+        fail:^(int code, NSString *desc) {
+          if (callback) {
+              callback(NO, desc);
+          }
+        }];
+#else
+    [TUITool dispatchMainAsync:^{
+      NSMutableArray *list = [self topConversationList].mutableCopy;
+      if ([list containsObject:conv]) {
+          [list removeObject:conv];
+      }
+      [list insertObject:conv atIndex:0];
+      [[NSUserDefaults standardUserDefaults] setValue:list forKey:TOP_CONV_KEY];
+      [[NSNotificationCenter defaultCenter] postNotificationName:kTopConversationListChangedNotification object:nil];
+      if (callback) {
+          callback(YES, nil);
+      }
+    }];
+#endif
+}
+
+- (void)removeTopConversation:(NSString *)conv callback:(void (^)(BOOL success, NSString *errorMessage))callback {
+#ifndef SDKPlaceTop
+#define SDKPlaceTop
+#endif
+#ifdef SDKPlaceTop
+    [V2TIMManager.sharedInstance pinConversation:conv
+        isPinned:NO
+        succ:^{
+          if (callback) {
+              callback(YES, nil);
+          }
+        }
+        fail:^(int code, NSString *desc) {
+          if (callback) {
+              callback(NO, desc);
+          }
+        }];
+#else
+    [TUITool dispatchMainAsync:^{
+      NSMutableArray *list = [self topConversationList].mutableCopy;
+      if ([list containsObject:conv]) {
+          [list removeObject:conv];
+          [[NSUserDefaults standardUserDefaults] setValue:list forKey:TOP_CONV_KEY];
+          [[NSNotificationCenter defaultCenter] postNotificationName:kTopConversationListChangedNotification object:nil];
+      }
+      if (callback) {
+          callback(YES, nil);
+      }
+    }];
+#endif
+}
+@end
+
+/////////////////////////////////////////////////////////////////////////////////
+//
+//                          TUICommonContactSelectCellData
+//
+/////////////////////////////////////////////////////////////////////////////////
+@implementation TUICommonContactSelectCellData
+
+- (instancetype)init {
+    self = [super init];
+    if (self) {
+        _enabled = YES;
+    }
+    return self;
+}
+
+- (NSComparisonResult)compare:(TUICommonContactSelectCellData *)data {
+    return [self.title localizedCompare:data.title];
+}
+
+@end
+
+/////////////////////////////////////////////////////////////////////////////////
+//
+//                          TUICommonContactListPickerCell
+//
+/////////////////////////////////////////////////////////////////////////////////
+@implementation TUICommonContactListPickerCell
+
+- (instancetype)initWithFrame:(CGRect)frame {
+    self = [super initWithFrame:frame];
+    if (self) {
+        CGFloat avatarWidth = 35.0;
+        _avatar = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, avatarWidth, avatarWidth)];
+        [self.contentView addSubview:_avatar];
+        _avatar.center = CGPointMake(avatarWidth / 2.0, avatarWidth / 2.0);
+        _avatar.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin |
+                                   UIViewAutoresizingFlexibleBottomMargin;
+    }
+    return self;
+}
+
+- (void)layoutSubviews {
+    [super layoutSubviews];
+    if ([TUIConfig defaultConfig].avatarType == TAvatarTypeRounded) {
+        _avatar.layer.masksToBounds = YES;
+        _avatar.layer.cornerRadius = _avatar.frame.size.height / 2;
+    } else if ([TUIConfig defaultConfig].avatarType == TAvatarTypeRadiusCorner) {
+        _avatar.layer.masksToBounds = YES;
+        _avatar.layer.cornerRadius = [TUIConfig defaultConfig].avatarCornerRadius;
+    }
+}
+
+@end
+
+/////////////////////////////////////////////////////////////////////////////////
+//
+//                          TUIContactListPickerOnCancel
+//
+/////////////////////////////////////////////////////////////////////////////////
+@interface TUIContactListPicker () <UICollectionViewDelegate, UICollectionViewDataSource>
+@property(nonatomic) UICollectionView *collectionView;
+@property(nonatomic) UIButton *accessoryBtn;
+@end
+
+@implementation TUIContactListPicker
+
+- (instancetype)initWithFrame:(CGRect)frame {
+    self = [super initWithFrame:frame];
+
+    [self initControl];
+    [self setupBinding];
+
+    return self;
+}
+
+- (void)initControl {
+    UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
+    layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
+
+    self.collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout];
+    self.collectionView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
+    self.collectionView.showsHorizontalScrollIndicator = NO;
+    self.collectionView.decelerationRate = UIScrollViewDecelerationRateNormal;
+
+    [self.collectionView registerClass:[TUICommonContactListPickerCell class] forCellWithReuseIdentifier:@"PickerIdentifier"];
+    [self.collectionView setBackgroundColor:[UIColor clearColor]];
+    [self.collectionView setDelegate:self];
+    [self.collectionView setDataSource:self];
+
+    [self addSubview:_collectionView];
+
+    self.accessoryBtn = [UIButton buttonWithType:UIButtonTypeCustom];
+    [self.accessoryBtn setBackgroundImage:TIMCommonBundleImage(@"icon_cell_blue_normal") forState:UIControlStateNormal];
+    [self.accessoryBtn setBackgroundImage:TIMCommonBundleImage(@"icon_cell_blue_normal") forState:UIControlStateHighlighted];
+    [self.accessoryBtn setTitle:[NSString stringWithFormat:@" %@ ", TIMCommonLocalizableString(Confirm)] forState:UIControlStateNormal];
+    self.accessoryBtn.enabled = NO;
+    [self addSubview:self.accessoryBtn];
+}
+
+- (void)setupBinding {
+    [self addObserver:self forKeyPath:@"selectArray" options:NSKeyValueObservingOptionNew context:nil];
+}
+
+- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey, id> *)change context:(void *)context {
+    if ([keyPath isEqualToString:@"selectArray"]) {
+        [self.collectionView reloadData];
+        NSArray *newSelectArray = change[NSKeyValueChangeNewKey];
+        if ([newSelectArray isKindOfClass:NSArray.class]) {
+            self.accessoryBtn.enabled = [newSelectArray count];
+        }
+    }
+}
+
+- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
+    return [self.selectArray count];
+}
+
+- (CGSize)collectionView:(nonnull UICollectionView *)collectionView
+                    layout:(nonnull UICollectionViewLayout *)collectionViewLayout
+    sizeForItemAtIndexPath:(nonnull NSIndexPath *)indexPath {
+    return CGSizeMake(35, collectionView.bounds.size.height);
+}
+
+- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
+    TUICommonContactListPickerCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"PickerIdentifier" forIndexPath:indexPath];
+
+    TUICommonContactSelectCellData *data = self.selectArray[indexPath.row];
+    if (data.avatarUrl) {
+        [cell.avatar sd_setImageWithURL:data.avatarUrl placeholderImage:DefaultAvatarImage];
+    } else if (data.avatarImage) {
+        cell.avatar.image = data.avatarImage;
+    } else {
+        cell.avatar.image = DefaultAvatarImage;
+    }
+    return cell;
+}
+
+- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
+    [collectionView deselectItemAtIndexPath:indexPath animated:NO];
+    if (indexPath.item >= self.selectArray.count) {
+        return;
+    }
+    TUICommonContactSelectCellData *data = self.selectArray[indexPath.item];
+    if (self.onCancel) {
+        self.onCancel(data);
+    }
+}
+
+- (void)layoutSubviews {
+    [super layoutSubviews];
+
+    self.accessoryBtn.mm_sizeToFit().mm_height(30).mm_right(15).mm_top(13);
+    self.collectionView.mm_left(15).mm_height(40).mm_width(self.accessoryBtn.mm_x - 30).mm__centerY(self.accessoryBtn.mm_centerY);
+    if (isRTL()) {
+        [self.accessoryBtn resetFrameToFitRTL];
+        [self.collectionView resetFrameToFitRTL];
+    }
+}
+
+@end
+
+/////////////////////////////////////////////////////////////////////////////////
+//
+//                          TUIProfileCardCell & VC
+//
+/////////////////////////////////////////////////////////////////////////////////
+@implementation TUIProfileCardCellData
+
+- (instancetype)init {
+    self = [super init];
+    if (self) {
+        _avatarImage = DefaultAvatarImage;
+
+        if ([_genderString isEqualToString:TIMCommonLocalizableString(Male)]) {
+            _genderIconImage = TUIContactCommonBundleImage(@"male");
+        } else if ([_genderString isEqualToString:TIMCommonLocalizableString(Female)]) {
+            _genderIconImage = TUIContactCommonBundleImage(@"female");
+        } else {
+            _genderIconImage = nil;
+        }
+    }
+    return self;
+}
+
+- (CGFloat)heightOfWidth:(CGFloat)width {
+    return TPersonalCommonCell_Image_Size.height + 2 * TPersonalCommonCell_Margin + (self.showSignature ? 24 : 0);
+}
+
+@end
+
+@implementation TUIProfileCardCell
+- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
+    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
+    if (self) {
+        [self setupViews];
+    }
+    return self;
+}
+
+- (void)setupViews {
+    CGSize headSize = TPersonalCommonCell_Image_Size;
+    _avatar = [[UIImageView alloc] initWithFrame:CGRectMake(TPersonalCommonCell_Margin, TPersonalCommonCell_Margin, headSize.width, headSize.height)];
+    _avatar.contentMode = UIViewContentModeScaleAspectFit;
+    _avatar.layer.cornerRadius = 4;
+    _avatar.layer.masksToBounds = YES;
+    UITapGestureRecognizer *tapAvatar = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTapAvatar)];
+    [_avatar addGestureRecognizer:tapAvatar];
+    _avatar.userInteractionEnabled = YES;
+
+    if ([TUIConfig defaultConfig].avatarType == TAvatarTypeRounded) {
+        self.avatar.layer.masksToBounds = YES;
+        self.avatar.layer.cornerRadius = headSize.height / 2;
+    } else if ([TUIConfig defaultConfig].avatarType == TAvatarTypeRadiusCorner) {
+        self.avatar.layer.masksToBounds = YES;
+        self.avatar.layer.cornerRadius = [TUIConfig defaultConfig].avatarCornerRadius;
+    }
+    [self.contentView addSubview:_avatar];
+
+    // CGSize genderIconSize = CGSizeMake(20, 20);
+    _genderIcon = [[UIImageView alloc] init];
+    _genderIcon.contentMode = UIViewContentModeScaleAspectFit;
+    _genderIcon.image = self.cardData.genderIconImage;
+    [self.contentView addSubview:_genderIcon];
+
+    _name = [[UILabel alloc] init];
+    [_name setFont:[UIFont boldSystemFontOfSize:18]];
+    [_name setTextColor:TIMCommonDynamicColor(@"form_title_color", @"#000000")];
+    [self.contentView addSubview:_name];
+
+    _identifier = [[UILabel alloc] init];
+    [_identifier setFont:[UIFont systemFontOfSize:13]];
+    [_identifier setTextColor:TIMCommonDynamicColor(@"form_subtitle_color", @"#888888")];
+    [self.contentView addSubview:_identifier];
+
+    _signature = [[UILabel alloc] init];
+    [_signature setFont:[UIFont systemFontOfSize:14]];
+    [_signature setTextColor:TIMCommonDynamicColor(@"form_subtitle_color", @"#888888")];
+    [self.contentView addSubview:_signature];
+
+    self.selectionStyle = UITableViewCellSelectionStyleNone;
+}
+
+- (void)fillWithData:(TUIProfileCardCellData *)data {
+    [super fillWithData:data];
+    self.cardData = data;
+    _signature.hidden = !data.showSignature;
+    // set data
+    @weakify(self);
+
+    RAC(_signature, text) = [RACObserve(data, signature) takeUntil:self.rac_prepareForReuseSignal];
+    [[[RACObserve(data, identifier) takeUntil:self.rac_prepareForReuseSignal] distinctUntilChanged] subscribeNext:^(NSString *x) {
+      @strongify(self);
+        self.identifier.text = [NSString stringWithFormat:@"%@:%@",TIMCommonLocalizableString(TUIKitIdentity),data.identifier];
+    }];
+
+    [[[RACObserve(data, name) takeUntil:self.rac_prepareForReuseSignal] distinctUntilChanged] subscribeNext:^(NSString *x) {
+      @strongify(self);
+      self.name.text = x;
+      [self.name sizeToFit];
+    }];
+    [[RACObserve(data, avatarUrl) takeUntil:self.rac_prepareForReuseSignal] subscribeNext:^(NSURL *x) {
+      @strongify(self);
+      [self.avatar sd_setImageWithURL:x placeholderImage:self.cardData.avatarImage];
+    }];
+
+    [[RACObserve(data, genderString) takeUntil:self.rac_prepareForReuseSignal] subscribeNext:^(NSString *x) {
+      @strongify(self);
+      if ([x isEqualToString:TIMCommonLocalizableString(Male)]) {
+          self.genderIcon.image = TUIContactCommonBundleImage(@"male");
+      } else if ([x isEqualToString:TIMCommonLocalizableString(Female)]) {
+          self.genderIcon.image = TUIContactCommonBundleImage(@"female");
+      } else {
+          self.genderIcon.image = nil;
+      }
+    }];
+
+    if (data.showAccessory) {
+        self.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
+    } else {
+        self.accessoryType = UITableViewCellAccessoryNone;
+    }
+    // tell constraints they need updating
+    [self setNeedsUpdateConstraints];
+
+    // update constraints now so we can animate the change
+    [self updateConstraintsIfNeeded];
+
+    [self layoutIfNeeded];
+}
+
+- (void)layoutSubviews {
+    [super layoutSubviews];
+}
+
++ (BOOL)requiresConstraintBasedLayout {
+    return YES;
+}
+
+// this is Apple's recommended place for adding/updating constraints
+- (void)updateConstraints {
+    [super updateConstraints];
+    CGSize headSize = CGSizeMake(kScale390(66), kScale390(66));
+
+    [self.avatar mas_remakeConstraints:^(MASConstraintMaker *make) {
+        make.size.mas_equalTo(headSize);
+        make.top.mas_equalTo(kScale390(10));
+        make.leading.mas_equalTo(kScale390(16));
+    }];
+    
+    if ([TUIConfig defaultConfig].avatarType == TAvatarTypeRounded) {
+        self.avatar.layer.masksToBounds = YES;
+        self.avatar.layer.cornerRadius = headSize.height / 2;
+    } else if ([TUIConfig defaultConfig].avatarType == TAvatarTypeRadiusCorner) {
+        self.avatar.layer.masksToBounds = YES;
+        self.avatar.layer.cornerRadius = [TUIConfig defaultConfig].avatarCornerRadius;
+    }
+
+    [self.name sizeToFit];
+    [self.name mas_remakeConstraints:^(MASConstraintMaker *make) {
+        make.top.mas_equalTo(TPersonalCommonCell_Margin);
+        make.leading.mas_equalTo(self.avatar.mas_trailing).mas_offset(15);
+        make.width.mas_lessThanOrEqualTo(self.name.frame.size.width);
+        make.height.mas_greaterThanOrEqualTo(self.name.frame.size.height);
+        make.trailing.mas_lessThanOrEqualTo(self.genderIcon.mas_leading).mas_offset(- 1);
+    }];
+
+    [self.genderIcon mas_remakeConstraints:^(MASConstraintMaker *make) {
+        make.width.height.mas_equalTo(self.name.font.pointSize *0.9);
+        make.centerY.mas_equalTo(self.name);
+        make.leading.mas_equalTo(self.name.mas_trailing).mas_offset(1);
+        make.trailing.mas_lessThanOrEqualTo(self.contentView.mas_trailing).mas_offset(- 10);
+    }];
+
+    [self.identifier sizeToFit];
+    [self.identifier mas_remakeConstraints:^(MASConstraintMaker *make) {
+        make.leading.mas_equalTo(self.name);
+        make.top.mas_equalTo(self.name.mas_bottom).mas_offset(5);
+        if(self.identifier.frame.size.width > 80) {
+            make.width.mas_greaterThanOrEqualTo(self.identifier.frame.size.width);
+        }
+        else {
+            make.width.mas_greaterThanOrEqualTo(@80);
+        }
+        make.height.mas_greaterThanOrEqualTo(self.identifier.frame.size.height);
+        make.trailing.mas_lessThanOrEqualTo(self.contentView.mas_trailing).mas_offset(-1);
+    }];
+
+    if (self.cardData.showSignature) {
+        [self.signature sizeToFit];
+        [self.signature mas_remakeConstraints:^(MASConstraintMaker *make) {
+            make.leading.mas_equalTo(self.name);
+            make.top.mas_equalTo(self.identifier.mas_bottom).mas_offset(5);
+            if(self.signature.frame.size.width > 80) {
+                make.width.mas_greaterThanOrEqualTo(self.signature.frame.size.width);
+            }
+            else {
+                make.width.mas_greaterThanOrEqualTo(@80);
+            }
+            make.height.mas_greaterThanOrEqualTo(self.signature.frame.size.height);
+            make.trailing.mas_lessThanOrEqualTo(self.contentView.mas_trailing).mas_offset(-1);
+        }];
+        
+    } else {
+        self.signature.frame = CGRectZero;
+    }
+
+}
+
+- (void)onTapAvatar {
+    if (_delegate && [_delegate respondsToSelector:@selector(didTapOnAvatar:)]) [_delegate didTapOnAvatar:self];
+}
+
+@end
+
+@interface TUIAvatarViewController () <UIScrollViewDelegate>
+@property UIImageView *avatarView;
+
+@property TUIScrollView *avatarScrollView;
+
+@property UIImage *saveBackgroundImage;
+@property UIImage *saveShadowImage;
+
+@end
+
+@implementation TUIAvatarViewController
+
+- (void)viewDidLoad {
+    [super viewDidLoad];
+
+    self.saveBackgroundImage = [self.navigationController.navigationBar backgroundImageForBarMetrics:UIBarMetricsDefault];
+    self.saveShadowImage = self.navigationController.navigationBar.shadowImage;
+    [self.navigationController.navigationBar setBackgroundImage:[UIImage new] forBarMetrics:UIBarMetricsDefault];
+    self.navigationController.navigationBar.shadowImage = [UIImage new];
+
+    CGRect rect = self.view.bounds;
+    self.avatarScrollView = [[TUIScrollView alloc] initWithFrame:CGRectZero];
+    [self.view addSubview:self.avatarScrollView];
+    self.avatarScrollView.backgroundColor = [UIColor blackColor];
+    self.avatarScrollView.frame = rect;
+
+    self.avatarView = [[UIImageView alloc] initWithImage:self.avatarData.avatarImage];
+    self.avatarScrollView.imageView = self.avatarView;
+    self.avatarScrollView.maximumZoomScale = 4.0;
+    self.avatarScrollView.delegate = self;
+
+    self.avatarView.image = self.avatarData.avatarImage;
+    TUIProfileCardCellData *data = self.avatarData;
+    /*
+     @weakify(self);
+    [RACObserve(data, avatarUrl) subscribeNext:^(NSURL *x) {
+        @strongify(self);
+        [self.avatarView sd_setImageWithURL:x placeholderImage:self.avatarData.avatarImage];
+    }];
+    */
+    @weakify(self);
+    [RACObserve(data, avatarUrl) subscribeNext:^(NSURL *x) {
+      @strongify(self);
+      [self.avatarView sd_setImageWithURL:x placeholderImage:self.avatarData.avatarImage];
+      [self.avatarScrollView setNeedsLayout];
+    }];
+}
+
+- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
+    return self.avatarView;
+}
+
+- (void)viewWillAppear:(BOOL)animated {
+    [super viewWillAppear:animated];
+    [[UIApplication sharedApplication] setStatusBarHidden:YES withAnimation:UIStatusBarAnimationNone];
+
+    [self.navigationController.navigationBar setBackgroundImage:[UIImage new] forBarMetrics:UIBarMetricsDefault];
+    self.navigationController.navigationBar.shadowImage = [UIImage new];
+
+    self.navigationController.navigationBar.backgroundColor = [UIColor clearColor];
+}
+
+- (void)viewWillDisappear:(BOOL)animated {
+    [super viewWillDisappear:animated];
+    [[UIApplication sharedApplication] setStatusBarHidden:NO withAnimation:UIStatusBarAnimationNone];
+}
+
+- (void)willMoveToParentViewController:(UIViewController *)parent {
+    if (parent == nil) {
+        [self.navigationController.navigationBar setBackgroundImage:self.saveBackgroundImage forBarMetrics:UIBarMetricsDefault];
+        self.navigationController.navigationBar.shadowImage = self.saveShadowImage;
+    }
+}
+
+@end
+
+#define UserAvatarURL(x) [NSString stringWithFormat:@"https://im.sdk.qcloud.com/download/tuikit-resource/avatar/avatar_%d.png", x]
+#define UserAvatarCount 26
+
+#define GroupAvatarURL(x) [NSString stringWithFormat:@"https://im.sdk.qcloud.com/download/tuikit-resource/group-avatar/group_avatar_%d.png", x]
+#define GroupAvatarCount 24
+
+#define Community_coverURL(x) [NSString stringWithFormat:@"https://im.sdk.qcloud.com/download/tuikit-resource/community-cover/community_cover_%d.png", x]
+#define Community_coverCount 12
+
+#define BackGroundCoverURL(x) \
+    [NSString stringWithFormat:@"https://im.sdk.qcloud.com/download/tuikit-resource/conversation-backgroundImage/backgroundImage_%d.png", x]
+
+#define BackGroundCoverURL_full(x) \
+    [NSString stringWithFormat:@"https://im.sdk.qcloud.com/download/tuikit-resource/conversation-backgroundImage/backgroundImage_%d_full.png", x]
+
+#define BackGroundCoverCount 7
+
+@implementation TUISelectAvatarCardItem
+
+@end
+
+@interface TUISelectAvatarCollectionCell : UICollectionViewCell
+
+@property(nonatomic, strong) UIImageView *imageView;
+@property(nonatomic, strong) UIImageView *selectedView;
+
+@property(nonatomic, strong) UIView *maskView;
+@property(nonatomic, strong) UILabel *descLabel;
+@property(nonatomic, strong) TUISelectAvatarCardItem *cardItem;
+
+- (void)updateSelectedUI;
+
+@end
+
+@implementation TUISelectAvatarCollectionCell
+
+- (instancetype)initWithFrame:(CGRect)frame {
+    self = [super initWithFrame:frame];
+
+    if (self) {
+        self.imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height)];
+
+        [self.imageView setUserInteractionEnabled:YES];
+
+        self.imageView.layer.cornerRadius = [TUIConfig defaultConfig].avatarCornerRadius;
+
+        self.imageView.layer.borderWidth = 2;
+
+        self.imageView.layer.masksToBounds = YES;
+
+        [self.contentView addSubview:self.imageView];
+
+        [self.imageView addSubview:self.selectedView];
+
+        [self setupMaskView];
+    }
+
+    return self;
+}
+
+- (void)layoutSubviews {
+    [self updateCellView];
+
+    self.selectedView.frame = CGRectMake(self.imageView.frame.size.width - 16 - 4, 4, 16, 16);
+}
+
+- (void)updateCellView {
+    [self updateSelectedUI];
+    [self updateImageView];
+    [self updateMaskView];
+}
+
+- (void)updateSelectedUI {
+    if (self.cardItem.isSelect) {
+        self.imageView.layer.borderColor = TIMCommonDynamicColor(@"", @"#006EFF").CGColor;
+        self.selectedView.hidden = NO;
+    } else {
+        if (self.cardItem.isDefaultBackgroundItem) {
+            self.imageView.layer.borderColor = [[UIColor grayColor] colorWithAlphaComponent:0.1].CGColor;
+        } else {
+            self.imageView.layer.borderColor = UIColor.clearColor.CGColor;
+        }
+        self.selectedView.hidden = YES;
+    }
+}
+
+- (void)updateImageView {
+    if (self.cardItem.isGroupGridAvatar) {
+        [self updateNormalGroupGridAvatar];
+    } else {
+        [self.imageView sd_setImageWithURL:[NSURL URLWithString:self.cardItem.posterUrlStr]
+                          placeholderImage:TIMCommonBundleThemeImage(@"default_c2c_head_img", @"default_c2c_head_img")];
+    }
+}
+- (void)updateMaskView {
+    if (self.cardItem.isDefaultBackgroundItem) {
+        self.maskView.hidden = NO;
+        self.maskView.frame = CGRectMake(0, self.imageView.frame.size.height - 28, self.imageView.frame.size.width, 28);
+        [self.descLabel sizeToFit];
+        self.descLabel.tui_mm_center();
+    } else {
+        self.maskView.hidden = YES;
+    }
+}
+
+- (void)updateNormalGroupGridAvatar {
+    if (TUIConfig.defaultConfig.enableGroupGridAvatar && self.cardItem.cacheGroupGridAvatarImage) {
+        [self.imageView sd_setImageWithURL:nil placeholderImage:self.cardItem.cacheGroupGridAvatarImage];
+    }
+}
+
+- (void)setupMaskView {
+    self.maskView = [[UIView alloc] initWithFrame:CGRectZero];
+    self.maskView.backgroundColor = [UIColor tui_colorWithHex:@"cccccc"];
+    [self.imageView addSubview:self.maskView];
+    self.descLabel = [[UILabel alloc] initWithFrame:CGRectZero];
+    self.descLabel.text = TIMCommonLocalizableString(TUIKitDefaultBackground);
+    self.descLabel.textColor = [UIColor whiteColor];
+    self.descLabel.font = [UIFont systemFontOfSize:13];
+    [self.maskView addSubview:self.descLabel];
+    [self.descLabel sizeToFit];
+    self.descLabel.tui_mm_center();
+}
+
+- (void)setCardItem:(TUISelectAvatarCardItem *)cardItem {
+    _cardItem = cardItem;
+}
+
+- (UIImageView *)selectedView {
+    if (!_selectedView) {
+        _selectedView = [[UIImageView alloc] initWithFrame:CGRectZero];
+        _selectedView.image = [UIImage imageNamed:TIMCommonImagePath(@"icon_avatar_selected")];
+    }
+    return _selectedView;
+}
+
+@end
+
+@interface TUISelectAvatarController () <UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout>
+
+@property(nonatomic, strong) TUINaviBarIndicatorView *titleView;
+@property(nonatomic, strong) UICollectionView *collectionView;
+@property(nonatomic, strong) NSMutableArray *dataArr;
+@property(nonatomic, strong) TUISelectAvatarCardItem *currentSelectCardItem;
+@property(nonatomic, strong) UIButton *rightButton;
+
+@end
+
+@implementation TUISelectAvatarController
+
+static NSString *const reuseIdentifier = @"TUISelectAvatarCollectionCell";
+
+- (instancetype)init {
+    if (self = [super init]) {
+        self.selectAvatarType = TUISelectAvatarTypeUserAvatar;
+    }
+    return self;
+}
+- (void)viewDidLoad {
+    [super viewDidLoad];
+
+    UICollectionViewFlowLayout *flowLayout = [[UICollectionViewFlowLayout alloc] init];
+    [flowLayout setScrollDirection:UICollectionViewScrollDirectionVertical];
+    CGRect frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height);
+    self.collectionView = [[UICollectionView alloc] initWithFrame:frame collectionViewLayout:flowLayout];
+    [self.view addSubview:self.collectionView];
+
+    self.collectionView.backgroundColor = TIMCommonDynamicColor(@"controller_bg_color", @"#F2F3F5");
+
+    self.collectionView.dataSource = self;
+    self.collectionView.delegate = self;
+
+    // Register cell classes
+    [self.collectionView registerClass:[TUISelectAvatarCollectionCell class] forCellWithReuseIdentifier:reuseIdentifier];
+
+    // Do any additional setup after loading the view.
+    [self setupNavigator];
+
+    self.dataArr = [NSMutableArray arrayWithCapacity:3];
+
+    [self loadData];
+}
+- (void)loadData {
+    if (self.selectAvatarType == TUISelectAvatarTypeUserAvatar) {
+        for (int i = 0; i < UserAvatarCount; i++) {
+            TUISelectAvatarCardItem *cardItem = [self creatCardItemByURL:UserAvatarURL(i + 1)];
+            [self.dataArr addObject:cardItem];
+        }
+    } else if (self.selectAvatarType == TUISelectAvatarTypeGroupAvatar) {
+        if (TUIConfig.defaultConfig.enableGroupGridAvatar && self.cacheGroupGridAvatarImage) {
+            TUISelectAvatarCardItem *cardItem = [self creatGroupGridAvatarCardItem];
+            [self.dataArr addObject:cardItem];
+        }
+
+        for (int i = 0; i < GroupAvatarCount; i++) {
+            TUISelectAvatarCardItem *cardItem = [self creatCardItemByURL:GroupAvatarURL(i + 1)];
+            [self.dataArr addObject:cardItem];
+        }
+    } else if (self.selectAvatarType == TUISelectAvatarTypeConversationBackGroundCover) {
+        TUISelectAvatarCardItem *cardItem = [self creatCleanCardItem];
+        [self.dataArr addObject:cardItem];
+        for (int i = 0; i < BackGroundCoverCount; i++) {
+            TUISelectAvatarCardItem *cardItem = [self creatCardItemByURL:BackGroundCoverURL(i + 1) fullUrl:BackGroundCoverURL_full(i + 1)];
+            [self.dataArr addObject:cardItem];
+        }
+    }
+
+    else {
+        for (int i = 0; i < Community_coverCount; i++) {
+            TUISelectAvatarCardItem *cardItem = [self creatCardItemByURL:Community_coverURL(i + 1)];
+            [self.dataArr addObject:cardItem];
+        }
+    }
+    [self.collectionView reloadData];
+}
+
+- (TUISelectAvatarCardItem *)creatCardItemByURL:(NSString *)urlStr {
+    TUISelectAvatarCardItem *cardItem = [[TUISelectAvatarCardItem alloc] init];
+    cardItem.posterUrlStr = urlStr;
+    cardItem.isSelect = NO;
+    if ([cardItem.posterUrlStr isEqualToString:self.profilFaceURL]) {
+        cardItem.isSelect = YES;
+        self.currentSelectCardItem = cardItem;
+    }
+    return cardItem;
+}
+
+- (TUISelectAvatarCardItem *)creatGroupGridAvatarCardItem {
+    TUISelectAvatarCardItem *cardItem = [[TUISelectAvatarCardItem alloc] init];
+    cardItem.posterUrlStr = nil;
+    cardItem.isSelect = NO;
+    cardItem.isGroupGridAvatar = YES;
+    cardItem.createGroupType = self.createGroupType;
+    cardItem.cacheGroupGridAvatarImage = self.cacheGroupGridAvatarImage;
+    if (!self.profilFaceURL) {
+        cardItem.isSelect = YES;
+        self.currentSelectCardItem = cardItem;
+    }
+    return cardItem;
+}
+
+- (TUISelectAvatarCardItem *)creatCardItemByURL:(NSString *)urlStr fullUrl:(NSString *)fullUrl {
+    TUISelectAvatarCardItem *cardItem = [[TUISelectAvatarCardItem alloc] init];
+    cardItem.posterUrlStr = urlStr;
+    cardItem.fullUrlStr = fullUrl;
+    cardItem.isSelect = NO;
+    if ([cardItem.posterUrlStr isEqualToString:self.profilFaceURL] || [cardItem.fullUrlStr isEqualToString:self.profilFaceURL]) {
+        cardItem.isSelect = YES;
+        self.currentSelectCardItem = cardItem;
+    }
+    return cardItem;
+}
+
+- (TUISelectAvatarCardItem *)creatCleanCardItem {
+    TUISelectAvatarCardItem *cardItem = [[TUISelectAvatarCardItem alloc] init];
+    cardItem.posterUrlStr = nil;
+    cardItem.isSelect = NO;
+    cardItem.isDefaultBackgroundItem = YES;
+    if (self.profilFaceURL.length == 0) {
+        cardItem.isSelect = YES;
+        self.currentSelectCardItem = cardItem;
+    }
+    return cardItem;
+}
+- (void)setupNavigator {
+    _titleView = [[TUINaviBarIndicatorView alloc] init];
+    self.navigationItem.titleView = _titleView;
+    self.navigationItem.title = @"";
+
+    if (self.selectAvatarType == TUISelectAvatarTypeCover) {
+        [self.titleView setTitle:TIMCommonLocalizableString(TUIKitChooseCover)];
+    } else if (self.selectAvatarType == TUISelectAvatarTypeConversationBackGroundCover) {
+        [self.titleView setTitle:TIMCommonLocalizableString(TUIKitChooseBackground)];
+    } else {
+        [self.titleView setTitle:TIMCommonLocalizableString(TUIKitChooseAvatar)];
+    }
+
+    self.rightButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 30, 30)];
+    [self.rightButton setTitle:TIMCommonLocalizableString(Save) forState:UIControlStateNormal];
+    [self.rightButton addTarget:self action:@selector(rightBarButtonClick) forControlEvents:UIControlEventTouchUpInside];
+    self.rightButton.titleLabel.font = [UIFont fontWithName:@"PingFangSC-Regular" size:14];
+    [self.rightButton setTitleColor:[UIColor grayColor] forState:UIControlStateNormal];
+
+    UIBarButtonItem *rightItem = [[UIBarButtonItem alloc] initWithCustomView:self.rightButton];
+    self.navigationItem.rightBarButtonItems = @[ rightItem ];
+}
+
+- (void)setCurrentSelectCardItem:(TUISelectAvatarCardItem *)currentSelectCardItem {
+    _currentSelectCardItem = currentSelectCardItem;
+    if (_currentSelectCardItem) {
+        [self.rightButton setTitleColor:TIMCommonDynamicColor(@"", @"#006EFF") forState:UIControlStateNormal];
+    } else {
+        [self.rightButton setTitleColor:[UIColor grayColor] forState:UIControlStateNormal];
+    }
+}
+- (void)rightBarButtonClick {
+    if (!self.currentSelectCardItem) {
+        return;
+    }
+
+    if (self.selectCallBack) {
+        if (self.selectAvatarType == TUISelectAvatarTypeConversationBackGroundCover) {
+            if (IS_NOT_EMPTY_NSSTRING(self.currentSelectCardItem.fullUrlStr)) {
+                dispatch_async(dispatch_get_main_queue(), ^{
+                  [TUITool makeToastActivity];
+                });
+                @weakify(self);
+                [[SDWebImagePrefetcher sharedImagePrefetcher]
+                    prefetchURLs:@[ [NSURL URLWithString:self.currentSelectCardItem.fullUrlStr] ]
+                        progress:nil
+                       completed:^(NSUInteger noOfFinishedUrls, NSUInteger noOfSkippedUrls) {
+                         @strongify(self);
+                         dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
+                           dispatch_async(dispatch_get_main_queue(), ^{
+                             [TUITool hideToastActivity];
+                             [TUITool makeToast:TIMCommonLocalizableString(TUIKitChooseBackgroundSuccess)];
+                             if (self.selectCallBack) {
+                                 self.selectCallBack(self.currentSelectCardItem.fullUrlStr);
+                                 [self.navigationController popViewControllerAnimated:YES];
+                             }
+                           });
+                         });
+                       }];
+            } else {
+                [TUITool makeToast:TIMCommonLocalizableString(TUIKitChooseBackgroundSuccess)];
+                self.selectCallBack(self.currentSelectCardItem.fullUrlStr);
+                [self.navigationController popViewControllerAnimated:YES];
+            }
+        } else {
+            self.selectCallBack(self.currentSelectCardItem.posterUrlStr);
+            [self.navigationController popViewControllerAnimated:YES];
+        }
+    }
+}
+
+#pragma mark - UICollectionViewDelegateFlowLayout
+- (CGSize)collectionView:(UICollectionView *)collectionView
+                    layout:(UICollectionViewLayout *)collectionViewLayout
+    sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
+    CGFloat margin = 15;
+
+    CGFloat padding = 13;
+
+    int rowCount = 4.0;
+
+    if (self.selectAvatarType == TUISelectAvatarTypeCover || self.selectAvatarType == TUISelectAvatarTypeConversationBackGroundCover) {
+        rowCount = 2.0;
+    } else {
+        rowCount = 4.0;
+    }
+
+    CGFloat width = (self.view.frame.size.width - 2 * margin - (rowCount - 1) * padding) / rowCount;
+
+    CGFloat height = 77;
+    if (self.selectAvatarType == TUISelectAvatarTypeConversationBackGroundCover) {
+        height = 125;
+    }
+
+    return CGSizeMake(width, height);
+}
+
+- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView
+                        layout:(UICollectionViewLayout *)collectionViewLayout
+        insetForSectionAtIndex:(NSInteger)section {
+    return UIEdgeInsetsMake(24, 15, 0, 15);
+}
+
+#pragma mark <UICollectionViewDataSource>
+
+- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
+    return 1;
+}
+
+- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
+    return [self.dataArr count];
+}
+
+- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
+    TUISelectAvatarCollectionCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:reuseIdentifier forIndexPath:indexPath];
+    // Configure the cell
+
+    if (indexPath.row < self.dataArr.count) {
+        cell.cardItem = self.dataArr[indexPath.row];
+    }
+
+    return cell;
+}
+
+#pragma mark <UICollectionViewDelegate>
+
+- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
+    [self recoverSelectedStatus];
+
+    TUISelectAvatarCollectionCell *cell = (TUISelectAvatarCollectionCell *)[self.collectionView cellForItemAtIndexPath:indexPath];
+
+    if (cell == nil) {
+        [self.collectionView layoutIfNeeded];
+        cell = (TUISelectAvatarCollectionCell *)[self.collectionView cellForItemAtIndexPath:indexPath];
+    }
+    if (self.currentSelectCardItem == cell.cardItem) {
+        self.currentSelectCardItem = nil;
+    } else {
+        cell.cardItem.isSelect = YES;
+        [cell updateSelectedUI];
+        self.currentSelectCardItem = cell.cardItem;
+    }
+}
+
+- (void)recoverSelectedStatus {
+    NSInteger index = 0;
+    for (TUISelectAvatarCardItem *card in self.dataArr) {
+        if (self.currentSelectCardItem == card) {
+            card.isSelect = NO;
+            break;
+        }
+        index++;
+    }
+
+    NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
+    TUISelectAvatarCollectionCell *cell = (TUISelectAvatarCollectionCell *)[self.collectionView cellForItemAtIndexPath:indexPath];
+
+    if (cell == nil) {
+        [self.collectionView layoutIfNeeded];
+        cell = (TUISelectAvatarCollectionCell *)[self.collectionView cellForItemAtIndexPath:indexPath];
+    }
+    [cell updateSelectedUI];
+}
+@end
+
+/////////////////////////////////////////////////////////////////////////////////
+//
+//                          TUICommonAvatarCell & Data
+//
+/////////////////////////////////////////////////////////////////////////////////
+@implementation TUICommonAvatarCellData
+- (instancetype)init {
+    self = [super init];
+    if (self) {
+        _avatarImage = DefaultAvatarImage;
+    }
+    return self;
+}
+
+- (CGFloat)heightOfWidth:(CGFloat)width {
+    return TPersonalCommonCell_Image_Size.height + 2 * TPersonalCommonCell_Margin;
+}
+
+@end
+
+@interface TUICommonAvatarCell ()
+@property TUICommonAvatarCellData *avatarData;
+@end
+
+@implementation TUICommonAvatarCell
+- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
+    if (self = [super initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:reuseIdentifier]) {
+        [self setupViews];
+        self.selectionStyle = UITableViewCellSelectionStyleNone;
+    }
+    return self;
+}
+
+- (void)fillWithData:(TUICommonAvatarCellData *)avatarData {
+    [super fillWithData:avatarData];
+
+    self.avatarData = avatarData;
+
+    RAC(_keyLabel, text) = [RACObserve(avatarData, key) takeUntil:self.rac_prepareForReuseSignal];
+    RAC(_valueLabel, text) = [RACObserve(avatarData, value) takeUntil:self.rac_prepareForReuseSignal];
+    @weakify(self);
+    [[RACObserve(avatarData, avatarUrl) takeUntil:self.rac_prepareForReuseSignal] subscribeNext:^(NSURL *x) {
+      @strongify(self);
+      [self.avatar sd_setImageWithURL:x placeholderImage:self.avatarData.avatarImage];
+    }];
+
+    if (avatarData.showAccessory) {
+        self.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
+    } else {
+        self.accessoryType = UITableViewCellAccessoryNone;
+    }
+    
+    // tell constraints they need updating
+    [self setNeedsUpdateConstraints];
+
+    // update constraints now so we can animate the change
+    [self updateConstraintsIfNeeded];
+
+    [self layoutIfNeeded];
+    
+}
+
+- (void)setupViews {
+    _avatar = [[UIImageView alloc] initWithFrame:CGRectZero];
+    _avatar.contentMode = UIViewContentModeScaleAspectFit;
+
+    [self addSubview:_avatar];
+
+    _keyLabel = self.textLabel;
+    _valueLabel = self.detailTextLabel;
+
+    [self addSubview:_keyLabel];
+    [self addSubview:_valueLabel];
+
+    self.keyLabel.textColor = TIMCommonDynamicColor(@"form_key_text_color", @"#444444");
+    self.valueLabel.textColor = TIMCommonDynamicColor(@"form_value_text_color", @"#000000");
+
+    self.selectionStyle = UITableViewCellSelectionStyleNone;
+}
+
++ (BOOL)requiresConstraintBasedLayout {
+    return YES;
+}
+
+// this is Apple's recommended place for adding/updating constraints
+- (void)updateConstraints {
+    [super updateConstraints];
+    CGSize headSize = TPersonalCommonCell_Image_Size;    
+    [self.avatar mas_remakeConstraints:^(MASConstraintMaker *make) {
+        make.size.mas_equalTo(headSize);
+        if (self.avatarData.showAccessory) {
+            make.trailing.mas_equalTo(self.contentView.mas_trailing).mas_offset(-10);
+        }
+        else {
+            make.trailing.mas_equalTo(self.contentView.mas_trailing).mas_offset(-20);
+        }
+        make.centerY.mas_equalTo(self);
+    }];
+    
+    if ([TUIConfig defaultConfig].avatarType == TAvatarTypeRounded) {
+        self.avatar.layer.masksToBounds = YES;
+        self.avatar.layer.cornerRadius = headSize.height / 2;
+    } else if ([TUIConfig defaultConfig].avatarType == TAvatarTypeRadiusCorner) {
+        self.avatar.layer.masksToBounds = YES;
+        self.avatar.layer.cornerRadius = [TUIConfig defaultConfig].avatarCornerRadius;
+    }
+}
+- (void)layoutSubviews {
+    [super layoutSubviews];
+
+    
+}
+@end
+
+/////////////////////////////////////////////////////////////////////////////////
+//
+//                             TUIConversationGroupItem
+//
+/////////////////////////////////////////////////////////////////////////////////
+NSUInteger kConversationMarkStarType = V2TIM_CONVERSATION_MARK_TYPE_STAR;
+@implementation TUIConversationGroupItem
+- (instancetype)init {
+    self = [super init];
+    if (self) {
+        self.unreadCount = 0;
+        self.groupIndex = 0;
+        self.isShow = YES;
+    }
+    return self;
+}
+@end
+
+@implementation TUISendMessageAppendParams
++ (instancetype)defaultConfig {
+    TUISendMessageAppendParams *params = [[TUISendMessageAppendParams alloc] init];
+    params.priority = V2TIM_PRIORITY_NORMAL;
+    return params;
+}
+
+@end

+ 44 - 0
ThirdParty/TUIKit/TIMCommon/CommonModel/TIMConfig.h

@@ -0,0 +1,44 @@
+//
+//  TIMConfig.h
+//  Pods
+//
+//  Created by cologne on 2023/3/14.
+//  Copyright © 2023 Tencent. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+#import <TUICore/TUIConfig.h>
+#import "TIMCommonModel.h"
+#import "TIMDefine.h"
+
+@class TUIFaceCellData;
+@class TUIFaceGroup;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface TIMConfig : NSObject
+
++ (TIMConfig *)defaultConfig;
+/**
+ * In respect for the copyright of the emoji design, the Chat Demo/TUIKit project does not include the cutouts of large emoji elements. Please replace them
+ * with your own designed or copyrighted emoji packs before the official launch for commercial use. The default small yellow face emoji pack is copyrighted by
+ * Tencent Cloud and can be authorized for a fee. If you wish to obtain authorization, please submit a ticket to contact us.
+ *
+ * submit a ticket url:https://console.cloud.tencent.com/workorder/category?level1_id=29&level2_id=40&source=14&data_title=%E5%8D%B3%E6%97%B6%E9%80%9A%E4%BF%A1%20IM&step=1 (China mainland)
+ * submit a ticket url:https://console.tencentcloud.com/workorder/category?level1_id=29&level2_id=40&source=14&data_title=Chat&step=1 (Other regions)
+ */
+@property(nonatomic, strong) NSArray<TUIFaceGroup *> *faceGroups;
+
+/**
+ * 
+ * The list of emoticons displayed after long-pressing the message on the chat interface
+ */
+@property(nonatomic, strong) NSArray<TUIFaceGroup *> *chatPopDetailGroups;
+
+
+@property(nonatomic, assign) BOOL enableMessageBubble;
+
++ (BOOL)isClassicEntrance;
+@end
+
+NS_ASSUME_NONNULL_END

+ 80 - 0
ThirdParty/TUIKit/TIMCommon/CommonModel/TIMConfig.m

@@ -0,0 +1,80 @@
+//
+//  TIMConfig.m
+//  Pods
+//
+//  Created by cologne on 2023/3/14.
+//  Copyright © 2023 Tencent. All rights reserved.
+//
+
+#import "TIMConfig.h"
+#import "TIMCommonMediator.h"
+#import "TUIEmojiMeditorProtocol.h"
+#define kTUIKitFirstInitAppStyleID @"Classic";  // Classic / Minimalist
+
+typedef NS_OPTIONS(NSInteger, emojiFaceType) {
+    emojiFaceTypeKeyBoard = 1 << 0,
+    emojiFaceTypePopDetail = 1 << 1,
+};
+
+@interface TIMConfig ()
+
+@end
+
+@implementation TIMConfig
+
++ (void)load {
+    TUIRegisterThemeResourcePath(TIMCommonThemePath, TUIThemeModuleTIMCommon);
+}
+
+- (id)init {
+    self = [super init];
+    if (self) {
+        self.enableMessageBubble = YES;
+    }
+    return self;
+}
+
++ (id)defaultConfig {
+    static dispatch_once_t onceToken;
+    static TIMConfig *config;
+    dispatch_once(&onceToken, ^{
+      config = [[TIMConfig alloc] init];
+    });
+    return config;
+}
+
+- (NSArray<TUIFaceGroup *> *)faceGroups {
+    id<TUIEmojiMeditorProtocol> service = [[TIMCommonMediator share] getObject:@protocol(TUIEmojiMeditorProtocol)];
+    return [service getFaceGroup];
+}
+
+- (NSArray<TUIFaceGroup *> *)chatPopDetailGroups {
+    id<TUIEmojiMeditorProtocol> service = [[TIMCommonMediator share] getObject:@protocol(TUIEmojiMeditorProtocol)];
+    return [service getChatPopDetailGroups];
+}
+
++ (NSString *)getCurrentStyleSelectID {
+    NSString *styleID = [[NSUserDefaults standardUserDefaults] objectForKey:@"StyleSelectkey"];
+    if (IS_NOT_EMPTY_NSSTRING(styleID)) {
+        return styleID;
+    } else {
+        // First Init
+        NSString *initStyleID = kTUIKitFirstInitAppStyleID;
+        [[NSUserDefaults standardUserDefaults] setValue:initStyleID forKey:@"StyleSelectkey"];
+        [NSUserDefaults.standardUserDefaults synchronize];
+        return initStyleID;
+    }
+}
+
++ (BOOL)isClassicEntrance {
+    NSString *styleID = [self.class getCurrentStyleSelectID];
+    if ([styleID isKindOfClass:NSString.class]) {
+        if (styleID.length > 0) {
+            if ([styleID isEqualToString:@"Classic"]) {
+                return YES;
+            }
+        }
+    }
+    return NO;
+}
+@end

+ 26 - 0
ThirdParty/TUIKit/TIMCommon/CommonModel/TIMDefine.h

@@ -0,0 +1,26 @@
+//
+//  TIMDefine.h
+//  Pods
+//
+//  Created by cologne on 2023/3/14.
+//  Copyright © 2023 Tencent. All rights reserved.
+//
+
+#ifndef TIMDefine_h
+#define TIMDefine_h
+
+#import <ReactiveObjC/ReactiveObjC.h>
+#import <TUICore/TUIDefine.h>
+#import <Masonry/Masonry.h>
+#import "TIMConfig.h"
+#import "TIMCommonModel.h"
+#import "TIMRTLUtil.h"
+
+#define kEnableAllRotationOrientationNotification @"kEnableAllRotationOrientationNotification"
+#define kDisableAllRotationOrientationNotification @"kDisableAllRotationOrientationNotification"
+#define TUIMessageMediaViewDeviceOrientationChangeNotification @"TUIMessageMediaViewDeviceOrientationChangeNotification"
+
+//Provide customers with the ability to modify the default emoji expression size in various input behaviors
+#define kTIMDefaultEmojiSize CGSizeMake(23, 23)
+
+#endif /* TIMDefine_h */

+ 20 - 0
ThirdParty/TUIKit/TIMCommon/CommonModel/TIMGroupInfo+TUIDataProvider.h

@@ -0,0 +1,20 @@
+
+//  Created by Tencent on 2023/06/09.
+//  Copyright © 2023 Tencent. All rights reserved.
+#import <Foundation/Foundation.h>
+@import ImSDK_Plus;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface V2TIMGroupInfo (TUIDataProvider)
+
+- (BOOL)isMeOwner;
+- (BOOL)isPrivate;
+- (BOOL)canInviteMember;
+- (BOOL)canRemoveMember;
+- (BOOL)canDismissGroup;
+- (BOOL)canSupportSetAdmain;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 46 - 0
ThirdParty/TUIKit/TIMCommon/CommonModel/TIMGroupInfo+TUIDataProvider.m

@@ -0,0 +1,46 @@
+
+//  Created by Tencent on 2023/06/09.
+//  Copyright © 2023 Tencent. All rights reserved.
+
+#import <TUICore/TUIGlobalization.h>
+#import "TIMGroupInfo+TUIDataProvider.h"
+
+@implementation V2TIMGroupInfo (TUIDataProvider)
+
+- (BOOL)isMeOwner {
+    return [self.owner isEqualToString:[[V2TIMManager sharedInstance] getLoginUser]] || (self.role == V2TIM_GROUP_MEMBER_ROLE_ADMIN);
+}
+
+- (BOOL)isPrivate {
+    return [self.groupType isEqualToString:@"Work"];
+}
+
+- (BOOL)canInviteMember {
+    return self.groupApproveOpt != V2TIM_GROUP_ADD_FORBID;
+}
+
+- (BOOL)canRemoveMember {
+    return [self isMeOwner] && (self.memberCount > 1);
+}
+
+- (BOOL)canDismissGroup {
+    if ([self isPrivate]) {
+        return NO;
+    } else {
+        if ([self.owner isEqualToString:[[V2TIMManager sharedInstance] getLoginUser]] || (self.role == V2TIM_GROUP_MEMBER_ROLE_SUPER)) {
+            return YES;
+        } else {
+            return NO;
+        }
+    }
+}
+
+- (BOOL)canSupportSetAdmain {
+    BOOL isMeSuper = [self.owner isEqualToString:[[V2TIMManager sharedInstance] getLoginUser]] || (self.role == V2TIM_GROUP_MEMBER_ROLE_SUPER);
+
+    BOOL isCurrentGroupTypeSupportSetAdmain = ([self.groupType isEqualToString:@"Public"] || [self.groupType isEqualToString:@"Meeting"] ||
+                                               [self.groupType isEqualToString:@"Community"] || [self.groupType isEqualToString:@"Private"]);
+
+    return isMeSuper && isCurrentGroupTypeSupportSetAdmain && (self.memberCount > 1);
+}
+@end

+ 19 - 0
ThirdParty/TUIKit/TIMCommon/CommonModel/TIMInputViewMoreActionProtocol.h

@@ -0,0 +1,19 @@
+//
+//  TIMInputViewMoreActionProtocol.h
+//  TIMCommon
+//
+//  Created by wyl on 2023/5/5.
+//  Copyright © 2023 Tencent. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@protocol TIMInputViewMoreActionProtocol <NSObject>
+
+- (void)sendMessage:(V2TIMMessage *)message;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 33 - 0
ThirdParty/TUIKit/TIMCommon/CommonModel/TIMPopActionProtocol.h

@@ -0,0 +1,33 @@
+//
+//  TIMPopActionProtocol.h
+//  TIMCommon
+//
+//  Created by wyl on 2023/4/3.
+//  Copyright © 2023 Tencent. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@protocol TIMPopActionProtocol <NSObject>
+
+- (void)onDelete:(id)sender;
+
+- (void)onCopyMsg:(id)sender;
+
+- (void)onRevoke:(id)sender;
+
+- (void)onReSend:(id)sender;
+
+- (void)onMulitSelect:(id)sender;
+
+- (void)onForward:(id)sender;
+
+- (void)onReply:(id)sender;
+
+- (void)onReference:(id)sender;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 51 - 0
ThirdParty/TUIKit/TIMCommon/CommonModel/TIMRTLUtil.h

@@ -0,0 +1,51 @@
+//
+//  TIMRTLUtil.h
+//  TIMCommon
+//
+//  Created by cologne on 2023/7/21.
+//  Copyright © 2023 Tencent. All rights reserved
+//
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+
+@interface TIMRTLUtil : NSObject
+
+@end
+
+@interface UIView (TUIRTL)
+- (void)resetFrameToFitRTL;
+
+@end
+
+@interface UIImage (TUIRTL)
+
+- (UIImage *_Nonnull)checkOverturn;
+- (UIImage *)rtl_imageFlippedForRightToLeftLayoutDirection;
+@end
+
+typedef NS_ENUM(NSUInteger, TUITextRTLAlignment) {
+    TUITextRTLAlignmentUndefine,
+    TUITextRTLAlignmentLeading,
+    TUITextRTLAlignmentTrailing,
+    TUITextRTLAlignmentCenter,
+};
+@interface UILabel (TUIRTL)
+@property (nonatomic, assign) TUITextRTLAlignment rtlAlignment;
+@end
+
+@interface NSMutableAttributedString (TUIRTL)
+@property (nonatomic, assign) TUITextRTLAlignment rtlAlignment;
+@end
+
+BOOL isRTLString(NSString *string);
+NSString * rtlString(NSString *string);
+NSAttributedString *rtlAttributeString(NSAttributedString *attributeString ,NSTextAlignment textAlignment );
+UIEdgeInsets rtlEdgeInsetsWithInsets(UIEdgeInsets insets);
+
+@interface TUICollectionRTLFitFlowLayout : UICollectionViewFlowLayout
+
+@end
+NS_ASSUME_NONNULL_END

+ 291 - 0
ThirdParty/TUIKit/TIMCommon/CommonModel/TIMRTLUtil.m

@@ -0,0 +1,291 @@
+//
+//  TIMRTLUtil.m
+//  TIMCommon
+//
+//  Created by cologne on 2023/7/21.
+//  Copyright © 2023 Tencent. All rights reserved
+//
+
+#import "TIMRTLUtil.h"
+#import <objc/runtime.h>
+#import <TUICore/TUIGlobalization.h>
+
+@implementation TIMRTLUtil
+
+@end
+
+
+@interface UIView (TUIRTL)
+
+@end
+@implementation UIView (TUIRTL)
+- (void)setRTLFrame:(CGRect)frame width:(CGFloat)width {
+    if (isRTL()) {
+        if (self.superview == nil) {
+            NSAssert(0, @"must invoke after have superView");
+        }
+        CGFloat x = width - frame.origin.x - frame.size.width;
+        frame.origin.x = x;
+    }
+    self.frame = frame;
+}
+
+- (void)setRTLFrame:(CGRect)frame {
+    [self setRTLFrame:frame width:self.superview.frame.size.width];
+}
+
+- (void)resetFrameToFitRTL {
+    [self setRTLFrame:self.frame];
+}
+
+@end
+
+@interface UIImage (TUIRTL)
+
+@end
+@implementation UIImage (TUIRTL)
+- (UIImage *_Nonnull)checkOverturn{
+    if (isRTL()) {
+        UIGraphicsBeginImageContextWithOptions(self.size, false, self.scale);
+        CGContextRef bitmap = UIGraphicsGetCurrentContext();
+        CGContextTranslateCTM(bitmap, self.size.width / 2, self.size.height / 2);
+        CGContextScaleCTM(bitmap, -1.0, -1.0);
+        CGContextTranslateCTM(bitmap, -self.size.width / 2, -self.size.height / 2);
+        CGContextDrawImage(bitmap, CGRectMake(0, 0, self.size.width, self.size.height), self.CGImage);
+        UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
+        return image;
+    }
+    return self;
+}
+- (UIImage *)_imageFlippedForRightToLeftLayoutDirection {
+    if (isRTL()) {
+        return [UIImage imageWithCGImage:self.CGImage
+                                   scale:self.scale
+                             orientation:UIImageOrientationUpMirrored];
+    }
+
+    return self;
+}
+
+- (UIImage *)rtl_imageFlippedForRightToLeftLayoutDirection {
+    if (isRTL()) {
+        if (@available(iOS 13.0, *)) {
+            UITraitCollection *const scaleTraitCollection = [UITraitCollection currentTraitCollection];
+            UITraitCollection *const darkUnscaledTraitCollection = [UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleDark];
+            UITraitCollection *const darkScaledTraitCollection =
+                [UITraitCollection traitCollectionWithTraitsFromCollections:@[ scaleTraitCollection, darkUnscaledTraitCollection ]];
+
+            UIImage *lightImg = [[self.imageAsset imageWithTraitCollection:[UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleLight]]
+                _imageFlippedForRightToLeftLayoutDirection];
+
+            UIImage *darkImage = [[self.imageAsset imageWithTraitCollection:[UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleDark]]
+                _imageFlippedForRightToLeftLayoutDirection];
+
+            UIImage *image =
+                [lightImg imageWithConfiguration:[self.configuration
+                                                     configurationWithTraitCollection:[UITraitCollection
+                                                                                          traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleLight]]];
+            [image.imageAsset registerImage:darkImage withTraitCollection:darkScaledTraitCollection];
+            return image;
+        } else {
+            return [UIImage imageWithCGImage:self.CGImage scale:self.scale orientation:UIImageOrientationUpMirrored];
+        }
+    }
+    return self;
+}
+
+@end
+@interface UINavigationController (TUIRTL)
+@end
+@implementation UINavigationController (TUIRTL)
++ (void)load {
+    Method oldMethod = class_getInstanceMethod(self, @selector(initWithRootViewController:));
+    Method newMethod = class_getInstanceMethod(self, @selector(rtl_initWithRootViewController:));
+    method_exchangeImplementations(oldMethod, newMethod);
+}
+
+- (instancetype)rtl_initWithRootViewController:(UIViewController *)rootViewController {
+    if ([self rtl_initWithRootViewController:rootViewController]) {
+        if (@available(iOS 9.0, *)) {
+            if (isRTL()) {
+                self.navigationBar.semanticContentAttribute = [UIView appearance].semanticContentAttribute;
+                self.view.semanticContentAttribute = [UIView appearance].semanticContentAttribute;
+            }
+        }
+    }
+    return self;
+}
+@end
+
+
+UIEdgeInsets rtlEdgeInsetsWithInsets(UIEdgeInsets insets) {
+    if (insets.left != insets.right && isRTL()) {
+        CGFloat temp = insets.left;
+        insets.left = insets.right;
+        insets.right = temp;
+    }
+    return insets;
+    
+}
+
+@implementation UIButton (TUIRTL)
+
+void swizzleInstanceMethod(Class cls, SEL originSelector, SEL swizzleSelector){
+    if (!cls) {
+        return;
+    }
+    /* if current class not exist selector, then get super*/
+    Method originalMethod = class_getInstanceMethod(cls, originSelector);
+    Method swizzledMethod = class_getInstanceMethod(cls, swizzleSelector);
+    
+    /* add selector if not exist, implement append with method */
+    if (class_addMethod(cls,
+                        originSelector,
+                        method_getImplementation(swizzledMethod),
+                        method_getTypeEncoding(swizzledMethod)) ) {
+        /* replace class instance method, added if selector not exist */
+        /* for class cluster , it always add new selector here */
+        class_replaceMethod(cls,
+                            swizzleSelector,
+                            method_getImplementation(originalMethod),
+                            method_getTypeEncoding(originalMethod));
+        
+    } else {
+        /* swizzleMethod maybe belong to super */
+        class_replaceMethod(cls,
+                            swizzleSelector,
+                            class_replaceMethod(cls,
+                                                originSelector,
+                                                method_getImplementation(swizzledMethod),
+                                                method_getTypeEncoding(swizzledMethod)),
+                            method_getTypeEncoding(originalMethod));
+    }
+}
++ (void)load
+{
+    swizzleInstanceMethod(self, @selector(setContentEdgeInsets:), @selector(rtl_setContentEdgeInsets:));
+    swizzleInstanceMethod(self, @selector(setImageEdgeInsets:), @selector(rtl_setImageEdgeInsets:));
+    swizzleInstanceMethod(self, @selector(setTitleEdgeInsets:), @selector(rtl_setTitleEdgeInsets:));
+}
+
+- (void)rtl_setContentEdgeInsets:(UIEdgeInsets)contentEdgeInsets {
+    [self rtl_setContentEdgeInsets:rtlEdgeInsetsWithInsets(contentEdgeInsets)];
+}
+
+- (void)rtl_setImageEdgeInsets:(UIEdgeInsets)imageEdgeInsets {
+    [self rtl_setImageEdgeInsets:rtlEdgeInsetsWithInsets(imageEdgeInsets)];
+}
+
+- (void)rtl_setTitleEdgeInsets:(UIEdgeInsets)titleEdgeInsets {
+    [self rtl_setTitleEdgeInsets:rtlEdgeInsetsWithInsets(titleEdgeInsets)];
+}
+
+
+@end
+
+
+@implementation UILabel (TUIRTL)
+
+- (void)setRtlAlignment:(TUITextRTLAlignment)rtlAlignment {
+    objc_setAssociatedObject(self, @selector(rtlAlignment), @(rtlAlignment), OBJC_ASSOCIATION_ASSIGN);
+    switch (rtlAlignment) {
+        case TUITextRTLAlignmentLeading:
+            self.textAlignment = (isRTL() ? NSTextAlignmentRight : NSTextAlignmentLeft);
+            break;
+        case TUITextRTLAlignmentTrailing:
+            self.textAlignment = (isRTL() ? NSTextAlignmentLeft : NSTextAlignmentRight);
+            break;
+        case TUITextRTLAlignmentCenter:
+            self.textAlignment = NSTextAlignmentCenter;
+        case TUITextRTLAlignmentUndefine:
+            break;
+        default:
+            break;
+    }
+}
+
+- (TUITextRTLAlignment)rtlAlignment {
+    NSNumber *identifier = objc_getAssociatedObject(self, @selector(rtlAlignment));
+    if (identifier) {
+        return identifier.integerValue;
+    }
+    return TUITextRTLAlignmentUndefine;
+}
+@end
+
+@implementation NSMutableAttributedString (TUIRTL)
+- (void)setRtlAlignment:(TUITextRTLAlignment)rtlAlignment {
+    switch (rtlAlignment) {
+        case TUITextRTLAlignmentLeading:
+            self.rtlAlignment = (isRTL() ? NSTextAlignmentRight : NSTextAlignmentLeft);
+            break;
+        case TUITextRTLAlignmentTrailing:
+            self.rtlAlignment = (isRTL() ? NSTextAlignmentLeft : NSTextAlignmentRight);
+            break;
+        case TUITextRTLAlignmentCenter:
+            self.rtlAlignment = NSTextAlignmentCenter;
+        case TUITextRTLAlignmentUndefine:
+            break;
+        default:
+            break;
+    }
+}
+@end
+
+BOOL isRTLString(NSString *string) {
+    if ([string hasPrefix:@"\u202B"] || [string hasPrefix:@"\u202A"]) {
+        return YES;
+    }
+    return NO;
+}
+
+NSString * rtlString(NSString *string) {
+    if (string.length == 0 || isRTLString(string)) {
+        return string;
+    }
+    if (isRTL()) {
+        string = [@"\u202B" stringByAppendingString:string];
+    } else {
+        string = [@"\u202A" stringByAppendingString:string];
+    }
+    return string;
+}
+
+NSAttributedString *rtlAttributeString(NSAttributedString *attributeString ,NSTextAlignment textAlignment ){
+    if (attributeString.length == 0) {
+        return attributeString;
+    }
+    NSRange range;
+    NSDictionary *originAttributes = [attributeString attributesAtIndex:0 effectiveRange:&range];
+    NSParagraphStyle *style = [originAttributes objectForKey:NSParagraphStyleAttributeName];
+
+    if (style && isRTLString(attributeString.string)) {
+        return attributeString;
+    }
+
+    NSMutableDictionary *attributes = originAttributes ? [originAttributes mutableCopy] : [NSMutableDictionary new];
+    if (!style) {
+        NSMutableParagraphStyle *mutableParagraphStyle = [[NSMutableParagraphStyle alloc] init];
+        UILabel *test = [UILabel new];
+        test.textAlignment = textAlignment;
+        mutableParagraphStyle.alignment = test.textAlignment;
+        style = mutableParagraphStyle;
+        [attributes setValue:mutableParagraphStyle forKey:NSParagraphStyleAttributeName];
+    }
+    NSString *string = rtlString(attributeString.string);
+    return [[NSAttributedString alloc] initWithString:string attributes:attributes];
+}
+
+@implementation TUICollectionRTLFitFlowLayout
+- (UIUserInterfaceLayoutDirection)effectiveUserInterfaceLayoutDirection {
+    if (isRTL()) {
+        return UIUserInterfaceLayoutDirectionRightToLeft;
+    }
+    return UIUserInterfaceLayoutDirectionLeftToRight;
+}
+
+- (BOOL)flipsHorizontallyInOppositeLayoutDirection{
+    
+    return isRTL()? YES:NO;
+}
+@end

+ 661 - 0
ThirdParty/TUIKit/TIMCommon/CommonModel/TUIAttributedLabel.h

@@ -0,0 +1,661 @@
+
+//  Created by Tencent on 2023/06/09.
+//  Copyright © 2023 Tencent. All rights reserved.
+// TUIAttributedLabel.h
+
+#import <CoreText/CoreText.h>
+#import <UIKit/UIKit.h>
+
+//! Project version number for TUIAttributedLabel.
+FOUNDATION_EXPORT double TUIAttributedLabelVersionNumber;
+
+//! Project version string for TUIAttributedLabel.
+FOUNDATION_EXPORT const unsigned char TUIAttributedLabelVersionString[];
+
+@class TUIAttributedLabelLink;
+
+/**
+ Vertical alignment for text in a label whose bounds are larger than its text bounds
+ */
+typedef NS_ENUM(NSInteger, TUIAttributedLabelVerticalAlignment) {
+    TUIAttributedLabelVerticalAlignmentCenter = 0,
+    TUIAttributedLabelVerticalAlignmentTop = 1,
+    TUIAttributedLabelVerticalAlignmentBottom = 2,
+};
+
+/**
+ Determines whether the text to which this attribute applies has a strikeout drawn through itself.
+ */
+extern NSString *const kTUIStrikeOutAttributeName;
+
+/**
+ The background fill color. Value must be a `CGColorRef`. Default value is `nil` (no fill).
+ */
+extern NSString *const kTUIBackgroundFillColorAttributeName;
+
+/**
+ The padding for the background fill. Value must be a `UIEdgeInsets`. Default value is `UIEdgeInsetsZero` (no padding).
+ */
+extern NSString *const kTUIBackgroundFillPaddingAttributeName;
+
+/**
+ The background stroke color. Value must be a `CGColorRef`. Default value is `nil` (no stroke).
+ */
+extern NSString *const kTUIBackgroundStrokeColorAttributeName;
+
+/**
+ The background stroke line width. Value must be an `NSNumber`. Default value is `1.0f`.
+ */
+extern NSString *const kTUIBackgroundLineWidthAttributeName;
+
+/**
+ The background corner radius. Value must be an `NSNumber`. Default value is `5.0f`.
+ */
+extern NSString *const kTUIBackgroundCornerRadiusAttributeName;
+
+@protocol TUIAttributedLabelDelegate;
+
+// Override UILabel @property to accept both NSString and NSAttributedString
+@protocol TUIAttributedLabel <NSObject>
+@property(nonatomic, copy) IBInspectable id text;
+@end
+
+IB_DESIGNABLE
+
+/**
+ `TUIAttributedLabel` is a drop-in replacement for `UILabel` that supports `NSAttributedString`, as well as automatically-detected and manually-added links to
+ URLs, addresses, phone numbers, and dates.
+
+ ## Differences Between `TUIAttributedLabel` and `UILabel`
+
+ For the most part, `TUIAttributedLabel` behaves just like `UILabel`. The following are notable exceptions, in which `TUIAttributedLabel` may act differently:
+
+ - `text` - This property now takes an `id` type argument, which can either be a kind of `NSString` or `NSAttributedString` (mutable or immutable in both cases)
+ - `attributedText` - Do not set this property directly. Instead, pass an `NSAttributedString` to `text`.
+ - `lineBreakMode` - This property displays only the first line when the value is `UILineBreakModeHeadTruncation`, `UILineBreakModeTailTruncation`, or
+ `UILineBreakModeMiddleTruncation`
+ - `adjustsFontsizeToFitWidth` - Supported in iOS 5 and greater, this property is effective for any value of `numberOfLines` greater than zero. In iOS 4,
+ setting `numberOfLines` to a value greater than 1 with `adjustsFontSizeToFitWidth` set to `YES` may cause `sizeToFit` to execute indefinitely.
+ - `baselineAdjustment` - This property has no affect.
+ - `textAlignment` - This property does not support justified alignment.
+ - `NSTextAttachment` - This string attribute is not supported.
+
+ Any properties affecting text or paragraph styling, such as `firstLineIndent` will only apply when text is set with an `NSString`. If the text is set with an
+ `NSAttributedString`, these properties will not apply.
+
+ ### NSCoding
+
+ `TUIAttributedLabel`, like `UILabel`, conforms to `NSCoding`. However, if the build target is set to less than iOS 6.0, `linkAttributes` and
+ `activeLinkAttributes` will not be encoded or decoded. This is due to an runtime exception thrown when attempting to copy non-object CoreText values in
+ dictionaries.
+
+ @warning Any properties changed on the label after setting the text will not be reflected until a subsequent call to `setText:` or
+ `setText:afterInheritingLabelAttributesAndConfiguringWithBlock:`. This is to say, order of operations matters in this case. For example, if the label text
+ color is originally black when the text is set, changing the text color to red will have no effect on the display of the label until the text is set once
+ again.
+
+ @bug Setting `attributedText` directly is not recommended, as it may cause a crash when attempting to access any links previously set. Instead, call
+ `setText:`, passing an `NSAttributedString`.
+ */
+@interface TUIAttributedLabel : UILabel <TUIAttributedLabel, UIGestureRecognizerDelegate>
+
+/**
+ * The designated initializers are @c initWithFrame: and @c initWithCoder:.
+ * init will not properly initialize many required properties and other configuration.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+///-----------------------------
+/// @name Accessing the Delegate
+///-----------------------------
+
+/**
+ The receiver's delegate.
+
+ @discussion A `TUIAttributedLabel` delegate responds to messages sent by tapping on links in the label. You can use the delegate to respond to links
+ referencing a URL, address, phone number, date, or date with a specified time zone and duration.
+ */
+@property(nonatomic, unsafe_unretained) IBOutlet id<TUIAttributedLabelDelegate> delegate;
+
+///--------------------------------------------
+/// @name Detecting, Accessing, & Styling Links
+///--------------------------------------------
+
+/**
+ A bitmask of `NSTextCheckingType` which are used to automatically detect links in the label text.
+
+ @warning You must specify `enabledTextCheckingTypes` before setting the `text`, with either `setText:` or
+ `setText:afterInheritingLabelAttributesAndConfiguringWithBlock:`.
+ */
+@property(nonatomic, assign) NSTextCheckingTypes enabledTextCheckingTypes;
+
+/**
+ An array of `NSTextCheckingResult` objects for links detected or manually added to the label text.
+ */
+@property(readonly, nonatomic, strong) NSArray *links;
+
+/**
+ A dictionary containing the default `NSAttributedString` attributes to be applied to links detected or manually added to the label text. The default link style
+ is blue and underlined.
+
+ @warning You must specify `linkAttributes` before setting autodecting or manually-adding links for these attributes to be applied.
+ */
+@property(nonatomic, strong) NSDictionary *linkAttributes;
+
+/**
+ A dictionary containing the default `NSAttributedString` attributes to be applied to links when they are in the active state. If `nil` or an empty
+ `NSDictionary`, active links will not be styled. The default active link style is red and underlined.
+ */
+@property(nonatomic, strong) NSDictionary *activeLinkAttributes;
+
+/**
+ A dictionary containing the default `NSAttributedString` attributes to be applied to links when they are in the inactive state, which is triggered by a change
+ in `tintColor` in iOS 7 and later. If `nil` or an empty `NSDictionary`, inactive links will not be styled. The default inactive link style is gray and
+ unadorned.
+ */
+@property(nonatomic, strong) NSDictionary *inactiveLinkAttributes;
+
+/**
+ The edge inset for the background of a link. The default value is `{0, -1, 0, -1}`.
+ */
+@property(nonatomic, assign) UIEdgeInsets linkBackgroundEdgeInset;
+
+/**
+ Indicates if links will be detected within an extended area around the touch
+ to emulate the link detection behaviour of WKWebView.
+ Default value is NO. Enabling this may adversely impact performance.
+ */
+@property(nonatomic, assign) BOOL extendsLinkTouchArea;
+
+///---------------------------------------
+/// @name Acccessing Text Style Attributes
+///---------------------------------------
+
+/**
+ The shadow blur radius for the label. A value of 0 indicates no blur, while larger values produce correspondingly larger blurring. This value must not be
+ negative. The default value is 0.
+ */
+@property(nonatomic, assign) IBInspectable CGFloat shadowRadius;
+
+/**
+ The shadow blur radius for the label when the label's `highlighted` property is `YES`. A value of 0 indicates no blur, while larger values produce
+ correspondingly larger blurring. This value must not be negative. The default value is 0.
+ */
+@property(nonatomic, assign) IBInspectable CGFloat highlightedShadowRadius;
+/**
+ The shadow offset for the label when the label's `highlighted` property is `YES`. A size of {0, 0} indicates no offset, with positive values extending down and
+ to the right. The default size is {0, 0}.
+ */
+@property(nonatomic, assign) IBInspectable CGSize highlightedShadowOffset;
+/**
+ The shadow color for the label when the label's `highlighted` property is `YES`. The default value is `nil` (no shadow color).
+ */
+@property(nonatomic, strong) IBInspectable UIColor *highlightedShadowColor;
+
+/**
+ The amount to kern the next character. Default is standard kerning. If this attribute is set to 0.0, no kerning is done at all.
+ */
+@property(nonatomic, assign) IBInspectable CGFloat kern;
+
+///--------------------------------------------
+/// @name Acccessing Paragraph Style Attributes
+///--------------------------------------------
+
+/**
+ The distance, in points, from the leading margin of a frame to the beginning of the
+ paragraph's first line. This value is always nonnegative, and is 0.0 by default.
+ This applies to the full text, rather than any specific paragraph metrics.
+ */
+@property(nonatomic, assign) IBInspectable CGFloat firstLineIndent;
+
+/**
+ The space in points added between lines within the paragraph. This value is always nonnegative and is 0.0 by default.
+ */
+@property(nonatomic, assign) IBInspectable CGFloat lineSpacing;
+
+/**
+ The minimum line height within the paragraph. If the value is 0.0, the minimum line height is set to the line height of the `font`. 0.0 by default.
+ */
+@property(nonatomic, assign) IBInspectable CGFloat minimumLineHeight;
+
+/**
+ The maximum line height within the paragraph. If the value is 0.0, the maximum line height is set to the line height of the `font`. 0.0 by default.
+ */
+@property(nonatomic, assign) IBInspectable CGFloat maximumLineHeight;
+
+/**
+ The line height multiple. This value is 1.0 by default.
+ */
+@property(nonatomic, assign) IBInspectable CGFloat lineHeightMultiple;
+
+/**
+ The distance, in points, from the margin to the text container. This value is `UIEdgeInsetsZero` by default.
+ sizeThatFits: will have its returned size increased by these margins.
+ drawTextInRect: will inset all drawn text by these margins.
+ */
+@property(nonatomic, assign) IBInspectable UIEdgeInsets textInsets;
+
+/**
+ The vertical text alignment for the label, for when the frame size is greater than the text rect size. The vertical alignment is
+ `TUIAttributedLabelVerticalAlignmentCenter` by default.
+ */
+@property(nonatomic, assign) TUIAttributedLabelVerticalAlignment verticalAlignment;
+
+///--------------------------------------------
+/// @name Accessing Truncation Token Appearance
+///--------------------------------------------
+
+/**
+ The attributed string to apply to the truncation token at the end of a truncated line.
+ */
+@property(nonatomic, strong) IBInspectable NSAttributedString *attributedTruncationToken;
+
+///--------------------------
+/// @name Long press gestures
+///--------------------------
+
+/**
+ *  The long-press gesture recognizer used internally by the label.
+ */
+@property(nonatomic, strong, readonly) UILongPressGestureRecognizer *longPressGestureRecognizer;
+
+///--------------------------------------------
+/// @name Calculating Size of Attributed String
+///--------------------------------------------
+
+/**
+ Calculate and return the size that best fits an attributed string, given the specified constraints on size and number of lines.
+
+ @param attributedString The attributed string.
+ @param size The maximum dimensions used to calculate size.
+ @param numberOfLines The maximum number of lines in the text to draw, if the constraining size cannot accomodate the full attributed string.
+
+ @return The size that fits the attributed string within the specified constraints.
+ */
++ (CGSize)sizeThatFitsAttributedString:(NSAttributedString *)attributedString withConstraints:(CGSize)size limitedToNumberOfLines:(NSUInteger)numberOfLines;
+
+///----------------------------------
+/// @name Setting the Text Attributes
+///----------------------------------
+
+/**
+ Sets the text displayed by the label.
+
+ @param text An `NSString` or `NSAttributedString` object to be displayed by the label. If the specified text is an `NSString`, the label will display the text
+ like a `UILabel`, inheriting the text styles of the label. If the specified text is an `NSAttributedString`, the label text styles will be overridden by the
+ styles specified in the attributed string.
+
+ @discussion This method overrides `UILabel -setText:` to accept both `NSString` and `NSAttributedString` objects. This string is `nil` by default.
+ */
+- (void)setText:(id)text;
+
+/**
+ Sets the text displayed by the label, after configuring an attributed string containing the text attributes inherited from the label in a block.
+
+ @param text An `NSString` or `NSAttributedString` object to be displayed by the label.
+ @param block A block object that returns an `NSMutableAttributedString` object and takes a single argument, which is an `NSMutableAttributedString` object with
+ the text from the first parameter, and the text attributes inherited from the label text styles. For example, if you specified the `font` of the label to be
+ `[UIFont boldSystemFontOfSize:14]` and `textColor` to be `[UIColor redColor]`, the `NSAttributedString` argument of the block would be contain the
+ `NSAttributedString` attribute equivalents of those properties. In this block, you can set further attributes on particular ranges.
+
+ @discussion This string is `nil` by default.
+ */
+- (void)setText:(id)text
+    afterInheritingLabelAttributesAndConfiguringWithBlock:(NSMutableAttributedString * (^)(NSMutableAttributedString *mutableAttributedString))block;
+
+///------------------------------------
+/// @name Accessing the Text Attributes
+///------------------------------------
+
+/**
+ A copy of the label's current attributedText. This returns `nil` if an attributed string has never been set on the label.
+
+ @warning Do not set this property directly. Instead, set @c text to an @c NSAttributedString.
+ */
+@property(readwrite, nonatomic, copy) NSAttributedString *attributedText;
+
+///-------------------
+/// @name Adding Links
+///-------------------
+
+/**
+ Adds a link. You can customize an individual link's appearance and accessibility value by creating your own @c TUIAttributedLabelLink and passing it to this
+ method. The other methods for adding links will use the label's default attributes.
+
+ @warning Modifying the link's attribute dictionaries must be done before calling this method.
+
+ @param link A @c TUIAttributedLabelLink object.
+ */
+- (void)addLink:(TUIAttributedLabelLink *)link;
+
+/**
+ Adds a link to an @c NSTextCheckingResult.
+
+ @param result An @c NSTextCheckingResult representing the link's location and type.
+
+ @return The newly added link object.
+ */
+- (TUIAttributedLabelLink *)addLinkWithTextCheckingResult:(NSTextCheckingResult *)result;
+
+/**
+ Adds a link to an @c NSTextCheckingResult.
+
+ @param result An @c NSTextCheckingResult representing the link's location and type.
+ @param attributes The attributes to be added to the text in the range of the specified link. If set, the label's @c activeAttributes and @c inactiveAttributes
+ will be applied to the link. If `nil`, no attributes are added to the link.
+
+ @return The newly added link object.
+ */
+- (TUIAttributedLabelLink *)addLinkWithTextCheckingResult:(NSTextCheckingResult *)result attributes:(NSDictionary *)attributes;
+
+/**
+ Adds a link to a URL for a specified range in the label text.
+
+ @param url The url to be linked to
+ @param range The range in the label text of the link. The range must not exceed the bounds of the receiver.
+
+ @return The newly added link object.
+ */
+- (TUIAttributedLabelLink *)addLinkToURL:(NSURL *)url withRange:(NSRange)range;
+
+/**
+ Adds a link to an address for a specified range in the label text.
+
+ @param addressComponents A dictionary of address components for the address to be linked to
+ @param range The range in the label text of the link. The range must not exceed the bounds of the receiver.
+
+ @discussion The address component dictionary keys are described in `NSTextCheckingResult`'s "Keys for Address Components."
+
+ @return The newly added link object.
+ */
+- (TUIAttributedLabelLink *)addLinkToAddress:(NSDictionary *)addressComponents withRange:(NSRange)range;
+
+/**
+ Adds a link to a phone number for a specified range in the label text.
+
+ @param phoneNumber The phone number to be linked to.
+ @param range The range in the label text of the link. The range must not exceed the bounds of the receiver.
+
+ @return The newly added link object.
+ */
+- (TUIAttributedLabelLink *)addLinkToPhoneNumber:(NSString *)phoneNumber withRange:(NSRange)range;
+
+/**
+ Adds a link to a date for a specified range in the label text.
+
+ @param date The date to be linked to.
+ @param range The range in the label text of the link. The range must not exceed the bounds of the receiver.
+
+ @return The newly added link object.
+ */
+- (TUIAttributedLabelLink *)addLinkToDate:(NSDate *)date withRange:(NSRange)range;
+
+/**
+ Adds a link to a date with a particular time zone and duration for a specified range in the label text.
+
+ @param date The date to be linked to.
+ @param timeZone The time zone of the specified date.
+ @param duration The duration, in seconds from the specified date.
+ @param range The range in the label text of the link. The range must not exceed the bounds of the receiver.
+
+ @return The newly added link object.
+ */
+- (TUIAttributedLabelLink *)addLinkToDate:(NSDate *)date timeZone:(NSTimeZone *)timeZone duration:(NSTimeInterval)duration withRange:(NSRange)range;
+
+/**
+ Adds a link to transit information for a specified range in the label text.
+
+ @param components A dictionary containing the transit components. The currently supported keys are `NSTextCheckingAirlineKey` and `NSTextCheckingFlightKey`.
+ @param range The range in the label text of the link. The range must not exceed the bounds of the receiver.
+
+ @return The newly added link object.
+ */
+- (TUIAttributedLabelLink *)addLinkToTransitInformation:(NSDictionary *)components withRange:(NSRange)range;
+
+/**
+ Returns whether an @c NSTextCheckingResult is found at the give point.
+
+ @discussion This can be used together with @c UITapGestureRecognizer to tap interactions with overlapping views.
+
+ @param point The point inside the label.
+ */
+- (BOOL)containslinkAtPoint:(CGPoint)point;
+
+/**
+ Returns the @c TUIAttributedLabelLink at the give point if it exists.
+
+ @discussion This can be used together with @c UIViewControllerPreviewingDelegate to peek into links.
+
+ @param point The point inside the label.
+ */
+- (TUIAttributedLabelLink *)linkAtPoint:(CGPoint)point;
+
+@end
+
+/**
+ The `TUIAttributedLabelDelegate` protocol defines the messages sent to an attributed label delegate when links are tapped. All of the methods of this protocol
+ are optional.
+ */
+@protocol TUIAttributedLabelDelegate <NSObject>
+
+///-----------------------------------
+/// @name Responding to Link Selection
+///-----------------------------------
+@optional
+
+/**
+ Tells the delegate that the user did select a link to a URL.
+
+ @param label The label whose link was selected.
+ @param url The URL for the selected link.
+ */
+- (void)attributedLabel:(TUIAttributedLabel *)label didSelectLinkWithURL:(NSURL *)url;
+
+/**
+ Tells the delegate that the user did select a link to an address.
+
+ @param label The label whose link was selected.
+ @param addressComponents The components of the address for the selected link.
+ */
+- (void)attributedLabel:(TUIAttributedLabel *)label didSelectLinkWithAddress:(NSDictionary *)addressComponents;
+
+/**
+ Tells the delegate that the user did select a link to a phone number.
+
+ @param label The label whose link was selected.
+ @param phoneNumber The phone number for the selected link.
+ */
+- (void)attributedLabel:(TUIAttributedLabel *)label didSelectLinkWithPhoneNumber:(NSString *)phoneNumber;
+
+/**
+ Tells the delegate that the user did select a link to a date.
+
+ @param label The label whose link was selected.
+ @param date The datefor the selected link.
+ */
+- (void)attributedLabel:(TUIAttributedLabel *)label didSelectLinkWithDate:(NSDate *)date;
+
+/**
+ Tells the delegate that the user did select a link to a date with a time zone and duration.
+
+ @param label The label whose link was selected.
+ @param date The date for the selected link.
+ @param timeZone The time zone of the date for the selected link.
+ @param duration The duration, in seconds from the date for the selected link.
+ */
+- (void)attributedLabel:(TUIAttributedLabel *)label didSelectLinkWithDate:(NSDate *)date timeZone:(NSTimeZone *)timeZone duration:(NSTimeInterval)duration;
+
+/**
+ Tells the delegate that the user did select a link to transit information
+
+ @param label The label whose link was selected.
+ @param components A dictionary containing the transit components. The currently supported keys are `NSTextCheckingAirlineKey` and `NSTextCheckingFlightKey`.
+ */
+- (void)attributedLabel:(TUIAttributedLabel *)label didSelectLinkWithTransitInformation:(NSDictionary *)components;
+
+/**
+ Tells the delegate that the user did select a link to a text checking result.
+
+ @discussion This method is called if no other delegate method was called, which can occur by either now implementing the method in `TUIAttributedLabelDelegate`
+ corresponding to a particular link, or the link was added by passing an instance of a custom `NSTextCheckingResult` subclass into
+ `-addLinkWithTextCheckingResult:`.
+
+ @param label The label whose link was selected.
+ @param result The custom text checking result.
+ */
+- (void)attributedLabel:(TUIAttributedLabel *)label didSelectLinkWithTextCheckingResult:(NSTextCheckingResult *)result;
+
+///---------------------------------
+/// @name Responding to Long Presses
+///---------------------------------
+
+/**
+ *  Long-press delegate methods include the CGPoint tapped within the label's coordinate space.
+ *  This may be useful on iPad to present a popover from a specific origin point.
+ */
+
+/**
+ Tells the delegate that the user long-pressed a link to a URL.
+
+ @param label The label whose link was long pressed.
+ @param url The URL for the link.
+ @param point the point pressed, in the label's coordinate space
+ */
+- (void)attributedLabel:(TUIAttributedLabel *)label didLongPressLinkWithURL:(NSURL *)url atPoint:(CGPoint)point;
+
+/**
+ Tells the delegate that the user long-pressed a link to an address.
+
+ @param label The label whose link was long pressed.
+ @param addressComponents The components of the address for the link.
+ @param point the point pressed, in the label's coordinate space
+ */
+- (void)attributedLabel:(TUIAttributedLabel *)label didLongPressLinkWithAddress:(NSDictionary *)addressComponents atPoint:(CGPoint)point;
+
+/**
+ Tells the delegate that the user long-pressed a link to a phone number.
+
+ @param label The label whose link was long pressed.
+ @param phoneNumber The phone number for the link.
+ @param point the point pressed, in the label's coordinate space
+ */
+- (void)attributedLabel:(TUIAttributedLabel *)label didLongPressLinkWithPhoneNumber:(NSString *)phoneNumber atPoint:(CGPoint)point;
+
+/**
+ Tells the delegate that the user long-pressed a link to a date.
+
+ @param label The label whose link was long pressed.
+ @param date The date for the selected link.
+ @param point the point pressed, in the label's coordinate space
+ */
+- (void)attributedLabel:(TUIAttributedLabel *)label didLongPressLinkWithDate:(NSDate *)date atPoint:(CGPoint)point;
+
+/**
+ Tells the delegate that the user long-pressed a link to a date with a time zone and duration.
+
+ @param label The label whose link was long pressed.
+ @param date The date for the link.
+ @param timeZone The time zone of the date for the link.
+ @param duration The duration, in seconds from the date for the link.
+ @param point the point pressed, in the label's coordinate space
+ */
+- (void)attributedLabel:(TUIAttributedLabel *)label
+    didLongPressLinkWithDate:(NSDate *)date
+                    timeZone:(NSTimeZone *)timeZone
+                    duration:(NSTimeInterval)duration
+                     atPoint:(CGPoint)point;
+
+/**
+ Tells the delegate that the user long-pressed a link to transit information.
+
+ @param label The label whose link was long pressed.
+ @param components A dictionary containing the transit components. The currently supported keys are `NSTextCheckingAirlineKey` and `NSTextCheckingFlightKey`.
+ @param point the point pressed, in the label's coordinate space
+ */
+- (void)attributedLabel:(TUIAttributedLabel *)label didLongPressLinkWithTransitInformation:(NSDictionary *)components atPoint:(CGPoint)point;
+
+/**
+ Tells the delegate that the user long-pressed a link to a text checking result.
+
+ @discussion Similar to `-attributedLabel:didSelectLinkWithTextCheckingResult:`, this method is called if a link is long pressed and the delegate does not
+ implement the method corresponding to this type of link.
+
+ @param label The label whose link was long pressed.
+ @param result The custom text checking result.
+ @param point the point pressed, in the label's coordinate space
+ */
+- (void)attributedLabel:(TUIAttributedLabel *)label didLongPressLinkWithTextCheckingResult:(NSTextCheckingResult *)result atPoint:(CGPoint)point;
+
+@end
+
+@interface TUIAttributedLabelLink : NSObject <NSCoding>
+
+typedef void (^TUIAttributedLabelLinkBlock)(TUIAttributedLabel *, TUIAttributedLabelLink *);
+
+/**
+ An `NSTextCheckingResult` representing the link's location and type.
+ */
+@property(readonly, nonatomic, strong) NSTextCheckingResult *result;
+
+/**
+ A dictionary containing the @c NSAttributedString attributes to be applied to the link.
+ */
+@property(readonly, nonatomic, copy) NSDictionary *attributes;
+
+/**
+ A dictionary containing the @c NSAttributedString attributes to be applied to the link when it is in the active state.
+ */
+@property(readonly, nonatomic, copy) NSDictionary *activeAttributes;
+
+/**
+ A dictionary containing the @c NSAttributedString attributes to be applied to the link when it is in the inactive state, which is triggered by a change in
+ `tintColor` in iOS 7 and later.
+ */
+@property(readonly, nonatomic, copy) NSDictionary *inactiveAttributes;
+
+/**
+ Additional information about a link for VoiceOver users. Has default values if the link's @c result is @c NSTextCheckingTypeLink, @c
+ NSTextCheckingTypePhoneNumber, or @c NSTextCheckingTypeDate.
+ */
+@property(nonatomic, copy) NSString *accessibilityValue;
+
+/**
+ A block called when this link is tapped.
+ If non-nil, tapping on this link will call this block instead of the
+ @c TUIAttributedLabelDelegate tap methods, which will not be called for this link.
+ */
+@property(nonatomic, copy) TUIAttributedLabelLinkBlock linkTapBlock;
+
+/**
+ A block called when this link is long-pressed.
+ If non-nil, long pressing on this link will call this block instead of the
+ @c TUIAttributedLabelDelegate long press methods, which will not be called for this link.
+ */
+@property(nonatomic, copy) TUIAttributedLabelLinkBlock linkLongPressBlock;
+
+/**
+ Initializes a link using the attribute dictionaries specified.
+
+ @param attributes         The @c attributes property for the link.
+ @param activeAttributes   The @c activeAttributes property for the link.
+ @param inactiveAttributes The @c inactiveAttributes property for the link.
+ @param result             An @c NSTextCheckingResult representing the link's location and type.
+
+ @return The initialized link object.
+ */
+- (instancetype)initWithAttributes:(NSDictionary *)attributes
+                  activeAttributes:(NSDictionary *)activeAttributes
+                inactiveAttributes:(NSDictionary *)inactiveAttributes
+                textCheckingResult:(NSTextCheckingResult *)result;
+
+/**
+ Initializes a link using the attribute dictionaries set on a specified label.
+
+ @param label  The attributed label from which to inherit attribute dictionaries.
+ @param result An @c NSTextCheckingResult representing the link's location and type.
+
+ @return The initialized link object.
+ */
+- (instancetype)initWithAttributesFromLabel:(TUIAttributedLabel *)label textCheckingResult:(NSTextCheckingResult *)result;
+
+@end

+ 1797 - 0
ThirdParty/TUIKit/TIMCommon/CommonModel/TUIAttributedLabel.m

@@ -0,0 +1,1797 @@
+
+//  Created by Tencent on 2023/06/09.
+//  Copyright © 2023 Tencent. All rights reserved.
+// TUIAttributedLabel.m
+
+#import "TUIAttributedLabel.h"
+
+#import <Availability.h>
+#import <QuartzCore/QuartzCore.h>
+#import <objc/runtime.h>
+
+#define kTUILineBreakWordWrapTextWidthScalingFactor (M_PI / M_E)
+
+static CGFloat const TUIFLOAT_MAX = 100000;
+
+NSString *const kTUIStrikeOutAttributeName = @"TUIStrikeOutAttribute";
+NSString *const kTUIBackgroundFillColorAttributeName = @"TUIBackgroundFillColor";
+NSString *const kTUIBackgroundFillPaddingAttributeName = @"TUIBackgroundFillPadding";
+NSString *const kTUIBackgroundStrokeColorAttributeName = @"TUIBackgroundStrokeColor";
+NSString *const kTUIBackgroundLineWidthAttributeName = @"TUIBackgroundLineWidth";
+NSString *const kTUIBackgroundCornerRadiusAttributeName = @"TUIBackgroundCornerRadius";
+
+const NSTextAlignment TUITextAlignmentLeft = NSTextAlignmentLeft;
+const NSTextAlignment TUITextAlignmentCenter = NSTextAlignmentCenter;
+const NSTextAlignment TUITextAlignmentRight = NSTextAlignmentRight;
+const NSTextAlignment TUITextAlignmentJustified = NSTextAlignmentJustified;
+const NSTextAlignment TUITextAlignmentNatural = NSTextAlignmentNatural;
+
+const NSLineBreakMode TUILineBreakByWordWrapping = NSLineBreakByWordWrapping;
+const NSLineBreakMode TUILineBreakByCharWrapping = NSLineBreakByCharWrapping;
+const NSLineBreakMode TUILineBreakByClipping = NSLineBreakByClipping;
+const NSLineBreakMode TUILineBreakByTruncatingHead = NSLineBreakByTruncatingHead;
+const NSLineBreakMode TUILineBreakByTruncatingMiddle = NSLineBreakByTruncatingMiddle;
+const NSLineBreakMode TUILineBreakByTruncatingTail = NSLineBreakByTruncatingTail;
+
+typedef NSTextAlignment TUITextAlignment;
+typedef NSLineBreakMode TUILineBreakMode;
+
+static inline CGFLOAT_TYPE formatCGFloatCeil(CGFLOAT_TYPE cgfloat) {
+#if CGFLOAT_IS_DOUBLE
+    return ceil(cgfloat);
+#else
+    return ceilf(cgfloat);
+#endif
+}
+
+static inline CGFLOAT_TYPE formatCGFloatFloor(CGFLOAT_TYPE cgfloat) {
+#if CGFLOAT_IS_DOUBLE
+    return floor(cgfloat);
+#else
+    return floorf(cgfloat);
+#endif
+}
+
+static inline CGFLOAT_TYPE formatCGFloatRound(CGFLOAT_TYPE cgfloat) {
+#if CGFLOAT_IS_DOUBLE
+    return round(cgfloat);
+#else
+    return roundf(cgfloat);
+#endif
+}
+
+static inline CGFLOAT_TYPE formatCGFloatSqrt(CGFLOAT_TYPE cgfloat) {
+#if CGFLOAT_IS_DOUBLE
+    return sqrt(cgfloat);
+#else
+    return sqrtf(cgfloat);
+#endif
+}
+
+static inline CGFloat flushFactorForTextAlignment(NSTextAlignment textAlignment) {
+    switch (textAlignment) {
+        case TUITextAlignmentCenter:
+            return 0.5f;
+        case TUITextAlignmentRight:
+            return 1.0f;
+        case TUITextAlignmentLeft:
+        default:
+            return 0.0f;
+    }
+}
+
+static inline NSDictionary *formatNSAttributedStringAttributesFromLabel(TUIAttributedLabel *label) {
+    NSMutableDictionary *mutableAttributes = [NSMutableDictionary dictionary];
+
+    [mutableAttributes setObject:label.font forKey:(NSString *)kCTFontAttributeName];
+    [mutableAttributes setObject:label.textColor forKey:(NSString *)kCTForegroundColorAttributeName];
+    [mutableAttributes setObject:@(label.kern) forKey:(NSString *)kCTKernAttributeName];
+
+    NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
+    paragraphStyle.alignment = label.textAlignment;
+    paragraphStyle.lineSpacing = label.lineSpacing;
+    paragraphStyle.minimumLineHeight = label.minimumLineHeight > 0 ? label.minimumLineHeight : label.font.lineHeight * label.lineHeightMultiple;
+    paragraphStyle.maximumLineHeight = label.maximumLineHeight > 0 ? label.maximumLineHeight : label.font.lineHeight * label.lineHeightMultiple;
+    paragraphStyle.lineHeightMultiple = label.lineHeightMultiple;
+    paragraphStyle.firstLineHeadIndent = label.firstLineIndent;
+
+    if (label.numberOfLines == 1) {
+        paragraphStyle.lineBreakMode = label.lineBreakMode;
+    } else {
+        paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping;
+    }
+
+    [mutableAttributes setObject:paragraphStyle forKey:(NSString *)kCTParagraphStyleAttributeName];
+
+    return [NSDictionary dictionaryWithDictionary:mutableAttributes];
+}
+
+static inline CGColorRef formatCGColorRefFromColor(id color);
+static inline NSDictionary *convertNSAttributedStringAttributesToCTAttributes(NSDictionary *attributes);
+
+static inline NSAttributedString *formatNSAttributedStringByScalingFontSize(NSAttributedString *attributedString, CGFloat scale) {
+    NSMutableAttributedString *mutableAttributedString = [attributedString mutableCopy];
+    [mutableAttributedString enumerateAttribute:(NSString *)kCTFontAttributeName
+                                        inRange:NSMakeRange(0, [mutableAttributedString length])
+                                        options:0
+                                     usingBlock:^(id value, NSRange range, BOOL *__unused stop) {
+                                       UIFont *font = (UIFont *)value;
+                                       if (font) {
+                                           NSString *fontName;
+                                           CGFloat pointSize;
+
+                                           if ([font isKindOfClass:[UIFont class]]) {
+                                               fontName = font.fontName;
+                                               pointSize = font.pointSize;
+                                           } else {
+                                               fontName = (NSString *)CFBridgingRelease(CTFontCopyName((__bridge CTFontRef)font, kCTFontPostScriptNameKey));
+                                               pointSize = CTFontGetSize((__bridge CTFontRef)font);
+                                           }
+
+                                           [mutableAttributedString removeAttribute:(NSString *)kCTFontAttributeName range:range];
+                                           CTFontRef fontRef =
+                                               CTFontCreateWithName((__bridge CFStringRef)fontName, formatCGFloatFloor(pointSize * scale), NULL);
+                                           [mutableAttributedString addAttribute:(NSString *)kCTFontAttributeName value:(__bridge id)fontRef range:range];
+                                           CFRelease(fontRef);
+                                       }
+                                     }];
+
+    return mutableAttributedString;
+}
+
+static inline NSAttributedString *formatNSAttributedStringBySettingColorFromContext(NSAttributedString *attributedString, UIColor *color) {
+    if (!color) {
+        return attributedString;
+    }
+
+    NSMutableAttributedString *mutableAttributedString = [attributedString mutableCopy];
+    [mutableAttributedString enumerateAttribute:(NSString *)kCTForegroundColorFromContextAttributeName
+                                        inRange:NSMakeRange(0, [mutableAttributedString length])
+                                        options:0
+                                     usingBlock:^(id value, NSRange range, __unused BOOL *stop) {
+                                       BOOL usesColorFromContext = (BOOL)value;
+                                       if (usesColorFromContext) {
+                                           [mutableAttributedString
+                                               setAttributes:[NSDictionary dictionaryWithObject:color forKey:(NSString *)kCTForegroundColorAttributeName]
+                                                       range:range];
+                                           [mutableAttributedString removeAttribute:(NSString *)kCTForegroundColorFromContextAttributeName range:range];
+                                       }
+                                     }];
+
+    return mutableAttributedString;
+}
+
+static inline CGSize formatCTFramesetterSuggestFrameSizeForAttributedStringWithConstraints(CTFramesetterRef framesetter, NSAttributedString *attributedString,
+                                                                                           CGSize size, NSUInteger numberOfLines) {
+    CFRange rangeToSize = CFRangeMake(0, (CFIndex)[attributedString length]);
+    CGSize constraints = CGSizeMake(size.width, TUIFLOAT_MAX);
+
+    if (numberOfLines == 1) {
+        // If there is one line, the size that fits is the full width of the line
+        constraints = CGSizeMake(TUIFLOAT_MAX, TUIFLOAT_MAX);
+    } else if (numberOfLines > 0) {
+        // If the line count of the label more than 1, limit the range to size to the number of lines that have been set
+        CGMutablePathRef path = CGPathCreateMutable();
+        CGPathAddRect(path, NULL, CGRectMake(0.0f, 0.0f, constraints.width, TUIFLOAT_MAX));
+        CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
+        CFArrayRef lines = CTFrameGetLines(frame);
+
+        if (CFArrayGetCount(lines) > 0) {
+            NSInteger lastVisibleLineIndex = MIN((CFIndex)numberOfLines, CFArrayGetCount(lines)) - 1;
+            CTLineRef lastVisibleLine = CFArrayGetValueAtIndex(lines, lastVisibleLineIndex);
+
+            CFRange rangeToLayout = CTLineGetStringRange(lastVisibleLine);
+            rangeToSize = CFRangeMake(0, rangeToLayout.location + rangeToLayout.length);
+        }
+
+        CFRelease(frame);
+        CGPathRelease(path);
+    }
+
+    CGSize suggestedSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, rangeToSize, NULL, constraints, NULL);
+
+    return CGSizeMake(formatCGFloatCeil(suggestedSize.width), formatCGFloatCeil(suggestedSize.height));
+}
+
+@interface TUIAccessibilityElement : UIAccessibilityElement
+@property(nonatomic, weak) UIView *superview;
+@property(nonatomic, assign) CGRect boundingRect;
+@end
+
+@implementation TUIAccessibilityElement
+
+- (CGRect)accessibilityFrame {
+    return UIAccessibilityConvertFrameToScreenCoordinates(self.boundingRect, self.superview);
+}
+
+@end
+
+@interface TUIAttributedLabel ()
+@property(readwrite, nonatomic, copy) NSAttributedString *inactiveAttributedText;
+@property(readwrite, nonatomic, copy) NSAttributedString *renderedAttributedText;
+@property(readwrite, atomic, strong) NSDataDetector *dataDetector;
+@property(readwrite, nonatomic, strong) NSArray *linkModels;
+@property(readwrite, nonatomic, strong) TUIAttributedLabelLink *activeLink;
+@property(readwrite, nonatomic, strong) NSArray *accessibilityElements;
+
+- (void)longPressGestureDidFire:(UILongPressGestureRecognizer *)sender;
+@end
+
+@implementation TUIAttributedLabel {
+@private
+    BOOL _needsFramesetter;
+    CTFramesetterRef _framesetter;
+    CTFramesetterRef _highlightFramesetter;
+}
+
+@dynamic text;
+@synthesize attributedText = _attributedText;
+
+#ifndef kCFCoreFoundationVersionNumber_iOS_7_0
+#define kCFCoreFoundationVersionNumber_iOS_7_0 847.2
+#endif
+
++ (void)load {
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+      if (kCFCoreFoundationVersionNumber < kCFCoreFoundationVersionNumber_iOS_7_0) {
+          Class class = [self class];
+          Class superclass = class_getSuperclass(class);
+
+          NSArray *strings = @[
+              NSStringFromSelector(@selector(isAccessibilityElement)),
+              NSStringFromSelector(@selector(accessibilityElementCount)),
+              NSStringFromSelector(@selector(accessibilityElementAtIndex:)),
+              NSStringFromSelector(@selector(indexOfAccessibilityElement:)),
+          ];
+
+          for (NSString *string in strings) {
+              SEL selector = NSSelectorFromString(string);
+              IMP superImplementation = class_getMethodImplementation(superclass, selector);
+              Method method = class_getInstanceMethod(class, selector);
+              const char *types = method_getTypeEncoding(method);
+              class_replaceMethod(class, selector, superImplementation, types);
+          }
+      }
+    });
+}
+
+- (instancetype)initWithFrame:(CGRect)frame {
+    self = [super initWithFrame:frame];
+    if (!self) {
+        return nil;
+    }
+
+    [self commonInit];
+
+    return self;
+}
+
+- (void)commonInit {
+    self.userInteractionEnabled = YES;
+#if !TARGET_OS_TV
+    self.multipleTouchEnabled = NO;
+#endif
+
+    self.textInsets = UIEdgeInsetsZero;
+    self.lineHeightMultiple = 1.0f;
+
+    self.linkModels = [NSArray array];
+
+    self.linkBackgroundEdgeInset = UIEdgeInsetsMake(0.0f, -1.0f, 0.0f, -1.0f);
+
+    NSMutableDictionary *mutableLinkAttributes = [NSMutableDictionary dictionary];
+    [mutableLinkAttributes setObject:[NSNumber numberWithBool:YES] forKey:(NSString *)kCTUnderlineStyleAttributeName];
+
+    NSMutableDictionary *mutableActiveLinkAttributes = [NSMutableDictionary dictionary];
+    [mutableActiveLinkAttributes setObject:[NSNumber numberWithBool:NO] forKey:(NSString *)kCTUnderlineStyleAttributeName];
+
+    NSMutableDictionary *mutableInactiveLinkAttributes = [NSMutableDictionary dictionary];
+    [mutableInactiveLinkAttributes setObject:[NSNumber numberWithBool:NO] forKey:(NSString *)kCTUnderlineStyleAttributeName];
+
+    if ([NSMutableParagraphStyle class]) {
+        [mutableLinkAttributes setObject:[UIColor blueColor] forKey:(NSString *)kCTForegroundColorAttributeName];
+        [mutableActiveLinkAttributes setObject:[UIColor redColor] forKey:(NSString *)kCTForegroundColorAttributeName];
+        [mutableInactiveLinkAttributes setObject:[UIColor grayColor] forKey:(NSString *)kCTForegroundColorAttributeName];
+    } else {
+        [mutableLinkAttributes setObject:(__bridge id)[[UIColor blueColor] CGColor] forKey:(NSString *)kCTForegroundColorAttributeName];
+        [mutableActiveLinkAttributes setObject:(__bridge id)[[UIColor redColor] CGColor] forKey:(NSString *)kCTForegroundColorAttributeName];
+        [mutableInactiveLinkAttributes setObject:(__bridge id)[[UIColor grayColor] CGColor] forKey:(NSString *)kCTForegroundColorAttributeName];
+    }
+
+    self.linkAttributes = [NSDictionary dictionaryWithDictionary:mutableLinkAttributes];
+    self.activeLinkAttributes = [NSDictionary dictionaryWithDictionary:mutableActiveLinkAttributes];
+    self.inactiveLinkAttributes = [NSDictionary dictionaryWithDictionary:mutableInactiveLinkAttributes];
+    _extendsLinkTouchArea = NO;
+    _longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPressGestureDidFire:)];
+    self.longPressGestureRecognizer.delegate = self;
+    [self addGestureRecognizer:self.longPressGestureRecognizer];
+}
+
+- (void)dealloc {
+    if (_framesetter) {
+        CFRelease(_framesetter);
+    }
+
+    if (_highlightFramesetter) {
+        CFRelease(_highlightFramesetter);
+    }
+
+    if (_longPressGestureRecognizer) {
+        [self removeGestureRecognizer:_longPressGestureRecognizer];
+    }
+}
+
+#pragma mark -
+
++ (CGSize)sizeThatFitsAttributedString:(NSAttributedString *)attributedString withConstraints:(CGSize)size limitedToNumberOfLines:(NSUInteger)numberOfLines {
+    if (!attributedString || attributedString.length == 0) {
+        return CGSizeZero;
+    }
+
+    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)attributedString);
+
+    CGSize calculatedSize = formatCTFramesetterSuggestFrameSizeForAttributedStringWithConstraints(framesetter, attributedString, size, numberOfLines);
+
+    CFRelease(framesetter);
+
+    return calculatedSize;
+}
+
+#pragma mark -
+
+- (void)setAttributedText:(NSAttributedString *)text {
+    if ([text isEqualToAttributedString:_attributedText]) {
+        return;
+    }
+
+    _attributedText = [text copy];
+
+    [self setNeedsFramesetter];
+    [self setNeedsDisplay];
+
+    if ([self respondsToSelector:@selector(invalidateIntrinsicContentSize)]) {
+        [self invalidateIntrinsicContentSize];
+    }
+
+    [super setText:[self.attributedText string]];
+}
+
+- (NSAttributedString *)renderedAttributedText {
+    if (!_renderedAttributedText) {
+        NSMutableAttributedString *fullString = [[NSMutableAttributedString alloc] initWithAttributedString:self.attributedText];
+
+        if (self.attributedTruncationToken) {
+            [fullString appendAttributedString:self.attributedTruncationToken];
+        }
+
+        NSAttributedString *string = [[NSAttributedString alloc] initWithAttributedString:fullString];
+        self.renderedAttributedText = formatNSAttributedStringBySettingColorFromContext(string, self.textColor);
+    }
+
+    return _renderedAttributedText;
+}
+
+- (NSArray *)links {
+    return [_linkModels valueForKey:@"result"];
+}
+
+- (void)setLinkModels:(NSArray *)linkModels {
+    _linkModels = linkModels;
+
+    self.accessibilityElements = nil;
+}
+
+- (void)setNeedsFramesetter {
+    // Reset the rendered attributed text so it has a chance to regenerate
+    self.renderedAttributedText = nil;
+
+    _needsFramesetter = YES;
+}
+
+- (CTFramesetterRef)framesetter {
+    if (_needsFramesetter) {
+        @synchronized(self) {
+            CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)self.renderedAttributedText);
+            [self setFramesetter:framesetter];
+            [self setHighlightFramesetter:nil];
+            _needsFramesetter = NO;
+
+            if (framesetter) {
+                CFRelease(framesetter);
+            }
+        }
+    }
+
+    return _framesetter;
+}
+
+- (void)setFramesetter:(CTFramesetterRef)framesetter {
+    if (framesetter) {
+        CFRetain(framesetter);
+    }
+
+    if (_framesetter) {
+        CFRelease(_framesetter);
+    }
+
+    _framesetter = framesetter;
+}
+
+- (CTFramesetterRef)highlightFramesetter {
+    return _highlightFramesetter;
+}
+
+- (void)setHighlightFramesetter:(CTFramesetterRef)highlightFramesetter {
+    if (highlightFramesetter) {
+        CFRetain(highlightFramesetter);
+    }
+
+    if (_highlightFramesetter) {
+        CFRelease(_highlightFramesetter);
+    }
+
+    _highlightFramesetter = highlightFramesetter;
+}
+
+#pragma mark -
+
+- (void)setEnabledTextCheckingTypes:(NSTextCheckingTypes)enabledTextCheckingTypes {
+    if (self.enabledTextCheckingTypes == enabledTextCheckingTypes) {
+        return;
+    }
+
+    _enabledTextCheckingTypes = enabledTextCheckingTypes;
+
+    // one detector instance per type (combination), fast reuse e.g. in cells
+    static NSMutableDictionary *dataDetectorsByType = nil;
+
+    if (!dataDetectorsByType) {
+        dataDetectorsByType = [NSMutableDictionary dictionary];
+    }
+
+    if (enabledTextCheckingTypes) {
+        if (![dataDetectorsByType objectForKey:@(enabledTextCheckingTypes)]) {
+            NSDataDetector *detector = [NSDataDetector dataDetectorWithTypes:enabledTextCheckingTypes error:nil];
+            if (detector) {
+                [dataDetectorsByType setObject:detector forKey:@(enabledTextCheckingTypes)];
+            }
+        }
+        self.dataDetector = [dataDetectorsByType objectForKey:@(enabledTextCheckingTypes)];
+    } else {
+        self.dataDetector = nil;
+    }
+}
+
+- (void)addLink:(TUIAttributedLabelLink *)link {
+    [self addLinks:@[ link ]];
+}
+
+- (void)addLinks:(NSArray *)links {
+    NSMutableArray *mutableLinkModels = [NSMutableArray arrayWithArray:self.linkModels];
+
+    NSMutableAttributedString *mutableAttributedString = [self.attributedText mutableCopy];
+
+    for (TUIAttributedLabelLink *link in links) {
+        if (link.attributes) {
+            [mutableAttributedString addAttributes:link.attributes range:link.result.range];
+        }
+    }
+
+    self.attributedText = mutableAttributedString;
+    [self setNeedsDisplay];
+
+    [mutableLinkModels addObjectsFromArray:links];
+
+    self.linkModels = [NSArray arrayWithArray:mutableLinkModels];
+}
+
+- (TUIAttributedLabelLink *)addLinkWithTextCheckingResult:(NSTextCheckingResult *)result attributes:(NSDictionary *)attributes {
+    return [self addLinksWithTextCheckingResults:@[ result ] attributes:attributes].firstObject;
+}
+
+- (NSArray *)addLinksWithTextCheckingResults:(NSArray *)results attributes:(NSDictionary *)attributes {
+    NSMutableArray *links = [NSMutableArray array];
+
+    for (NSTextCheckingResult *result in results) {
+        NSDictionary *activeAttributes = attributes ? self.activeLinkAttributes : nil;
+        NSDictionary *inactiveAttributes = attributes ? self.inactiveLinkAttributes : nil;
+
+        TUIAttributedLabelLink *link = [[TUIAttributedLabelLink alloc] initWithAttributes:attributes
+                                                                         activeAttributes:activeAttributes
+                                                                       inactiveAttributes:inactiveAttributes
+                                                                       textCheckingResult:result];
+
+        [links addObject:link];
+    }
+
+    [self addLinks:links];
+
+    return links;
+}
+
+- (TUIAttributedLabelLink *)addLinkWithTextCheckingResult:(NSTextCheckingResult *)result {
+    return [self addLinkWithTextCheckingResult:result attributes:self.linkAttributes];
+}
+
+- (TUIAttributedLabelLink *)addLinkToURL:(NSURL *)url withRange:(NSRange)range {
+    return [self addLinkWithTextCheckingResult:[NSTextCheckingResult linkCheckingResultWithRange:range URL:url]];
+}
+
+- (TUIAttributedLabelLink *)addLinkToAddress:(NSDictionary *)addressComponents withRange:(NSRange)range {
+    return [self addLinkWithTextCheckingResult:[NSTextCheckingResult addressCheckingResultWithRange:range components:addressComponents]];
+}
+
+- (TUIAttributedLabelLink *)addLinkToPhoneNumber:(NSString *)phoneNumber withRange:(NSRange)range {
+    return [self addLinkWithTextCheckingResult:[NSTextCheckingResult phoneNumberCheckingResultWithRange:range phoneNumber:phoneNumber]];
+}
+
+- (TUIAttributedLabelLink *)addLinkToDate:(NSDate *)date withRange:(NSRange)range {
+    return [self addLinkWithTextCheckingResult:[NSTextCheckingResult dateCheckingResultWithRange:range date:date]];
+}
+
+- (TUIAttributedLabelLink *)addLinkToDate:(NSDate *)date timeZone:(NSTimeZone *)timeZone duration:(NSTimeInterval)duration withRange:(NSRange)range {
+    return [self addLinkWithTextCheckingResult:[NSTextCheckingResult dateCheckingResultWithRange:range date:date timeZone:timeZone duration:duration]];
+}
+
+- (TUIAttributedLabelLink *)addLinkToTransitInformation:(NSDictionary *)components withRange:(NSRange)range {
+    return [self addLinkWithTextCheckingResult:[NSTextCheckingResult transitInformationCheckingResultWithRange:range components:components]];
+}
+
+#pragma mark -
+
+- (BOOL)containslinkAtPoint:(CGPoint)point {
+    return [self linkAtPoint:point] != nil;
+}
+
+- (TUIAttributedLabelLink *)linkAtPoint:(CGPoint)point {
+    // Stop quickly if none of the points to be tested are in the bounds.
+    if (!CGRectContainsPoint(CGRectInset(self.bounds, -15.f, -15.f), point) || self.links.count == 0) {
+        return nil;
+    }
+
+    TUIAttributedLabelLink *result = [self linkAtCharacterIndex:[self characterIndexAtPoint:point]];
+
+    if (!result && self.extendsLinkTouchArea) {
+        result = [self linkAtRadius:2.5f aroundPoint:point]
+              ?: [self linkAtRadius:5.f aroundPoint:point]
+              ?: [self linkAtRadius:7.5f aroundPoint:point]
+              ?: [self linkAtRadius:12.5f aroundPoint:point]
+              ?: [self linkAtRadius:15.f aroundPoint:point];
+    }
+
+    return result;
+}
+
+- (TUIAttributedLabelLink *)linkAtRadius:(const CGFloat)radius aroundPoint:(CGPoint)point {
+    const CGFloat diagonal = formatCGFloatSqrt(2 * radius * radius);
+    const CGPoint deltas[] = {
+        CGPointMake(0, -radius),           CGPointMake(0, radius),  // Above and below
+        CGPointMake(-radius, 0),           CGPointMake(radius, 0),  // Beside
+        CGPointMake(-diagonal, -diagonal), CGPointMake(-diagonal, diagonal), CGPointMake(diagonal, diagonal), CGPointMake(diagonal, -diagonal)  // Diagonal
+    };
+    const size_t count = sizeof(deltas) / sizeof(CGPoint);
+
+    TUIAttributedLabelLink *link = nil;
+
+    for (NSUInteger i = 0; i < count && link.result == nil; i++) {
+        CGPoint currentPoint = CGPointMake(point.x + deltas[i].x, point.y + deltas[i].y);
+        link = [self linkAtCharacterIndex:[self characterIndexAtPoint:currentPoint]];
+    }
+
+    return link;
+}
+
+- (TUIAttributedLabelLink *)linkAtCharacterIndex:(CFIndex)idx {
+    // Do not enumerate if the index is outside of the bounds of the text.
+    if (!NSLocationInRange((NSUInteger)idx, NSMakeRange(0, self.attributedText.length))) {
+        return nil;
+    }
+
+    NSEnumerator *enumerator = [self.linkModels reverseObjectEnumerator];
+    TUIAttributedLabelLink *link = nil;
+    while ((link = [enumerator nextObject])) {
+        if (NSLocationInRange((NSUInteger)idx, link.result.range)) {
+            return link;
+        }
+    }
+
+    return nil;
+}
+
+- (CFIndex)characterIndexAtPoint:(CGPoint)p {
+    if (!CGRectContainsPoint(self.bounds, p)) {
+        return NSNotFound;
+    }
+
+    CGRect textRect = [self textRectForBounds:self.bounds limitedToNumberOfLines:self.numberOfLines];
+    if (!CGRectContainsPoint(textRect, p)) {
+        return NSNotFound;
+    }
+
+    // Offset tap coordinates by textRect origin to make them relative to the origin of frame
+    p = CGPointMake(p.x - textRect.origin.x, p.y - textRect.origin.y);
+    // Convert tap coordinates (start at top left) to CT coordinates (start at bottom left)
+    p = CGPointMake(p.x, textRect.size.height - p.y);
+
+    CGMutablePathRef path = CGPathCreateMutable();
+    CGPathAddRect(path, NULL, textRect);
+    CTFrameRef frame = CTFramesetterCreateFrame([self framesetter], CFRangeMake(0, (CFIndex)[self.attributedText length]), path, NULL);
+    if (frame == NULL) {
+        CGPathRelease(path);
+        return NSNotFound;
+    }
+
+    CFArrayRef lines = CTFrameGetLines(frame);
+    NSInteger numberOfLines = self.numberOfLines > 0 ? MIN(self.numberOfLines, CFArrayGetCount(lines)) : CFArrayGetCount(lines);
+    if (numberOfLines == 0) {
+        CFRelease(frame);
+        CGPathRelease(path);
+        return NSNotFound;
+    }
+
+    CFIndex idx = NSNotFound;
+
+    CGPoint lineOrigins[numberOfLines];
+    CTFrameGetLineOrigins(frame, CFRangeMake(0, numberOfLines), lineOrigins);
+
+    for (CFIndex lineIndex = 0; lineIndex < numberOfLines; lineIndex++) {
+        CGPoint lineOrigin = lineOrigins[lineIndex];
+        CTLineRef line = CFArrayGetValueAtIndex(lines, lineIndex);
+
+        // Get bounding information of line
+        CGFloat ascent = 0.0f, descent = 0.0f, leading = 0.0f;
+        CGFloat width = (CGFloat)CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
+        CGFloat yMin = (CGFloat)floor(lineOrigin.y - descent);
+        CGFloat yMax = (CGFloat)ceil(lineOrigin.y + ascent);
+
+        // Apply penOffset using flushFactor for horizontal alignment to set lineOrigin since this is the horizontal offset from drawFramesetter
+        CGFloat flushFactor = flushFactorForTextAlignment(self.textAlignment);
+        CGFloat penOffset = (CGFloat)CTLineGetPenOffsetForFlush(line, flushFactor, textRect.size.width);
+        lineOrigin.x = penOffset;
+
+        // Check if we've already passed the line
+        if (p.y > yMax) {
+            break;
+        }
+        // Check if the point is within this line vertically
+        if (p.y >= yMin) {
+            // Check if the point is within this line horizontally
+            if (p.x >= lineOrigin.x && p.x <= lineOrigin.x + width) {
+                // Convert CT coordinates to line-relative coordinates
+                CGPoint relativePoint = CGPointMake(p.x - lineOrigin.x, p.y - lineOrigin.y);
+                idx = CTLineGetStringIndexForPosition(line, relativePoint);
+                break;
+            }
+        }
+    }
+
+    CFRelease(frame);
+    CGPathRelease(path);
+
+    return idx;
+}
+
+- (CGRect)boundingRectForCharacterRange:(NSRange)range {
+    NSMutableAttributedString *mutableAttributedString = [self.attributedText mutableCopy];
+
+    NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:mutableAttributedString];
+
+    NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
+    [textStorage addLayoutManager:layoutManager];
+
+    NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:self.bounds.size];
+    [layoutManager addTextContainer:textContainer];
+
+    NSRange glyphRange;
+    [layoutManager characterRangeForGlyphRange:range actualGlyphRange:&glyphRange];
+
+    return [layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:textContainer];
+}
+
+- (void)drawFramesetter:(CTFramesetterRef)framesetter
+       attributedString:(NSAttributedString *)attributedString
+              textRange:(CFRange)textRange
+                 inRect:(CGRect)rect
+                context:(CGContextRef)c {
+    CGMutablePathRef path = CGPathCreateMutable();
+    CGPathAddRect(path, NULL, rect);
+    CTFrameRef frame = CTFramesetterCreateFrame(framesetter, textRange, path, NULL);
+
+    [self drawBackground:frame inRect:rect context:c];
+
+    CFArrayRef lines = CTFrameGetLines(frame);
+    NSInteger numberOfLines = self.numberOfLines > 0 ? MIN(self.numberOfLines, CFArrayGetCount(lines)) : CFArrayGetCount(lines);
+    BOOL truncateLastLine = (self.lineBreakMode == TUILineBreakByTruncatingHead || self.lineBreakMode == TUILineBreakByTruncatingMiddle ||
+                             self.lineBreakMode == TUILineBreakByTruncatingTail);
+
+    CGPoint lineOrigins[numberOfLines];
+    CTFrameGetLineOrigins(frame, CFRangeMake(0, numberOfLines), lineOrigins);
+
+    for (CFIndex lineIndex = 0; lineIndex < numberOfLines; lineIndex++) {
+        CGPoint lineOrigin = lineOrigins[lineIndex];
+        CGContextSetTextPosition(c, lineOrigin.x, lineOrigin.y);
+        CTLineRef line = CFArrayGetValueAtIndex(lines, lineIndex);
+
+        CGFloat descent = 0.0f;
+        CTLineGetTypographicBounds((CTLineRef)line, NULL, &descent, NULL);
+
+        // Adjust pen offset for flush depending on text alignment
+        CGFloat flushFactor = flushFactorForTextAlignment(self.textAlignment);
+
+        if (lineIndex == numberOfLines - 1 && truncateLastLine) {
+            // Check if the range of text in the last line reaches the end of the full attributed string
+            CFRange lastLineRange = CTLineGetStringRange(line);
+
+            if (!(lastLineRange.length == 0 && lastLineRange.location == 0) &&
+                lastLineRange.location + lastLineRange.length < textRange.location + textRange.length) {
+                // Get correct truncationType and attribute position
+                CTLineTruncationType truncationType;
+                CFIndex truncationAttributePosition = lastLineRange.location;
+                TUILineBreakMode lineBreakMode = self.lineBreakMode;
+
+                // Multiple lines, only use UILineBreakModeTailTruncation
+                if (numberOfLines != 1) {
+                    lineBreakMode = TUILineBreakByTruncatingTail;
+                }
+
+                switch (lineBreakMode) {
+                    case TUILineBreakByTruncatingHead:
+                        truncationType = kCTLineTruncationStart;
+                        break;
+                    case TUILineBreakByTruncatingMiddle:
+                        truncationType = kCTLineTruncationMiddle;
+                        truncationAttributePosition += (lastLineRange.length / 2);
+                        break;
+                    case TUILineBreakByTruncatingTail:
+                    default:
+                        truncationType = kCTLineTruncationEnd;
+                        truncationAttributePosition += (lastLineRange.length - 1);
+                        break;
+                }
+
+                NSAttributedString *attributedTruncationString = self.attributedTruncationToken;
+                if (!attributedTruncationString) {
+                    NSString *truncationTokenString = @"\u2026";  // Unicode Character 'HORIZONTAL ELLIPSIS' (U+2026)
+
+                    NSDictionary *truncationTokenStringAttributes = truncationTokenStringAttributes =
+                        [attributedString attributesAtIndex:(NSUInteger)truncationAttributePosition effectiveRange:NULL];
+
+                    attributedTruncationString = [[NSAttributedString alloc] initWithString:truncationTokenString attributes:truncationTokenStringAttributes];
+                }
+                CTLineRef truncationToken = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)attributedTruncationString);
+
+                // Append truncationToken to the string
+                // because if string isn't too long, CT won't add the truncationToken on its own.
+                // There is no chance of a double truncationToken because CT only adds the
+                // token if it removes characters (and the one we add will go first)
+                NSMutableAttributedString *truncationString = [[NSMutableAttributedString alloc]
+                    initWithAttributedString:[attributedString attributedSubstringFromRange:NSMakeRange((NSUInteger)lastLineRange.location,
+                                                                                                        (NSUInteger)lastLineRange.length)]];
+                if (lastLineRange.length > 0) {
+                    // Remove any newline at the end (we don't want newline space between the text and the truncation token). There can only be one, because the
+                    // second would be on the next line.
+                    unichar lastCharacter = [[truncationString string] characterAtIndex:(NSUInteger)(lastLineRange.length - 1)];
+                    if ([[NSCharacterSet newlineCharacterSet] characterIsMember:lastCharacter]) {
+                        [truncationString deleteCharactersInRange:NSMakeRange((NSUInteger)(lastLineRange.length - 1), 1)];
+                    }
+                }
+                [truncationString appendAttributedString:attributedTruncationString];
+                CTLineRef truncationLine = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)truncationString);
+
+                // Truncate the line in case it is too long.
+                CTLineRef truncatedLine = CTLineCreateTruncatedLine(truncationLine, rect.size.width, truncationType, truncationToken);
+                if (!truncatedLine) {
+                    // If the line is not as wide as the truncationToken, truncatedLine is NULL
+                    truncatedLine = CFRetain(truncationToken);
+                }
+
+                CGFloat penOffset = (CGFloat)CTLineGetPenOffsetForFlush(truncatedLine, flushFactor, rect.size.width);
+                CGContextSetTextPosition(c, penOffset, lineOrigin.y - descent - self.font.descender);
+
+                CTLineDraw(truncatedLine, c);
+
+                NSRange linkRange;
+                if ([attributedTruncationString attribute:NSLinkAttributeName atIndex:0 effectiveRange:&linkRange]) {
+                    NSRange tokenRange = [truncationString.string rangeOfString:attributedTruncationString.string];
+                    NSRange tokenLinkRange =
+                        NSMakeRange((NSUInteger)(lastLineRange.location + lastLineRange.length) - tokenRange.length, (NSUInteger)tokenRange.length);
+
+                    [self addLinkToURL:[attributedTruncationString attribute:NSLinkAttributeName atIndex:0 effectiveRange:&linkRange] withRange:tokenLinkRange];
+                }
+
+                CFRelease(truncatedLine);
+                CFRelease(truncationLine);
+                CFRelease(truncationToken);
+            } else {
+                CGFloat penOffset = (CGFloat)CTLineGetPenOffsetForFlush(line, flushFactor, rect.size.width);
+                CGContextSetTextPosition(c, penOffset, lineOrigin.y - descent - self.font.descender);
+                CTLineDraw(line, c);
+            }
+        } else {
+            CGFloat penOffset = (CGFloat)CTLineGetPenOffsetForFlush(line, flushFactor, rect.size.width);
+            CGContextSetTextPosition(c, penOffset, lineOrigin.y - descent - self.font.descender);
+            CTLineDraw(line, c);
+        }
+    }
+
+    [self drawStrike:frame inRect:rect context:c];
+
+    CFRelease(frame);
+    CGPathRelease(path);
+}
+
+- (void)drawBackground:(CTFrameRef)frame inRect:(CGRect)rect context:(CGContextRef)c {
+    NSArray *lines = (__bridge NSArray *)CTFrameGetLines(frame);
+    CGPoint origins[[lines count]];
+    CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), origins);
+
+    CFIndex lineIndex = 0;
+    for (id line in lines) {
+        CGFloat ascent = 0.0f, descent = 0.0f, leading = 0.0f;
+        CGFloat width = (CGFloat)CTLineGetTypographicBounds((__bridge CTLineRef)line, &ascent, &descent, &leading);
+
+        for (id glyphRun in (__bridge NSArray *)CTLineGetGlyphRuns((__bridge CTLineRef)line)) {
+            NSDictionary *attributes = (__bridge NSDictionary *)CTRunGetAttributes((__bridge CTRunRef)glyphRun);
+            CGColorRef strokeColor = formatCGColorRefFromColor([attributes objectForKey:kTUIBackgroundStrokeColorAttributeName]);
+            CGColorRef fillColor = formatCGColorRefFromColor([attributes objectForKey:kTUIBackgroundFillColorAttributeName]);
+            UIEdgeInsets fillPadding = [[attributes objectForKey:kTUIBackgroundFillPaddingAttributeName] UIEdgeInsetsValue];
+            CGFloat cornerRadius = [[attributes objectForKey:kTUIBackgroundCornerRadiusAttributeName] floatValue];
+            CGFloat lineWidth = [[attributes objectForKey:kTUIBackgroundLineWidthAttributeName] floatValue];
+
+            if (strokeColor || fillColor) {
+                CGRect runBounds = CGRectZero;
+                CGFloat runAscent = 0.0f;
+                CGFloat runDescent = 0.0f;
+
+                runBounds.size.width = (CGFloat)CTRunGetTypographicBounds((__bridge CTRunRef)glyphRun, CFRangeMake(0, 0), &runAscent, &runDescent, NULL) +
+                                       fillPadding.left + fillPadding.right;
+                runBounds.size.height = runAscent + runDescent + fillPadding.top + fillPadding.bottom;
+
+                CGFloat xOffset = 0.0f;
+                CFRange glyphRange = CTRunGetStringRange((__bridge CTRunRef)glyphRun);
+                switch (CTRunGetStatus((__bridge CTRunRef)glyphRun)) {
+                    case kCTRunStatusRightToLeft:
+                        xOffset = CTLineGetOffsetForStringIndex((__bridge CTLineRef)line, glyphRange.location + glyphRange.length, NULL);
+                        break;
+                    default:
+                        xOffset = CTLineGetOffsetForStringIndex((__bridge CTLineRef)line, glyphRange.location, NULL);
+                        break;
+                }
+
+                runBounds.origin.x = origins[lineIndex].x + rect.origin.x + xOffset - fillPadding.left - rect.origin.x;
+                runBounds.origin.y = origins[lineIndex].y + rect.origin.y - fillPadding.bottom - rect.origin.y;
+                runBounds.origin.y -= runDescent;
+
+                // Don't draw higlightedLinkBackground too far to the right
+                if (CGRectGetWidth(runBounds) > width) {
+                    runBounds.size.width = width;
+                }
+
+                CGPathRef path =
+                    [[UIBezierPath bezierPathWithRoundedRect:CGRectInset(UIEdgeInsetsInsetRect(runBounds, self.linkBackgroundEdgeInset), lineWidth, lineWidth)
+                                                cornerRadius:cornerRadius] CGPath];
+
+                CGContextSetLineJoin(c, kCGLineJoinRound);
+
+                if (fillColor) {
+                    CGContextSetFillColorWithColor(c, fillColor);
+                    CGContextAddPath(c, path);
+                    CGContextFillPath(c);
+                }
+
+                if (strokeColor) {
+                    CGContextSetStrokeColorWithColor(c, strokeColor);
+                    CGContextAddPath(c, path);
+                    CGContextStrokePath(c);
+                }
+            }
+        }
+
+        lineIndex++;
+    }
+}
+
+- (void)drawStrike:(CTFrameRef)frame inRect:(__unused CGRect)rect context:(CGContextRef)c {
+    NSArray *lines = (__bridge NSArray *)CTFrameGetLines(frame);
+    CGPoint origins[[lines count]];
+    CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), origins);
+
+    CFIndex lineIndex = 0;
+    for (id line in lines) {
+        CGFloat ascent = 0.0f, descent = 0.0f, leading = 0.0f;
+        CGFloat width = (CGFloat)CTLineGetTypographicBounds((__bridge CTLineRef)line, &ascent, &descent, &leading);
+
+        for (id glyphRun in (__bridge NSArray *)CTLineGetGlyphRuns((__bridge CTLineRef)line)) {
+            NSDictionary *attributes = (__bridge NSDictionary *)CTRunGetAttributes((__bridge CTRunRef)glyphRun);
+            BOOL strikeOut = [[attributes objectForKey:kTUIStrikeOutAttributeName] boolValue];
+            NSInteger superscriptStyle = [[attributes objectForKey:(id)kCTSuperscriptAttributeName] integerValue];
+
+            if (strikeOut) {
+                CGRect runBounds = CGRectZero;
+                CGFloat runAscent = 0.0f;
+                CGFloat runDescent = 0.0f;
+
+                runBounds.size.width = (CGFloat)CTRunGetTypographicBounds((__bridge CTRunRef)glyphRun, CFRangeMake(0, 0), &runAscent, &runDescent, NULL);
+                runBounds.size.height = runAscent + runDescent;
+
+                CGFloat xOffset = 0.0f;
+                CFRange glyphRange = CTRunGetStringRange((__bridge CTRunRef)glyphRun);
+                switch (CTRunGetStatus((__bridge CTRunRef)glyphRun)) {
+                    case kCTRunStatusRightToLeft:
+                        xOffset = CTLineGetOffsetForStringIndex((__bridge CTLineRef)line, glyphRange.location + glyphRange.length, NULL);
+                        break;
+                    default:
+                        xOffset = CTLineGetOffsetForStringIndex((__bridge CTLineRef)line, glyphRange.location, NULL);
+                        break;
+                }
+                runBounds.origin.x = origins[lineIndex].x + xOffset;
+                runBounds.origin.y = origins[lineIndex].y;
+                runBounds.origin.y -= runDescent;
+
+                // Don't draw strikeout too far to the right
+                if (CGRectGetWidth(runBounds) > width) {
+                    runBounds.size.width = width;
+                }
+
+                switch (superscriptStyle) {
+                    case 1:
+                        runBounds.origin.y -= runAscent * 0.47f;
+                        break;
+                    case -1:
+                        runBounds.origin.y += runAscent * 0.25f;
+                        break;
+                    default:
+                        break;
+                }
+
+                // Use text color, or default to black
+                id color = [attributes objectForKey:(id)kCTForegroundColorAttributeName];
+                if (color) {
+                    CGContextSetStrokeColorWithColor(c, formatCGColorRefFromColor(color));
+                } else {
+                    CGContextSetGrayStrokeColor(c, 0.0f, 1.0);
+                }
+
+                CTFontRef font = CTFontCreateWithName((__bridge CFStringRef)self.font.fontName, self.font.pointSize, NULL);
+                CGContextSetLineWidth(c, CTFontGetUnderlineThickness(font));
+                CFRelease(font);
+
+                CGFloat y = formatCGFloatRound(runBounds.origin.y + runBounds.size.height / 2.0f);
+                CGContextMoveToPoint(c, runBounds.origin.x, y);
+                CGContextAddLineToPoint(c, runBounds.origin.x + runBounds.size.width, y);
+
+                CGContextStrokePath(c);
+            }
+        }
+
+        lineIndex++;
+    }
+}
+
+#pragma mark - TUIAttributedLabel
+
+- (void)setText:(id)text {
+    NSParameterAssert(!text || [text isKindOfClass:[NSAttributedString class]] || [text isKindOfClass:[NSString class]]);
+
+    if ([text isKindOfClass:[NSString class]]) {
+        [self setText:text afterInheritingLabelAttributesAndConfiguringWithBlock:nil];
+        return;
+    }
+
+    self.attributedText = text;
+    self.activeLink = nil;
+
+    self.linkModels = [NSArray array];
+    if (text && self.attributedText && self.enabledTextCheckingTypes) {
+        __weak __typeof(self) weakSelf = self;
+        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
+          __strong __typeof(weakSelf) strongSelf = weakSelf;
+
+          NSDataDetector *dataDetector = strongSelf.dataDetector;
+          if (dataDetector && [dataDetector respondsToSelector:@selector(matchesInString:options:range:)]) {
+              NSArray *results = [dataDetector matchesInString:[(NSAttributedString *)text string]
+                                                       options:0
+                                                         range:NSMakeRange(0, [(NSAttributedString *)text length])];
+              if ([results count] > 0) {
+                  dispatch_async(dispatch_get_main_queue(), ^{
+                    if ([[strongSelf.attributedText string] isEqualToString:[(NSAttributedString *)text string]]) {
+                        [strongSelf addLinksWithTextCheckingResults:results attributes:strongSelf.linkAttributes];
+                    }
+                  });
+              }
+          }
+        });
+    }
+
+    [self.attributedText enumerateAttribute:NSLinkAttributeName
+                                    inRange:NSMakeRange(0, self.attributedText.length)
+                                    options:0
+                                 usingBlock:^(id value, __unused NSRange range, __unused BOOL *stop) {
+                                   if (value) {
+                                       NSURL *URL = [value isKindOfClass:[NSString class]] ? [NSURL URLWithString:value] : value;
+                                       [self addLinkToURL:URL withRange:range];
+                                   }
+                                 }];
+}
+
+- (void)setText:(id)text
+    afterInheritingLabelAttributesAndConfiguringWithBlock:(NSMutableAttributedString * (^)(NSMutableAttributedString *mutableAttributedString))block {
+    NSMutableAttributedString *mutableAttributedString = nil;
+    if ([text isKindOfClass:[NSString class]]) {
+        mutableAttributedString = [[NSMutableAttributedString alloc] initWithString:text attributes:formatNSAttributedStringAttributesFromLabel(self)];
+    } else {
+        mutableAttributedString = [[NSMutableAttributedString alloc] initWithAttributedString:text];
+        [mutableAttributedString addAttributes:formatNSAttributedStringAttributesFromLabel(self) range:NSMakeRange(0, [mutableAttributedString length])];
+    }
+
+    if (block) {
+        mutableAttributedString = block(mutableAttributedString);
+    }
+
+    [self setText:mutableAttributedString];
+}
+
+- (void)setActiveLink:(TUIAttributedLabelLink *)activeLink {
+    _activeLink = activeLink;
+
+    NSDictionary *activeAttributes = activeLink.activeAttributes ?: self.activeLinkAttributes;
+
+    if (_activeLink && activeAttributes.count > 0) {
+        if (!self.inactiveAttributedText) {
+            self.inactiveAttributedText = [self.attributedText copy];
+        }
+
+        NSMutableAttributedString *mutableAttributedString = [self.inactiveAttributedText mutableCopy];
+        if (self.activeLink.result.range.length > 0 &&
+            NSLocationInRange(NSMaxRange(self.activeLink.result.range) - 1, NSMakeRange(0, [self.inactiveAttributedText length]))) {
+            [mutableAttributedString addAttributes:activeAttributes range:self.activeLink.result.range];
+        }
+
+        self.attributedText = mutableAttributedString;
+        [self setNeedsDisplay];
+
+        [CATransaction flush];
+    } else if (self.inactiveAttributedText) {
+        self.attributedText = self.inactiveAttributedText;
+        self.inactiveAttributedText = nil;
+
+        [self setNeedsDisplay];
+    }
+}
+
+- (void)setLinkAttributes:(NSDictionary *)linkAttributes {
+    _linkAttributes = convertNSAttributedStringAttributesToCTAttributes(linkAttributes);
+}
+
+- (void)setActiveLinkAttributes:(NSDictionary *)activeLinkAttributes {
+    _activeLinkAttributes = convertNSAttributedStringAttributesToCTAttributes(activeLinkAttributes);
+}
+
+- (void)setInactiveLinkAttributes:(NSDictionary *)inactiveLinkAttributes {
+    _inactiveLinkAttributes = convertNSAttributedStringAttributesToCTAttributes(inactiveLinkAttributes);
+}
+
+#pragma mark - UILabel
+
+- (void)setHighlighted:(BOOL)highlighted {
+    [super setHighlighted:highlighted];
+    [self setNeedsDisplay];
+}
+
+// Fixes crash when loading from a UIStoryboard
+- (UIColor *)textColor {
+    UIColor *color = [super textColor];
+    if (!color) {
+        color = [UIColor blackColor];
+    }
+
+    return color;
+}
+
+- (void)setTextColor:(UIColor *)textColor {
+    UIColor *oldTextColor = self.textColor;
+    [super setTextColor:textColor];
+
+    // Redraw to allow any ColorFromContext attributes a chance to update
+    if (textColor != oldTextColor) {
+        [self setNeedsFramesetter];
+        [self setNeedsDisplay];
+    }
+}
+
+- (CGRect)textRectForBounds:(CGRect)bounds limitedToNumberOfLines:(NSInteger)numberOfLines {
+    bounds = UIEdgeInsetsInsetRect(bounds, self.textInsets);
+    if (!self.attributedText) {
+        return [super textRectForBounds:bounds limitedToNumberOfLines:numberOfLines];
+    }
+
+    CGRect textRect = bounds;
+
+    // Calculate height with a minimum of double the font pointSize, to ensure that CTFramesetterSuggestFrameSizeWithConstraints doesn't return CGSizeZero, as
+    // it would if textRect height is insufficient.
+    textRect.size.height = MAX(self.font.lineHeight * MAX(2, numberOfLines), bounds.size.height);
+
+    // Adjust the text to be in the center vertically, if the text size is smaller than bounds
+    CGSize textSize =
+        CTFramesetterSuggestFrameSizeWithConstraints([self framesetter], CFRangeMake(0, (CFIndex)[self.attributedText length]), NULL, textRect.size, NULL);
+    textSize =
+        CGSizeMake(formatCGFloatCeil(textSize.width),
+                   formatCGFloatCeil(textSize.height));
+    // Fix for iOS 4, CTFramesetterSuggestFrameSizeWithConstraints sometimes returns fractional sizes
+
+    if (textSize.height < bounds.size.height) {
+        CGFloat yOffset = 0.0f;
+        switch (self.verticalAlignment) {
+            case TUIAttributedLabelVerticalAlignmentCenter:
+                yOffset = formatCGFloatFloor((bounds.size.height - textSize.height) / 2.0f);
+                break;
+            case TUIAttributedLabelVerticalAlignmentBottom:
+                yOffset = bounds.size.height - textSize.height;
+                break;
+            case TUIAttributedLabelVerticalAlignmentTop:
+            default:
+                break;
+        }
+
+        textRect.origin.y += yOffset;
+    }
+
+    return textRect;
+}
+
+- (void)drawTextInRect:(CGRect)rect {
+    CGRect insetRect = UIEdgeInsetsInsetRect(rect, self.textInsets);
+    if (!self.attributedText) {
+        [super drawTextInRect:insetRect];
+        return;
+    }
+
+    NSAttributedString *originalAttributedText = nil;
+
+    // Adjust the font size to fit width, if necessarry
+    if (self.adjustsFontSizeToFitWidth && self.numberOfLines > 0) {
+        // Framesetter could still be working with a resized version of the text;
+        // need to reset so we start from the original font size.
+        // See #393.
+        [self setNeedsFramesetter];
+        [self setNeedsDisplay];
+
+        if ([self respondsToSelector:@selector(invalidateIntrinsicContentSize)]) {
+            [self invalidateIntrinsicContentSize];
+        }
+
+        // Use infinite width to find the max width, which will be compared to availableWidth if needed.
+        CGSize maxSize = (self.numberOfLines > 1) ? CGSizeMake(TUIFLOAT_MAX, TUIFLOAT_MAX) : CGSizeZero;
+
+        CGFloat textWidth = [self sizeThatFits:maxSize].width;
+        CGFloat availableWidth = self.frame.size.width * self.numberOfLines;
+        if (self.numberOfLines > 1 && self.lineBreakMode == TUILineBreakByWordWrapping) {
+            textWidth *= kTUILineBreakWordWrapTextWidthScalingFactor;
+        }
+
+        if (textWidth > availableWidth && textWidth > 0.0f) {
+            originalAttributedText = [self.attributedText copy];
+
+            CGFloat scaleFactor = availableWidth / textWidth;
+            if ([self respondsToSelector:@selector(minimumScaleFactor)] && self.minimumScaleFactor > scaleFactor) {
+                scaleFactor = self.minimumScaleFactor;
+            }
+
+            self.attributedText = formatNSAttributedStringByScalingFontSize(self.attributedText, scaleFactor);
+        }
+    }
+
+    CGContextRef c = UIGraphicsGetCurrentContext();
+    CGContextSaveGState(c);
+    {
+        CGContextSetTextMatrix(c, CGAffineTransformIdentity);
+
+        // Inverts the CTM to match iOS coordinates (otherwise text draws upside-down; Mac OS's system is different)
+        CGContextTranslateCTM(c, 0.0f, insetRect.size.height);
+        CGContextScaleCTM(c, 1.0f, -1.0f);
+
+        CFRange textRange = CFRangeMake(0, (CFIndex)[self.attributedText length]);
+
+        // First, get the text rect (which takes vertical centering into account)
+        CGRect textRect = [self textRectForBounds:rect limitedToNumberOfLines:self.numberOfLines];
+
+        // CoreText draws its text aligned to the bottom, so we move the CTM here to take our vertical offsets into account
+        CGContextTranslateCTM(c, insetRect.origin.x, insetRect.size.height - textRect.origin.y - textRect.size.height);
+
+        // Second, trace the shadow before the actual text, if we have one
+        if (self.shadowColor && !self.highlighted) {
+            CGContextSetShadowWithColor(c, self.shadowOffset, self.shadowRadius, [self.shadowColor CGColor]);
+        } else if (self.highlightedShadowColor) {
+            CGContextSetShadowWithColor(c, self.highlightedShadowOffset, self.highlightedShadowRadius, [self.highlightedShadowColor CGColor]);
+        }
+
+        // Finally, draw the text or highlighted text itself (on top of the shadow, if there is one)
+        if (self.highlightedTextColor && self.highlighted) {
+            NSMutableAttributedString *highlightAttributedString = [self.renderedAttributedText mutableCopy];
+            [highlightAttributedString addAttribute:(__bridge NSString *)kCTForegroundColorAttributeName
+                                              value:(id)[self.highlightedTextColor CGColor]
+                                              range:NSMakeRange(0, highlightAttributedString.length)];
+
+            if (![self highlightFramesetter]) {
+                CTFramesetterRef highlightFramesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)highlightAttributedString);
+                [self setHighlightFramesetter:highlightFramesetter];
+                CFRelease(highlightFramesetter);
+            }
+
+            [self drawFramesetter:[self highlightFramesetter] attributedString:highlightAttributedString textRange:textRange inRect:textRect context:c];
+        } else {
+            [self drawFramesetter:[self framesetter] attributedString:self.renderedAttributedText textRange:textRange inRect:textRect context:c];
+        }
+
+        // If we adjusted the font size, set it back to its original size
+        if (originalAttributedText) {
+            // Use ivar directly to avoid clearing out framesetter and renderedAttributedText
+            _attributedText = originalAttributedText;
+        }
+    }
+    CGContextRestoreGState(c);
+}
+
+#pragma mark - UIAccessibilityElement
+
+- (BOOL)isAccessibilityElement {
+    return NO;
+}
+
+- (NSInteger)accessibilityElementCount {
+    return (NSInteger)[[self accessibilityElements] count];
+}
+
+- (id)accessibilityElementAtIndex:(NSInteger)index {
+    return [[self accessibilityElements] objectAtIndex:(NSUInteger)index];
+}
+
+- (NSInteger)indexOfAccessibilityElement:(id)element {
+    return (NSInteger)[[self accessibilityElements] indexOfObject:element];
+}
+
+- (NSArray *)accessibilityElements {
+    if (!_accessibilityElements) {
+        @synchronized(self) {
+            NSMutableArray *mutableAccessibilityItems = [NSMutableArray array];
+
+            for (TUIAttributedLabelLink *link in self.linkModels) {
+                if (link.result.range.location == NSNotFound) {
+                    continue;
+                }
+
+                NSString *sourceText = [self.text isKindOfClass:[NSString class]] ? self.text : [(NSAttributedString *)self.text string];
+
+                NSString *accessibilityLabel = [sourceText substringWithRange:link.result.range];
+                NSString *accessibilityValue = link.accessibilityValue;
+
+                if (accessibilityLabel) {
+                    TUIAccessibilityElement *linkElement = [[TUIAccessibilityElement alloc] initWithAccessibilityContainer:self];
+                    linkElement.accessibilityTraits = UIAccessibilityTraitLink;
+                    linkElement.boundingRect = [self boundingRectForCharacterRange:link.result.range];
+                    linkElement.superview = self;
+                    linkElement.accessibilityLabel = accessibilityLabel;
+
+                    if (![accessibilityLabel isEqualToString:accessibilityValue]) {
+                        linkElement.accessibilityValue = accessibilityValue;
+                    }
+
+                    [mutableAccessibilityItems addObject:linkElement];
+                }
+            }
+
+            TUIAccessibilityElement *baseElement = [[TUIAccessibilityElement alloc] initWithAccessibilityContainer:self];
+            baseElement.accessibilityLabel = [super accessibilityLabel];
+            baseElement.accessibilityHint = [super accessibilityHint];
+            baseElement.accessibilityValue = [super accessibilityValue];
+            baseElement.boundingRect = self.bounds;
+            baseElement.superview = self;
+            baseElement.accessibilityTraits = [super accessibilityTraits];
+
+            [mutableAccessibilityItems addObject:baseElement];
+
+            self.accessibilityElements = [NSArray arrayWithArray:mutableAccessibilityItems];
+        }
+    }
+
+    return _accessibilityElements;
+}
+
+#pragma mark - UIView
+
+- (CGSize)sizeThatFits:(CGSize)size {
+    if (!self.attributedText) {
+        return [super sizeThatFits:size];
+    } else {
+        NSAttributedString *string = [self renderedAttributedText];
+
+        CGSize labelSize =
+            formatCTFramesetterSuggestFrameSizeForAttributedStringWithConstraints([self framesetter], string, size, (NSUInteger)self.numberOfLines);
+        labelSize.width += self.textInsets.left + self.textInsets.right;
+        labelSize.height += self.textInsets.top + self.textInsets.bottom;
+
+        return labelSize;
+    }
+}
+
+- (CGSize)intrinsicContentSize {
+    // There's an implicit width from the original UILabel implementation
+    return [self sizeThatFits:[super intrinsicContentSize]];
+}
+
+- (void)tintColorDidChange {
+    if (!self.inactiveLinkAttributes || [self.inactiveLinkAttributes count] == 0) {
+        return;
+    }
+
+    BOOL isInactive = (self.tintAdjustmentMode == UIViewTintAdjustmentModeDimmed);
+
+    NSMutableAttributedString *mutableAttributedString = [self.attributedText mutableCopy];
+    for (TUIAttributedLabelLink *link in self.linkModels) {
+        NSDictionary *attributesToRemove = isInactive ? link.attributes : link.inactiveAttributes;
+        NSDictionary *attributesToAdd = isInactive ? link.inactiveAttributes : link.attributes;
+
+        [attributesToRemove enumerateKeysAndObjectsUsingBlock:^(NSString *name, __unused id value, __unused BOOL *stop) {
+          if (NSMaxRange(link.result.range) <= mutableAttributedString.length) {
+              [mutableAttributedString removeAttribute:name range:link.result.range];
+          }
+        }];
+
+        if (attributesToAdd) {
+            if (NSMaxRange(link.result.range) <= mutableAttributedString.length) {
+                [mutableAttributedString addAttributes:attributesToAdd range:link.result.range];
+            }
+        }
+    }
+
+    self.attributedText = mutableAttributedString;
+
+    [self setNeedsDisplay];
+}
+
+- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
+    if (![self linkAtPoint:point] || !self.userInteractionEnabled || self.hidden || self.alpha < 0.01) {
+        return [super hitTest:point withEvent:event];
+    }
+
+    return self;
+}
+
+#pragma mark - UIResponder
+
+- (BOOL)canBecomeFirstResponder {
+    return YES;
+}
+
+- (BOOL)canPerformAction:(SEL)action withSender:(__unused id)sender {
+#if !TARGET_OS_TV
+    return (action == @selector(copy:));
+#else
+    return NO;
+#endif
+}
+
+- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
+    UITouch *touch = [touches anyObject];
+
+    self.activeLink = [self linkAtPoint:[touch locationInView:self]];
+
+    if (!self.activeLink) {
+        [super touchesBegan:touches withEvent:event];
+    }
+}
+
+- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
+    if (self.activeLink) {
+        UITouch *touch = [touches anyObject];
+
+        if (self.activeLink != [self linkAtPoint:[touch locationInView:self]]) {
+            self.activeLink = nil;
+        }
+    } else {
+        [super touchesMoved:touches withEvent:event];
+    }
+}
+
+- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
+    if (self.activeLink) {
+        if (self.activeLink.linkTapBlock) {
+            self.activeLink.linkTapBlock(self, self.activeLink);
+            self.activeLink = nil;
+            return;
+        }
+
+        NSTextCheckingResult *result = self.activeLink.result;
+        self.activeLink = nil;
+
+        switch (result.resultType) {
+            case NSTextCheckingTypeLink:
+                if ([self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithURL:)]) {
+                    [self.delegate attributedLabel:self didSelectLinkWithURL:result.URL];
+                    return;
+                }
+                break;
+            case NSTextCheckingTypeAddress:
+                if ([self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithAddress:)]) {
+                    [self.delegate attributedLabel:self didSelectLinkWithAddress:result.addressComponents];
+                    return;
+                }
+                break;
+            case NSTextCheckingTypePhoneNumber:
+                if ([self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithPhoneNumber:)]) {
+                    [self.delegate attributedLabel:self didSelectLinkWithPhoneNumber:result.phoneNumber];
+                    return;
+                }
+                break;
+            case NSTextCheckingTypeDate:
+                if (result.timeZone && [self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithDate:timeZone:duration:)]) {
+                    [self.delegate attributedLabel:self didSelectLinkWithDate:result.date timeZone:result.timeZone duration:result.duration];
+                    return;
+                } else if ([self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithDate:)]) {
+                    [self.delegate attributedLabel:self didSelectLinkWithDate:result.date];
+                    return;
+                }
+                break;
+            case NSTextCheckingTypeTransitInformation:
+                if ([self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithTransitInformation:)]) {
+                    [self.delegate attributedLabel:self didSelectLinkWithTransitInformation:result.components];
+                    return;
+                }
+            default:
+                break;
+        }
+
+        // Fallback to `attributedLabel:didSelectLinkWithTextCheckingResult:` if no other delegate method matched.
+        if ([self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithTextCheckingResult:)]) {
+            [self.delegate attributedLabel:self didSelectLinkWithTextCheckingResult:result];
+        }
+    } else {
+        [super touchesEnded:touches withEvent:event];
+    }
+}
+
+- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
+    if (self.activeLink) {
+        self.activeLink = nil;
+    } else {
+        [super touchesCancelled:touches withEvent:event];
+    }
+}
+
+#pragma mark - UIGestureRecognizerDelegate
+
+- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
+    return [self containslinkAtPoint:[touch locationInView:self]];
+}
+
+#pragma mark - UILongPressGestureRecognizer
+
+- (void)longPressGestureDidFire:(UILongPressGestureRecognizer *)sender {
+    switch (sender.state) {
+        case UIGestureRecognizerStateBegan: {
+            CGPoint touchPoint = [sender locationInView:self];
+            TUIAttributedLabelLink *link = [self linkAtPoint:touchPoint];
+
+            if (link) {
+                if (link.linkLongPressBlock) {
+                    link.linkLongPressBlock(self, link);
+                    return;
+                }
+
+                NSTextCheckingResult *result = link.result;
+
+                if (!result) {
+                    return;
+                }
+
+                switch (result.resultType) {
+                    case NSTextCheckingTypeLink:
+                        if ([self.delegate respondsToSelector:@selector(attributedLabel:didLongPressLinkWithURL:atPoint:)]) {
+                            [self.delegate attributedLabel:self didLongPressLinkWithURL:result.URL atPoint:touchPoint];
+                            return;
+                        }
+                        break;
+                    case NSTextCheckingTypeAddress:
+                        if ([self.delegate respondsToSelector:@selector(attributedLabel:didLongPressLinkWithAddress:atPoint:)]) {
+                            [self.delegate attributedLabel:self didLongPressLinkWithAddress:result.addressComponents atPoint:touchPoint];
+                            return;
+                        }
+                        break;
+                    case NSTextCheckingTypePhoneNumber:
+                        if ([self.delegate respondsToSelector:@selector(attributedLabel:didLongPressLinkWithPhoneNumber:atPoint:)]) {
+                            [self.delegate attributedLabel:self didLongPressLinkWithPhoneNumber:result.phoneNumber atPoint:touchPoint];
+                            return;
+                        }
+                        break;
+                    case NSTextCheckingTypeDate:
+                        if (result.timeZone && [self.delegate respondsToSelector:@selector(attributedLabel:
+                                                                                     didLongPressLinkWithDate:timeZone:duration:atPoint:)]) {
+                            [self.delegate attributedLabel:self
+                                  didLongPressLinkWithDate:result.date
+                                                  timeZone:result.timeZone
+                                                  duration:result.duration
+                                                   atPoint:touchPoint];
+                            return;
+                        } else if ([self.delegate respondsToSelector:@selector(attributedLabel:didLongPressLinkWithDate:atPoint:)]) {
+                            [self.delegate attributedLabel:self didLongPressLinkWithDate:result.date atPoint:touchPoint];
+                            return;
+                        }
+                        break;
+                    case NSTextCheckingTypeTransitInformation:
+                        if ([self.delegate respondsToSelector:@selector(attributedLabel:didLongPressLinkWithTransitInformation:atPoint:)]) {
+                            [self.delegate attributedLabel:self didLongPressLinkWithTransitInformation:result.components atPoint:touchPoint];
+                            return;
+                        }
+                    default:
+                        break;
+                }
+
+                // Fallback to `attributedLabel:didLongPressLinkWithTextCheckingResult:atPoint:` if no other delegate method matched.
+                if ([self.delegate respondsToSelector:@selector(attributedLabel:didLongPressLinkWithTextCheckingResult:atPoint:)]) {
+                    [self.delegate attributedLabel:self didLongPressLinkWithTextCheckingResult:result atPoint:touchPoint];
+                }
+            }
+            break;
+        }
+        default:
+            break;
+    }
+}
+
+#if !TARGET_OS_TV
+#pragma mark - UIResponderStandardEditActions
+
+- (void)copy:(__unused id)sender {
+    [[UIPasteboard generalPasteboard] setString:self.text];
+}
+#endif
+
+#pragma mark - NSCoding
+
+- (void)encodeWithCoder:(NSCoder *)coder {
+    [super encodeWithCoder:coder];
+
+    [coder encodeObject:@(self.enabledTextCheckingTypes) forKey:NSStringFromSelector(@selector(enabledTextCheckingTypes))];
+
+    [coder encodeObject:self.linkModels forKey:NSStringFromSelector(@selector(linkModels))];
+    if ([NSMutableParagraphStyle class]) {
+        [coder encodeObject:self.linkAttributes forKey:NSStringFromSelector(@selector(linkAttributes))];
+        [coder encodeObject:self.activeLinkAttributes forKey:NSStringFromSelector(@selector(activeLinkAttributes))];
+        [coder encodeObject:self.inactiveLinkAttributes forKey:NSStringFromSelector(@selector(inactiveLinkAttributes))];
+    }
+    [coder encodeObject:@(self.shadowRadius) forKey:NSStringFromSelector(@selector(shadowRadius))];
+    [coder encodeObject:@(self.highlightedShadowRadius) forKey:NSStringFromSelector(@selector(highlightedShadowRadius))];
+    [coder encodeCGSize:self.highlightedShadowOffset forKey:NSStringFromSelector(@selector(highlightedShadowOffset))];
+    [coder encodeObject:self.highlightedShadowColor forKey:NSStringFromSelector(@selector(highlightedShadowColor))];
+    [coder encodeObject:@(self.kern) forKey:NSStringFromSelector(@selector(kern))];
+    [coder encodeObject:@(self.firstLineIndent) forKey:NSStringFromSelector(@selector(firstLineIndent))];
+    [coder encodeObject:@(self.lineSpacing) forKey:NSStringFromSelector(@selector(lineSpacing))];
+    [coder encodeObject:@(self.lineHeightMultiple) forKey:NSStringFromSelector(@selector(lineHeightMultiple))];
+    [coder encodeUIEdgeInsets:self.textInsets forKey:NSStringFromSelector(@selector(textInsets))];
+    [coder encodeInteger:self.verticalAlignment forKey:NSStringFromSelector(@selector(verticalAlignment))];
+
+    [coder encodeObject:self.attributedTruncationToken forKey:NSStringFromSelector(@selector(attributedTruncationToken))];
+
+    [coder encodeObject:NSStringFromUIEdgeInsets(self.linkBackgroundEdgeInset) forKey:NSStringFromSelector(@selector(linkBackgroundEdgeInset))];
+    [coder encodeObject:self.attributedText forKey:NSStringFromSelector(@selector(attributedText))];
+    [coder encodeObject:self.text forKey:NSStringFromSelector(@selector(text))];
+}
+
+- (id)initWithCoder:(NSCoder *)coder {
+    self = [super initWithCoder:coder];
+    if (!self) {
+        return nil;
+    }
+
+    [self commonInit];
+
+    if ([coder containsValueForKey:NSStringFromSelector(@selector(enabledTextCheckingTypes))]) {
+        self.enabledTextCheckingTypes = [[coder decodeObjectForKey:NSStringFromSelector(@selector(enabledTextCheckingTypes))] unsignedLongLongValue];
+    }
+
+    if ([NSMutableParagraphStyle class]) {
+        if ([coder containsValueForKey:NSStringFromSelector(@selector(linkAttributes))]) {
+            self.linkAttributes = [coder decodeObjectForKey:NSStringFromSelector(@selector(linkAttributes))];
+        }
+
+        if ([coder containsValueForKey:NSStringFromSelector(@selector(activeLinkAttributes))]) {
+            self.activeLinkAttributes = [coder decodeObjectForKey:NSStringFromSelector(@selector(activeLinkAttributes))];
+        }
+
+        if ([coder containsValueForKey:NSStringFromSelector(@selector(inactiveLinkAttributes))]) {
+            self.inactiveLinkAttributes = [coder decodeObjectForKey:NSStringFromSelector(@selector(inactiveLinkAttributes))];
+        }
+    }
+
+    if ([coder containsValueForKey:NSStringFromSelector(@selector(links))]) {
+        NSArray *oldLinks = [coder decodeObjectForKey:NSStringFromSelector(@selector(links))];
+        [self addLinksWithTextCheckingResults:oldLinks attributes:nil];
+    }
+
+    if ([coder containsValueForKey:NSStringFromSelector(@selector(linkModels))]) {
+        self.linkModels = [coder decodeObjectForKey:NSStringFromSelector(@selector(linkModels))];
+    }
+
+    if ([coder containsValueForKey:NSStringFromSelector(@selector(shadowRadius))]) {
+        self.shadowRadius = [[coder decodeObjectForKey:NSStringFromSelector(@selector(shadowRadius))] floatValue];
+    }
+
+    if ([coder containsValueForKey:NSStringFromSelector(@selector(highlightedShadowRadius))]) {
+        self.highlightedShadowRadius = [[coder decodeObjectForKey:NSStringFromSelector(@selector(highlightedShadowRadius))] floatValue];
+    }
+
+    if ([coder containsValueForKey:NSStringFromSelector(@selector(highlightedShadowOffset))]) {
+        self.highlightedShadowOffset = [coder decodeCGSizeForKey:NSStringFromSelector(@selector(highlightedShadowOffset))];
+    }
+
+    if ([coder containsValueForKey:NSStringFromSelector(@selector(highlightedShadowColor))]) {
+        self.highlightedShadowColor = [coder decodeObjectForKey:NSStringFromSelector(@selector(highlightedShadowColor))];
+    }
+
+    if ([coder containsValueForKey:NSStringFromSelector(@selector(kern))]) {
+        self.kern = [[coder decodeObjectForKey:NSStringFromSelector(@selector(kern))] floatValue];
+    }
+
+    if ([coder containsValueForKey:NSStringFromSelector(@selector(firstLineIndent))]) {
+        self.firstLineIndent = [[coder decodeObjectForKey:NSStringFromSelector(@selector(firstLineIndent))] floatValue];
+    }
+
+    if ([coder containsValueForKey:NSStringFromSelector(@selector(lineSpacing))]) {
+        self.lineSpacing = [[coder decodeObjectForKey:NSStringFromSelector(@selector(lineSpacing))] floatValue];
+    }
+
+    if ([coder containsValueForKey:NSStringFromSelector(@selector(minimumLineHeight))]) {
+        self.minimumLineHeight = [[coder decodeObjectForKey:NSStringFromSelector(@selector(minimumLineHeight))] floatValue];
+    }
+
+    if ([coder containsValueForKey:NSStringFromSelector(@selector(maximumLineHeight))]) {
+        self.maximumLineHeight = [[coder decodeObjectForKey:NSStringFromSelector(@selector(maximumLineHeight))] floatValue];
+    }
+
+    if ([coder containsValueForKey:NSStringFromSelector(@selector(lineHeightMultiple))]) {
+        self.lineHeightMultiple = [[coder decodeObjectForKey:NSStringFromSelector(@selector(lineHeightMultiple))] floatValue];
+    }
+
+    if ([coder containsValueForKey:NSStringFromSelector(@selector(textInsets))]) {
+        self.textInsets = [coder decodeUIEdgeInsetsForKey:NSStringFromSelector(@selector(textInsets))];
+    }
+
+    if ([coder containsValueForKey:NSStringFromSelector(@selector(verticalAlignment))]) {
+        self.verticalAlignment = [coder decodeIntegerForKey:NSStringFromSelector(@selector(verticalAlignment))];
+    }
+
+    if ([coder containsValueForKey:NSStringFromSelector(@selector(attributedTruncationToken))]) {
+        self.attributedTruncationToken = [coder decodeObjectForKey:NSStringFromSelector(@selector(attributedTruncationToken))];
+    }
+
+    if ([coder containsValueForKey:NSStringFromSelector(@selector(linkBackgroundEdgeInset))]) {
+        self.linkBackgroundEdgeInset = UIEdgeInsetsFromString([coder decodeObjectForKey:NSStringFromSelector(@selector(linkBackgroundEdgeInset))]);
+    }
+
+    if ([coder containsValueForKey:NSStringFromSelector(@selector(attributedText))]) {
+        self.attributedText = [coder decodeObjectForKey:NSStringFromSelector(@selector(attributedText))];
+    } else {
+        self.text = super.text;
+    }
+
+    return self;
+}
+
+@end
+
+#pragma mark - TUIAttributedLabelLink
+
+@implementation TUIAttributedLabelLink
+
+- (instancetype)initWithAttributes:(NSDictionary *)attributes
+                  activeAttributes:(NSDictionary *)activeAttributes
+                inactiveAttributes:(NSDictionary *)inactiveAttributes
+                textCheckingResult:(NSTextCheckingResult *)result {
+    if ((self = [super init])) {
+        _result = result;
+        _attributes = [attributes copy];
+        _activeAttributes = [activeAttributes copy];
+        _inactiveAttributes = [inactiveAttributes copy];
+    }
+
+    return self;
+}
+
+- (instancetype)initWithAttributesFromLabel:(TUIAttributedLabel *)label textCheckingResult:(NSTextCheckingResult *)result {
+    return [self initWithAttributes:label.linkAttributes
+                   activeAttributes:label.activeLinkAttributes
+                 inactiveAttributes:label.inactiveLinkAttributes
+                 textCheckingResult:result];
+}
+
+#pragma mark - Accessibility
+
+- (NSString *)accessibilityValue {
+    if ([_accessibilityValue length] == 0) {
+        switch (self.result.resultType) {
+            case NSTextCheckingTypeLink:
+                _accessibilityValue = self.result.URL.absoluteString;
+                break;
+            case NSTextCheckingTypePhoneNumber:
+                _accessibilityValue = self.result.phoneNumber;
+                break;
+            case NSTextCheckingTypeDate:
+                _accessibilityValue = [NSDateFormatter localizedStringFromDate:self.result.date
+                                                                     dateStyle:NSDateFormatterLongStyle
+                                                                     timeStyle:NSDateFormatterLongStyle];
+                break;
+            default:
+                break;
+        }
+    }
+
+    return _accessibilityValue;
+}
+
+#pragma mark - NSCoding
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+    [aCoder encodeObject:self.result forKey:NSStringFromSelector(@selector(result))];
+    [aCoder encodeObject:self.attributes forKey:NSStringFromSelector(@selector(attributes))];
+    [aCoder encodeObject:self.activeAttributes forKey:NSStringFromSelector(@selector(activeAttributes))];
+    [aCoder encodeObject:self.inactiveAttributes forKey:NSStringFromSelector(@selector(inactiveAttributes))];
+    [aCoder encodeObject:self.accessibilityValue forKey:NSStringFromSelector(@selector(accessibilityValue))];
+}
+
+- (id)initWithCoder:(NSCoder *)aDecoder {
+    if ((self = [super init])) {
+        _result = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(result))];
+        _attributes = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(attributes))];
+        _activeAttributes = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(activeAttributes))];
+        _inactiveAttributes = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(inactiveAttributes))];
+        self.accessibilityValue = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(accessibilityValue))];
+    }
+
+    return self;
+}
+
+@end
+
+#pragma mark -
+
+static inline CGColorRef formatCGColorRefFromColor(id color) { return [color isKindOfClass:[UIColor class]] ? [color CGColor] : (__bridge CGColorRef)color; }
+
+static inline CTFontRef formatCTFontRefFromUIFont(UIFont *font) {
+    CTFontRef ctfont = CTFontCreateWithName((__bridge CFStringRef)font.fontName, font.pointSize, NULL);
+    return CFAutorelease(ctfont);
+}
+
+static inline NSDictionary *convertNSAttributedStringAttributesToCTAttributes(NSDictionary *attributes) {
+    if (!attributes) return nil;
+
+    NSMutableDictionary *mutableAttributes = [NSMutableDictionary dictionary];
+
+    NSDictionary *convertMap = @{
+        NSFontAttributeName : (NSString *)kCTFontAttributeName,
+        NSBackgroundColorAttributeName : (NSString *)kTUIBackgroundFillColorAttributeName,
+        NSForegroundColorAttributeName : (NSString *)kCTForegroundColorAttributeName,
+        NSUnderlineColorAttributeName : (NSString *)kCTUnderlineColorAttributeName,
+        NSUnderlineStyleAttributeName : (NSString *)kCTUnderlineStyleAttributeName,
+        NSStrokeWidthAttributeName : (NSString *)kCTStrokeWidthAttributeName,
+        NSStrokeColorAttributeName : (NSString *)kCTStrokeWidthAttributeName,
+        NSKernAttributeName : (NSString *)kCTKernAttributeName,
+        NSLigatureAttributeName : (NSString *)kCTLigatureAttributeName
+    };
+
+    [attributes enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) {
+      key = [convertMap objectForKey:key] ?: key;
+
+      if (![NSMutableParagraphStyle class]) {
+          if ([value isKindOfClass:[UIFont class]]) {
+              value = (__bridge id)formatCTFontRefFromUIFont(value);
+          } else if ([value isKindOfClass:[UIColor class]]) {
+              value = (__bridge id)((UIColor *)value).CGColor;
+          }
+      }
+
+      [mutableAttributes setObject:value forKey:key];
+    }];
+
+    return [NSDictionary dictionaryWithDictionary:mutableAttributes];
+}

+ 27 - 0
ThirdParty/TUIKit/TIMCommon/CommonModel/TUIEmojiMeditorProtocol.h

@@ -0,0 +1,27 @@
+//
+//  TUIEmojiMeditorProtocol.h
+//  TUIEmojiPlugin
+//
+//  Created by wyl on 2023/11/14.
+//  Copyright © 2023 Tencent. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+#import "TIMDefine.h"
+#import "TIMCommonModel.h"
+@class V2TIMMessage;
+@class TUIFaceGroup;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@protocol TUIEmojiMeditorProtocol <NSObject>
+- (void)updateEmojiGroups;
+- (id)getFaceGroup;
+- (void)appendFaceGroup:(TUIFaceGroup *)faceGroup;
+- (id)getChatPopDetailGroups;
+- (id)getChatContextEmojiDetailGroups;
+- (id)getChatPopMenuRecentQueue;
+- (void)updateRecentMenuQueue:(NSString *)faceName;
+@end
+
+NS_ASSUME_NONNULL_END

+ 29 - 0
ThirdParty/TUIKit/TIMCommon/CommonModel/TUIFitButton.h

@@ -0,0 +1,29 @@
+//
+//  TUIFitButton.h
+//  TUICore
+//
+//  Created by wyl on 2022/5/24.
+//  Copyright © 2023 Tencent. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface TUIFitButton : UIButton
+@property(nonatomic, assign) CGRect titleRect;
+@property(nonatomic, assign) CGRect imageRect;
+
+@property(nonatomic, assign) CGSize imageSize;
+@property(nonatomic, assign) CGSize titleSize;
+
+@property(nonatomic, strong) UIImage* hoverImage;
+@property(nonatomic, strong) UIImage* normalImage;
+
+@end
+
+@interface TUIBlockButton : TUIFitButton
+@property(nonatomic, copy) void (^clickCallBack)(id button);
+@end
+
+NS_ASSUME_NONNULL_END

+ 66 - 0
ThirdParty/TUIKit/TIMCommon/CommonModel/TUIFitButton.m

@@ -0,0 +1,66 @@
+//
+//  TUIFitButton.m
+//  TUICore
+//
+//  Created by wyl on 2022/5/24.
+//  Copyright © 2023 Tencent. All rights reserved.
+//
+
+#import "TUIFitButton.h"
+
+@implementation TUIFitButton
+
+- (CGRect)titleRectForContentRect:(CGRect)contentRect {
+    if (!CGRectEqualToRect(self.titleRect, CGRectZero)) {
+        return self.titleRect;
+    } else if (!CGSizeEqualToSize(self.titleSize, CGSizeZero)) {
+        CGRect oldrect = [super titleRectForContentRect:contentRect];
+        CGRect newrect = CGRectZero;
+        newrect.origin.x = oldrect.origin.x + (oldrect.size.width - self.titleSize.width) / 2;
+        newrect.origin.y = oldrect.origin.y + (oldrect.size.height - self.titleSize.height) / 2;
+        newrect.size.width = self.titleSize.width;
+        newrect.size.height = self.titleSize.height;
+
+        return newrect;
+    }
+    return [super titleRectForContentRect:contentRect];
+}
+- (CGRect)imageRectForContentRect:(CGRect)contentRect {
+    if (!CGRectEqualToRect(self.imageRect, CGRectZero)) {
+        return self.imageRect;
+    } else if (!CGSizeEqualToSize(self.imageSize, CGSizeZero)) {
+        CGRect oldrect = [super imageRectForContentRect:contentRect];
+        CGRect newrect = CGRectZero;
+        newrect.origin.x = oldrect.origin.x + (oldrect.size.width - self.imageSize.width) / 2;
+        newrect.origin.y = oldrect.origin.y + (oldrect.size.height - self.imageSize.height) / 2;
+        newrect.size.width = self.imageSize.width;
+        newrect.size.height = self.imageSize.height;
+
+        return newrect;
+    }
+    return [super imageRectForContentRect:contentRect];
+}
+
+- (void)setNormalImage:(UIImage *)normalImage {
+    _normalImage = normalImage;
+    [self setImage:normalImage forState:UIControlStateNormal];
+}
+@end
+
+@implementation TUIBlockButton
+
+- (instancetype)initWithFrame:(CGRect)frame {
+    if (self = [super initWithFrame:frame]) {
+        [self addTarget:self action:@selector(buttonTap:) forControlEvents:UIControlEventTouchUpInside];
+    }
+    return self;
+}
+
+- (void)buttonTap:(id)button {
+    if (self.clickCallBack) {
+        self.clickCallBack(self);
+    }
+}
+- (void)dealloc {
+}
+@end

+ 54 - 0
ThirdParty/TUIKit/TIMCommon/CommonModel/TUIFloatViewController.h

@@ -0,0 +1,54 @@
+//
+//  TUIFloatViewController.h
+//  TIMCommon
+//
+//  Created by wyl on 2023/1/16.
+//  Copyright © 2023 Tencent. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@protocol TUIFloatSubViewControllerProtocol <NSObject>
+
+@optional
+
+@property(nonatomic, copy) void (^floatDataSourceChanged)(NSArray *arr);
+
+- (void)floatControllerLeftButtonClick;
+
+- (void)floatControllerRightButtonClick;
+
+@end
+
+@interface TUIFloatTitleView : UIView
+
+@property(nonatomic, strong) UIButton *leftButton;
+@property(nonatomic, strong) UIButton *rightButton;
+@property(nonatomic, copy) void (^leftButtonClickCallback)(void);
+@property(nonatomic, copy) void (^rightButtonClickCallback)(void);
+@property(nonatomic, strong) UILabel *titleLabel;
+@property(nonatomic, strong) UILabel *subTitleLabel;
+
+- (void)setTitleText:(NSString *)mainText subTitleText:(NSString *)secondText leftBtnText:(NSString *)leftBtnText rightBtnText:(NSString *)rightBtnText;
+@end
+
+@interface TUIFloatViewController : UIViewController
+@property(nonatomic, strong) TUIFloatTitleView *topGestureView;
+@property(nonatomic, strong) UIImageView *topImgView;
+@property(nonatomic, strong) UIView *containerView;
+@property(nonatomic, strong) UIViewController<TUIFloatSubViewControllerProtocol> *childVC;
+
+- (void)updateSubContainerView;
+
+- (void)setnormalTop;
+
+- (void)setNormalBottom;
+
+- (void)appendChildViewController:(UIViewController<TUIFloatSubViewControllerProtocol> *)vc topMargin:(CGFloat)topMargin;
+
+- (void)floatDismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion;
+@end
+
+NS_ASSUME_NONNULL_END

+ 283 - 0
ThirdParty/TUIKit/TIMCommon/CommonModel/TUIFloatViewController.m

@@ -0,0 +1,283 @@
+//
+//  TUIFloatTitleView.m
+//  TUI
+//
+//  Created by wyl on 2023/1/16.
+//  Copyright © 2023 Tencent. All rights reserved.
+//
+
+#import "TUIFloatViewController.h"
+#import <TIMCommon/TIMDefine.h>
+
+typedef enum : NSUInteger {
+    FLEX_TOP,
+    FLEX_Bottom,
+} FLEX_Location;
+
+@interface TUIFloatTitleView ()
+
+@end
+
+@implementation TUIFloatTitleView
+
+- (instancetype)initWithFrame:(CGRect)frame {
+    self = [super initWithFrame:frame];
+    if (self) {
+        [self setupView];
+    }
+    return self;
+}
+
+- (void)setupView {
+    self.titleLabel = [[UILabel alloc] init];
+    self.titleLabel.text = @"";
+    self.titleLabel.font = [UIFont boldSystemFontOfSize:kScale390(20)];
+    [self addSubview:self.titleLabel];
+
+    self.subTitleLabel = [[UILabel alloc] init];
+    self.subTitleLabel.text = @"";
+    self.subTitleLabel.font = [UIFont systemFontOfSize:kScale390(12)];
+    self.subTitleLabel.tintColor = [UIColor grayColor];
+    [self addSubview:self.subTitleLabel];
+
+    self.leftButton = [UIButton buttonWithType:UIButtonTypeCustom];
+    [self addSubview:self.leftButton];
+    [self.leftButton setTitle:TIMCommonLocalizableString(TUIKitCreateCancel) forState:UIControlStateNormal];
+    self.leftButton.titleLabel.font = [UIFont systemFontOfSize:kScale390(16)];
+    [self.leftButton addTarget:self action:@selector(leftButtonClick) forControlEvents:UIControlEventTouchUpInside];
+    [self.leftButton setTitleColor:[UIColor tui_colorWithHex:@"#0365F9"] forState:UIControlStateNormal];
+
+    self.rightButton = [UIButton buttonWithType:UIButtonTypeCustom];
+    [self addSubview:self.rightButton];
+    [self.rightButton setTitle:TIMCommonLocalizableString(TUIKitCreateNext) forState:UIControlStateNormal];
+    self.rightButton.titleLabel.font = [UIFont systemFontOfSize:kScale390(16)];
+    [self.rightButton addTarget:self action:@selector(rightButtonClick) forControlEvents:UIControlEventTouchUpInside];
+    [self.rightButton setTitleColor:[UIColor tui_colorWithHex:@"#0365F9"] forState:UIControlStateNormal];
+}
+
+- (void)layoutSubviews {
+    [super layoutSubviews];
+
+    [self.titleLabel sizeToFit];
+    [self.subTitleLabel sizeToFit];
+
+    if (self.subTitleLabel.isHidden || self.subTitleLabel.text.length == 0) {
+        self.titleLabel.frame = CGRectMake((self.frame.size.width - self.titleLabel.frame.size.width) * 0.5, kScale390(23.5), self.titleLabel.frame.size.width,
+                                           self.titleLabel.frame.size.height);
+    } else {
+        self.titleLabel.frame = CGRectMake((self.frame.size.width - self.titleLabel.frame.size.width) * 0.5, kScale390(17.5), self.titleLabel.frame.size.width,
+                                           self.titleLabel.frame.size.height);
+
+        self.subTitleLabel.frame = CGRectMake((self.frame.size.width - self.subTitleLabel.frame.size.width) * 0.5,
+                                              self.titleLabel.frame.origin.y + self.titleLabel.frame.size.height + kScale390(1),
+                                              self.subTitleLabel.frame.size.width, self.subTitleLabel.frame.size.height);
+    }
+
+    [self.leftButton sizeToFit];
+    self.leftButton.frame = CGRectMake(kScale390(15), kScale390(23.5), self.leftButton.frame.size.width, self.leftButton.frame.size.height);
+
+    [self.rightButton sizeToFit];
+    self.rightButton.frame = CGRectMake(self.frame.size.width - self.rightButton.frame.size.width - kScale390(14), kScale390(23.5),
+                                        self.rightButton.frame.size.width, self.rightButton.frame.size.height);
+    if (isRTL()){
+        [self.leftButton resetFrameToFitRTL];
+        [self.rightButton resetFrameToFitRTL];
+    }
+}
+- (void)leftButtonClick {
+    if (self.leftButtonClickCallback) {
+        self.leftButtonClickCallback();
+    }
+}
+
+- (void)rightButtonClick {
+    if (self.rightButtonClickCallback) {
+        self.rightButtonClickCallback();
+    }
+}
+
+- (void)setTitleText:(NSString *)mainText subTitleText:(NSString *)secondText leftBtnText:(NSString *)leftBtnText rightBtnText:(NSString *)rightBtnText {
+    self.titleLabel.text = mainText;
+    self.subTitleLabel.text = secondText;
+    [self.leftButton setTitle:leftBtnText forState:UIControlStateNormal];
+    [self.rightButton setTitle:rightBtnText forState:UIControlStateNormal];
+}
+@end
+
+@interface TUIFloatViewController ()
+
+@property(nonatomic, assign) CGFloat topMargin;
+
+@property(nonatomic, assign) FLEX_Location currentLoaction;
+
+@property(nonatomic, strong) UIPanGestureRecognizer *panCover;
+
+@property(nonatomic, strong) UITapGestureRecognizer *singleTap;
+
+@end
+
+@implementation TUIFloatViewController
+
+- (void)appendChildViewController:(UIViewController<TUIFloatSubViewControllerProtocol> *)vc topMargin:(CGFloat)topMargin {
+    self.childVC = vc;
+    self.topMargin = topMargin;
+
+    [self addChildViewController:vc];
+    [self.containerView addSubview:vc.view];
+}
+
+- (void)viewDidLoad {
+    [super viewDidLoad];
+
+    self.view.backgroundColor = [UIColor tui_colorWithHex:@"#000000" alpha:0.6];
+    self.modalPresentationStyle = UIModalPresentationCustom;
+
+    self.containerView.backgroundColor = [UIColor whiteColor];
+
+    self.topImgView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:TIMCommonImagePath(@"icon_flex_arrow")]];
+    [self.topGestureView addSubview:self.topImgView];
+    self.topImgView.hidden = YES;
+
+    @weakify(self);
+    self.topGestureView.leftButtonClickCallback = ^{
+      @strongify(self);
+      if ([self.childVC respondsToSelector:@selector(floatControllerLeftButtonClick)]) {
+          [self.childVC performSelector:@selector(floatControllerLeftButtonClick)];
+      }
+    };
+    self.topGestureView.rightButtonClickCallback = ^{
+      @strongify(self);
+      if ([self.childVC respondsToSelector:@selector(floatControllerRightButtonClick)]) {
+          [self.childVC performSelector:@selector(floatControllerRightButtonClick)];
+      }
+    };
+    [self addSingleTapGesture];
+
+    if (!_currentLoaction) {
+        self.currentLoaction = FLEX_TOP;
+    }
+
+    [self updateSubContainerView];
+}
+
+- (void)addSingleTapGesture {
+    // When clicking on the shadow, the page disappears
+    self.view.userInteractionEnabled = YES;
+    UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(singleTap:)];
+    singleTap.cancelsTouchesInView = NO;
+    [self.view addGestureRecognizer:singleTap];
+}
+
+- (void)singleTap:(UITapGestureRecognizer *)tap {
+    CGPoint translation = [tap locationInView:self.containerView];
+
+    if (translation.x < 0 || translation.y < 0) {
+        [self dismissViewControllerAnimated:YES completion:nil];
+    } else if (translation.x > self.containerView.frame.size.width || translation.y > self.containerView.frame.size.height) {
+        [self dismissViewControllerAnimated:YES completion:nil];
+    }
+}
+
+- (void)setnormalTop {
+    self.currentLoaction = FLEX_TOP;
+}
+
+- (void)setNormalBottom {
+    self.currentLoaction = FLEX_Bottom;
+}
+
+- (void)setCurrentLoaction:(FLEX_Location)currentLoaction {
+    _currentLoaction = currentLoaction;
+    if (currentLoaction == FLEX_TOP) {
+        self.containerView.frame = CGRectMake(0, self.topMargin, self.view.frame.size.width, self.view.frame.size.height - self.topMargin);
+    } else if (currentLoaction == FLEX_Bottom) {
+        self.containerView.frame = CGRectMake(0, self.view.frame.size.height - kScale390(393), self.view.frame.size.width, kScale390(393));
+    }
+}
+
+- (void)floatDismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion {
+    [self dismissViewControllerAnimated:flag completion:completion];
+}
+#pragma mark - lazy
+
+- (UIView *)containerView {
+    if (_containerView == nil) {
+        _containerView = [[UIView alloc] init];
+        _containerView.layer.cornerRadius = kScale390(12);
+        [self.view addSubview:_containerView];
+    }
+    return _containerView;
+}
+
+- (UIView *)topGestureView {
+    if (_topGestureView == nil) {
+        _topGestureView = [[TUIFloatTitleView alloc] init];
+        //        [_topGestureView addGestureRecognizer:self.panCover];
+        [self.containerView addSubview:_topGestureView];
+    }
+    return _topGestureView;
+}
+
+- (UIPanGestureRecognizer *)panCover {
+    if (_panCover == nil) {
+        _panCover = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(onPanCover:)];
+    }
+    return _panCover;
+}
+
+- (void)onPanCover:(UIPanGestureRecognizer *)pan {
+    CGPoint translation = [pan translationInView:self.topGestureView];
+    CGFloat absX = fabs(translation.x);
+    CGFloat absY = fabs(translation.y);
+
+    if (MAX(absX, absY) < 2) return;
+    if (absX > absY) {
+        if (translation.x < 0) {
+            // scroll left
+        } else {
+            // scroll right
+        }
+    } else if (absY > absX) {
+        if (translation.y < 0) {
+            // scroll up
+            [self.topGestureView removeGestureRecognizer:self.panCover];
+            [UIView animateWithDuration:0.3
+                animations:^{
+                  self.currentLoaction = FLEX_TOP;
+                  [self.topGestureView addGestureRecognizer:self.panCover];
+                }
+                completion:^(BOOL finished) {
+                  if (finished) {
+                      [self updateSubContainerView];
+                  }
+                }];
+        } else {
+            // scroll down
+            if (self.currentLoaction == FLEX_Bottom) {
+                [self dismissViewControllerAnimated:YES completion:nil];
+            }
+            [self.topGestureView removeGestureRecognizer:self.panCover];
+            [UIView animateWithDuration:0.3
+                animations:^{
+                  self.currentLoaction = FLEX_Bottom;
+                  [self.topGestureView addGestureRecognizer:self.panCover];
+                }
+                completion:^(BOOL finished) {
+                  if (finished) {
+                      dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
+                        [self updateSubContainerView];
+                      });
+                  }
+                }];
+        }
+    }
+}
+
+- (void)updateSubContainerView {
+    self.topGestureView.frame = CGRectMake(0, 0, self.containerView.frame.size.width, kScale390(68.5));
+    self.topImgView.frame = CGRectMake((self.topGestureView.frame.size.width - kScale390(24)) * 0.5, kScale390(22), kScale390(24), kScale390(6));
+
+    self.childVC.view.frame = CGRectMake(0, self.topGestureView.frame.origin.y + self.topGestureView.frame.size.height, self.containerView.frame.size.width,
+                                         self.containerView.frame.size.height - self.topGestureView.frame.size.height);
+}
+@end

+ 21 - 0
ThirdParty/TUIKit/TIMCommon/CommonModel/TUIGroupAvatar+Helper.h

@@ -0,0 +1,21 @@
+//
+//  TUIGroupAvatar+Helper.h
+//  TIMCommon
+//
+//  Created by wyl on 2023/4/27.
+//  Copyright © 2023 Tencent. All rights reserved.
+//
+
+#import <TUICore/TUIDefine.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface TUIGroupAvatar (Helper)
+
++ (UIImage *)getNormalGroupCacheAvatar:(NSString *)groupID groupType:(NSString *)groupType;
+
++ (void)configAvatarByParam:(NSDictionary *)param targetView:(UIImageView *)targetView;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 125 - 0
ThirdParty/TUIKit/TIMCommon/CommonModel/TUIGroupAvatar+Helper.m

@@ -0,0 +1,125 @@
+//
+//  TUIGroupAvatar+Helper.m
+//  TIMCommon
+//
+//  Created by wyl on 2023/4/27.
+//  Copyright © 2023 Tencent. All rights reserved.
+//
+
+#import <TUICore/TUIConfig.h>
+#import "TUIGroupAvatar+Helper.h"
+
+@implementation TUIGroupAvatar (Helper)
+
++ (UIImage *)getNormalGroupCacheAvatar:(NSString *)groupID groupType:(NSString *)groupType {
+    /**
+     * 
+     * Setup default avatar
+     */
+    UIImage *avatarImage = nil;
+    if (groupID.length > 0) {
+        /**
+         * If it is a group, change the group default avatar to the last used avatar
+         */
+        UIImage *avatar = nil;
+        if (TUIConfig.defaultConfig.enableGroupGridAvatar) {
+            NSString *key = [NSString stringWithFormat:@"TUIConversationLastGroupMember_%@", groupID];
+            NSInteger member = [NSUserDefaults.standardUserDefaults integerForKey:key];
+            avatar = [TUIGroupAvatar getCacheAvatarForGroup:groupID number:(UInt32)member];
+        }
+        avatarImage = avatar ? avatar : DefaultGroupAvatarImageByGroupType(groupType);
+        ;
+        return avatarImage;
+    }
+    return avatarImage;
+}
++ (void)configAvatarByParam:(NSDictionary *)param targetView:(UIImageView *)targetView {
+    NSString *groupID = param[@"groupID"];
+    NSString *faceUrl = param[@"faceUrl"];
+    NSString *groupType = param[@"groupType"];
+    UIImage *originAvatarImage = param[@"originAvatarImage"];
+    if (groupID.length > 0) {
+        /**
+         * 
+         * Group avatar
+         */
+        if (IS_NOT_EMPTY_NSSTRING(faceUrl)) {
+            /**
+             * 
+             * The group avatar has been manually set externally
+             */
+            [targetView sd_setImageWithURL:[NSURL URLWithString:faceUrl] placeholderImage:originAvatarImage];
+        } else {
+            /**
+             * The group avatar has not been set externally. If the synthetic avatar is allowed, the synthetic avatar will be used; otherwise, the default
+             * avatar will be used.
+             */
+            if (TUIConfig.defaultConfig.enableGroupGridAvatar) {
+                /**
+                 *
+                 * If the synthetic avatar is allowed, the synthetic avatar will be used
+                 * 1. Asynchronously obtain the cached synthetic avatar according to the number of group members
+                 * 2. If the cache is hit, use the cached synthetic avatar directly
+                 * 3. If the cache is not hit, recompose a new avatar
+                 *
+                 * Note:
+                 * 1. Since "asynchronously obtaining cached avatars" and "synthesizing avatars" take a long time, it is easy to cause cell reuse problems, so
+                 * it is necessary to confirm whether to assign values directly according to groupID.
+                 * 2. Use SDWebImage to implement placeholder, because SDWebImage has already dealt with the problem of cell reuse
+                 */
+ 
+                // 1. Obtain group avatar from cache
+
+                // fix: The getCacheGroupAvatar needs to request the
+                // network. When the network is disconnected, since the headImageView is not set, the current conversation sends a message, the conversation is
+                // moved up, and the avatar of the first conversation is reused, resulting in confusion of the avatar.
+                [targetView sd_setImageWithURL:nil placeholderImage:originAvatarImage];
+                [TUIGroupAvatar getCacheGroupAvatar:groupID
+                                           callback:^(UIImage *avatar, NSString *groupID) {
+                                             if ([groupID isEqualToString:groupID]) {
+                                                 // 1.1 When the callback is invoked, the cell is not reused
+
+                                                 if (avatar != nil) {
+                                                     // 2. Hit the cache and assign directly
+                                                     [targetView sd_setImageWithURL:nil placeholderImage:avatar];
+                                                 } else {
+                                                     // 3. Synthesize new avatars asynchronously without hitting cache
+
+                                                     [targetView sd_setImageWithURL:nil placeholderImage:originAvatarImage];
+                                                     [TUIGroupAvatar
+                                                         fetchGroupAvatars:groupID
+                                                               placeholder:originAvatarImage
+                                                                  callback:^(BOOL success, UIImage *image, NSString *groupID) {
+                                                                    if ([groupID isEqualToString:groupID]) {
+                                                                        // When the callback is invoked, the cell is not reused
+                                                                        [targetView
+                                                                            sd_setImageWithURL:nil
+                                                                              placeholderImage:success ? image : DefaultGroupAvatarImageByGroupType(groupType)];
+                                                                    } else {
+                                                                        //  callback, When the callback is invoked, the cell has
+                                                                        // been reused to other groupIDs. Since a new callback will be triggered when the new
+                                                                        // groupID synthesizes new avatar, it is ignored here
+                                                                    }
+                                                                  }];
+                                                 }
+                                             } else {
+                                                 // 1.2 callback ,cell  groupID。 groupID 
+                                                 // callback, 1.2 When the callback is invoked, the cell has been reused to other groupIDs. Since a new
+                                                 // callback will be triggered when the new groupID gets the cache, it is ignored here
+                                             }
+                                           }];
+            } else {
+                /**
+                 * Synthetic avatars are not allowed, use the default avatar directly
+                 */
+                [targetView sd_setImageWithURL:nil placeholderImage:originAvatarImage];
+            }
+        }
+    } else {
+        /**
+         * Personal avatar
+         */
+        [targetView sd_setImageWithURL:[NSURL URLWithString:faceUrl] placeholderImage:originAvatarImage];
+    }
+}
+@end

+ 22 - 0
ThirdParty/TUIKit/TIMCommon/CommonModel/TUISecondConfirm.h

@@ -0,0 +1,22 @@
+//
+//  TUISecondConfirm.h
+//  TIMCommon
+//
+//  Created by xiangzhang on 2023/5/15.
+//  Copyright © 2023 Tencent. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+NS_ASSUME_NONNULL_BEGIN
+typedef void (^TUISecondConfirmBtnClickCallback)(void);
+@interface TUISecondConfirmBtnInfo : NSObject
+@property(nonatomic, strong) NSString *tile;
+@property(nonatomic, copy) TUISecondConfirmBtnClickCallback click;
+@end
+
+@interface TUISecondConfirm : NSObject
++ (void)show:(NSString *)title cancelBtnInfo:(TUISecondConfirmBtnInfo *)cancelBtnInfo confirmBtnInfo:(TUISecondConfirmBtnInfo *)confirmBtnInfo;
+@end
+
+NS_ASSUME_NONNULL_END

+ 105 - 0
ThirdParty/TUIKit/TIMCommon/CommonModel/TUISecondConfirm.m

@@ -0,0 +1,105 @@
+//
+//  TUISecondConfirm.m
+//  TIMCommon
+//
+//  Created by xiangzhang on 2023/5/15.
+//  Copyright © 2023 Tencent. All rights reserved.
+//
+
+#import "TUISecondConfirm.h"
+#import <TUICore/TUIDefine.h>
+#import <TUICore/UIView+TUILayout.h>
+
+@implementation TUISecondConfirmBtnInfo
+
+@end
+
+static UIWindow *gSecondWindow = nil;
+static TUISecondConfirmBtnInfo *gCancelBtnInfo = nil;
+static TUISecondConfirmBtnInfo *gConfirmBtnInfo = nil;
+
+@implementation TUISecondConfirm
+
++ (void)show:(NSString *)title cancelBtnInfo:(TUISecondConfirmBtnInfo *)cancelBtnInfo confirmBtnInfo:(TUISecondConfirmBtnInfo *)confirmBtnInfo {
+    gCancelBtnInfo = cancelBtnInfo;
+    gConfirmBtnInfo = confirmBtnInfo;
+
+    gSecondWindow = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
+    gSecondWindow.windowLevel = UIWindowLevelAlert - 1;
+    gSecondWindow.backgroundColor = [UIColor clearColor];
+    gSecondWindow.hidden = NO;
+
+    if (@available(iOS 13.0, *)) {
+        for (UIWindowScene *windowScene in [UIApplication sharedApplication].connectedScenes) {
+            if (windowScene.activationState == UISceneActivationStateForegroundActive) {
+                gSecondWindow.windowScene = windowScene;
+                break;
+            }
+        }
+    }
+
+    UIView *backgroupView = [[UIView alloc] initWithFrame:gSecondWindow.bounds];
+    backgroupView.backgroundColor = RGBA(0, 0, 0, 0.4);
+    [gSecondWindow addSubview:backgroupView];
+
+    UIView *confimView = [[UIView alloc] init];
+    confimView.backgroundColor = TIMCommonDynamicColor(@"second_confirm_bg_color", @"#FFFFFF");
+    confimView.layer.cornerRadius = 13;
+    confimView.layer.masksToBounds = YES;
+    [gSecondWindow addSubview:confimView];
+    confimView.mm_width(gSecondWindow.mm_w - kScale375(32) * 2).mm_height(183).mm__centerX(gSecondWindow.mm_centerX).mm__centerY(gSecondWindow.mm_h / 2);
+
+    UILabel *titleLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, confimView.mm_w, 123)];
+    titleLabel.text = title;
+    titleLabel.textColor = TIMCommonDynamicColor(@"second_confirm_title_color", @"#000000");
+    titleLabel.textAlignment = NSTextAlignmentCenter;
+    titleLabel.font = [UIFont systemFontOfSize:16];
+    titleLabel.numberOfLines = 0;
+    [confimView addSubview:titleLabel];
+
+    UIView *line1 = [[UIView alloc] initWithFrame:CGRectMake(0, titleLabel.mm_maxY, titleLabel.mm_w, 0.5)];
+    line1.backgroundColor = TIMCommonDynamicColor(@"second_confirm_line_color", @"#DDDDDD");
+    [confimView addSubview:line1];
+
+    UIButton *cancelBtn = [UIButton buttonWithType:UIButtonTypeCustom];
+    [cancelBtn setFrame:CGRectMake(0, line1.mm_maxY, line1.mm_w / 2, confimView.mm_h - line1.mm_maxY)];
+    [cancelBtn setTitle:gCancelBtnInfo.tile forState:UIControlStateNormal];
+    [cancelBtn.titleLabel setFont:[UIFont systemFontOfSize:16]];
+    [cancelBtn setTitleColor:TIMCommonDynamicColor(@"second_confirm_cancel_btn_title_color", @"#000000") forState:UIControlStateNormal];
+    [cancelBtn addTarget:self action:@selector(onCancelBtnClick) forControlEvents:UIControlEventTouchUpInside];
+    [confimView addSubview:cancelBtn];
+
+    UIView *line2 = [[UIView alloc] initWithFrame:CGRectMake(cancelBtn.mm_maxX, cancelBtn.mm_y, 0.5, cancelBtn.mm_h)];
+    line2.backgroundColor = TIMCommonDynamicColor(@"second_confirm_line_color", @"#DDDDDD");
+    [confimView addSubview:line2];
+
+    UIButton *confirmBtn = [UIButton buttonWithType:UIButtonTypeCustom];
+    [confirmBtn setFrame:CGRectMake(line2.mm_maxX, cancelBtn.mm_y, cancelBtn.mm_w, cancelBtn.mm_h)];
+    [confirmBtn setTitle:gConfirmBtnInfo.tile forState:UIControlStateNormal];
+    [confirmBtn.titleLabel setFont:[UIFont systemFontOfSize:16]];
+    [confirmBtn setTitleColor:TIMCommonDynamicColor(@"second_confirm_confirm_btn_title_color", @"#FF584C") forState:UIControlStateNormal];
+    [confirmBtn addTarget:self action:@selector(onConfirmBtnBtnClick) forControlEvents:UIControlEventTouchUpInside];
+    [confimView addSubview:confirmBtn];
+}
+
++ (void)onCancelBtnClick {
+    if (gCancelBtnInfo && gCancelBtnInfo.click) {
+        gCancelBtnInfo.click();
+    }
+    [self dismiss];
+}
+
++ (void)onConfirmBtnBtnClick {
+    if (gConfirmBtnInfo && gConfirmBtnInfo.click) {
+        gConfirmBtnInfo.click();
+    }
+    [self dismiss];
+}
+
++ (void)dismiss {
+    gSecondWindow = nil;
+    gCancelBtnInfo = nil;
+    gConfirmBtnInfo = nil;
+}
+
+@end

+ 25 - 0
ThirdParty/TUIKit/TIMCommon/CommonModel/TUITextView.h

@@ -0,0 +1,25 @@
+//
+//  TUITextView.h
+//  Masonry
+//
+//  Created by xiangzhang on 2022/10/14.
+//  Copyright © 2023 Tencent. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@protocol TUITextViewDelegate <NSObject>
+- (void)onLongPressTextViewMessage:(UITextView *)textView;
+@end
+
+@interface TUITextView : UITextView
+@property (nonatomic, strong) UILongPressGestureRecognizer *longPressGesture;
+@property (nonatomic, weak) id<TUITextViewDelegate> tuiTextViewDelegate;
+
+- (void)disableHighlightLink;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 65 - 0
ThirdParty/TUIKit/TIMCommon/CommonModel/TUITextView.m

@@ -0,0 +1,65 @@
+//
+//  TUITextView.m
+//  Masonry
+//
+//  Created by xiangzhang on 2022/10/14.
+//  Copyright © 2023 Tencent. All rights reserved.
+//
+
+#import "TUITextView.h"
+#import <TUICore/TUIThemeManager.h>
+
+@implementation TUITextView
+
+- (instancetype)init {
+    self = [super init];
+    if (self) {
+        self.dataDetectorTypes = UIDataDetectorTypeLink | UIDataDetectorTypePhoneNumber;
+        [self setupLongPressGesture];
+        self.tintColor = TIMCommonDynamicColor(@"chat_highlight_link_color", @"#6495ED");
+    }
+    return self;
+}
+
+- (BOOL)canBecomeFirstResponder {
+    return YES;
+}
+
+- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
+    return NO;
+}
+
+- (void)buildMenuWithBuilder:(id<UIMenuBuilder>)builder API_AVAILABLE(ios(13.0)) {
+    if (@available(iOS 16.0, *)) {
+        [builder removeMenuForIdentifier:UIMenuLookup];
+    }
+    [super buildMenuWithBuilder:builder];
+}
+
+- (void)disableHighlightLink {
+    self.dataDetectorTypes = UIDataDetectorTypeNone;
+}
+
+- (void)setupLongPressGesture {
+    self.longPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
+    [self addGestureRecognizer:self.longPressGesture];
+}
+
+- (void)handleLongPress:(UILongPressGestureRecognizer *)gesture {
+    if ([gesture isKindOfClass:[UILongPressGestureRecognizer class]] && gesture.state == UIGestureRecognizerStateBegan) {
+        if (self.tuiTextViewDelegate && [self.tuiTextViewDelegate respondsToSelector:@selector(onLongPressTextViewMessage:)]) {
+            [self.tuiTextViewDelegate onLongPressTextViewMessage:self];
+        }
+    }
+}
+
+- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
+shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
+    if ([gestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]] &&
+        gestureRecognizer != self.longPressGesture) {
+        return NO;
+    }
+    return YES;
+}
+
+@end

+ 25 - 0
ThirdParty/TUIKit/TIMCommon/CommonModel/TUIUserAuthorizationCenter.h

@@ -0,0 +1,25 @@
+//
+//  TUIUserAuthorizationCenter.h
+//  TUIChat
+//
+//  Created by wyl on 2022/2/16.
+//  Copyright © 2023 Tencent. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+
+typedef NS_OPTIONS(NSUInteger, TUIChatAuthControlType) {
+    TUIChatAuthControlTypeMicro = 1 << 0,
+    TUIChatAuthControlTypeCamera = 1 << 1,
+    TUIChatAuthControlTypePhoto = 1 << 2,
+};
+@interface TUIUserAuthorizationCenter : NSObject
+@property(nonatomic, assign, class, readonly) BOOL isEnableCameraAuthorization;
+@property(nonatomic, assign, class, readonly) BOOL isEnableMicroAuthorization;
+
++ (void)cameraStateActionWithPopCompletion:(void (^)(void))completion API_AVAILABLE(ios(8.0));
++ (void)microStateActionWithPopCompletion:(void (^)(void))completion API_AVAILABLE(ios(8.0));
+
++ (void)openSettingPage;
++ (void)showAlert:(TUIChatAuthControlType)type;
+@end

+ 136 - 0
ThirdParty/TUIKit/TIMCommon/CommonModel/TUIUserAuthorizationCenter.m

@@ -0,0 +1,136 @@
+//
+//  TUIUserAuthorizationCenter.m
+//  TUIChat
+//
+//  Created by wyl on 2022/2/16.
+//  Copyright © 2023 Tencent. All rights reserved.
+//
+
+#import "TUIUserAuthorizationCenter.h"
+#import <AVFoundation/AVCaptureDevice.h>
+#import <CoreLocation/CLLocationManager.h>
+#import <CoreMotion/CoreMotion.h>
+#import <EventKit/EventKit.h>
+#import <Photos/Photos.h>
+#import <Speech/Speech.h>
+#import <TIMCommon/TIMDefine.h>
+#import <UserNotifications/UserNotifications.h>
+
+#import <TUICore/TUIGlobalization.h>
+@implementation TUIUserAuthorizationCenter
+
++ (BOOL)isEnableCameraAuthorization {
+    if (@available(iOS 7.0, *)) {
+        return [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo] == AVAuthorizationStatusAuthorized;
+    } else {
+        return YES;
+    }
+}
+
++ (void)cameraStateActionWithPopCompletion:(void (^)(void))completion {
+    if ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo] == AVAuthorizationStatusNotDetermined) {
+        [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo
+                                 completionHandler:^(BOOL granted) {
+                                   if (granted && completion) {
+                                       completion();
+                                   }
+                                 }];
+    } else {
+        [self showAlert:TUIChatAuthControlTypeCamera];
+    }
+}
+
++ (void)openSettingPage {
+    if (@available(iOS 8.0, *)) {
+        NSURL *url = [NSURL URLWithString:UIApplicationOpenSettingsURLString];
+        if ([[UIApplication sharedApplication] canOpenURL:url]) {
+            if (@available(iOS 10.0, *)) {
+                [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
+            } else {
+                [[UIApplication sharedApplication] openURL:url];
+            }
+        }
+    } else {
+        // Fallback on earlier versions
+    }
+}
+
++ (BOOL)isEnableMicroAuthorization {
+    if (@available(iOS 7.0, *)) {
+        return [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio] == AVAuthorizationStatusAuthorized;
+    } else {
+        return YES;
+    }
+}
+
++ (void)microStateActionWithPopCompletion:(void (^)(void))completion {
+#if !TARGET_OS_MACCATALYST
+    if ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio] == AVAuthorizationStatusNotDetermined) {
+        [AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio
+                                 completionHandler:^(BOOL granted) {
+                                   if (granted && completion) {
+                                       completion();
+                                   }
+                                 }];
+    } else {
+        [self showAlert:TUIChatAuthControlTypeMicro];
+    }
+#endif
+}
+
++ (BOOL)isEnableLocationAuthorization {
+    CLAuthorizationStatus status = [CLLocationManager authorizationStatus];
+    if (@available(iOS 8.0, *)) {
+        return status == kCLAuthorizationStatusAuthorizedAlways || status == kCLAuthorizationStatusAuthorizedWhenInUse;
+    } else {
+        // Fallback on earlier versions
+        return YES;
+    }
+}
+
++ (void)showAlert:(TUIChatAuthControlType)type {
+    NSString *title = @"";
+    NSString *message = @"";
+    NSString *laterMessage = @"";
+    NSString *openSettingMessage = @"";
+
+    if (TUIChatAuthControlTypeMicro == type) {
+        title = TIMCommonLocalizableString(TUIKitInputNoMicTitle);
+        message = TIMCommonLocalizableString(TUIKitInputNoMicTips);
+        laterMessage = TIMCommonLocalizableString(TUIKitInputNoMicOperateLater);
+        openSettingMessage = TIMCommonLocalizableString(TUIKitInputNoMicOperateEnable);
+    } else if (TUIChatAuthControlTypeCamera == type) {
+        title = TIMCommonLocalizableString(TUIKitInputNoCameraTitle);
+        message = TIMCommonLocalizableString(TUIKitInputNoCameraTips);
+        laterMessage = TIMCommonLocalizableString(TUIKitInputNoCameraOperateLater);
+        openSettingMessage = TIMCommonLocalizableString(TUIKitInputNoCameraOperateEnable);
+    } else if (TUIChatAuthControlTypePhoto == type) {
+        title = TIMCommonLocalizableString(TUIKitInputNoPhotoTitle);
+        message = TIMCommonLocalizableString(TUIKitInputNoPhotoTips);
+        laterMessage = TIMCommonLocalizableString(TUIKitInputNoPhotoOperateLater);
+        openSettingMessage = TIMCommonLocalizableString(TUIKitInputNoPhotoerateEnable);
+    } else {
+        return;
+    }
+    if (@available(iOS 8.0, *)) {
+        UIAlertController *ac = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert];
+        [ac tuitheme_addAction:[UIAlertAction actionWithTitle:laterMessage style:UIAlertActionStyleCancel handler:nil]];
+        [ac tuitheme_addAction:[UIAlertAction actionWithTitle:openSettingMessage
+                                                        style:UIAlertActionStyleDefault
+                                                      handler:^(UIAlertAction *_Nonnull action) {
+                                                        UIApplication *app = [UIApplication sharedApplication];
+                                                        NSURL *settingsURL = [NSURL URLWithString:UIApplicationOpenSettingsURLString];
+                                                        if ([app canOpenURL:settingsURL]) {
+                                                            [app openURL:settingsURL];
+                                                        }
+                                                      }]];
+        dispatch_async(dispatch_get_main_queue(), ^{
+          [UIApplication.sharedApplication.keyWindow.rootViewController presentViewController:ac animated:YES completion:nil];
+          //        [self presentViewController:ac animated:YES completion:nil];
+        });
+    } else {
+        // Fallback on earlier versions
+    }
+}
+
+@end

+ 36 - 0
ThirdParty/TUIKit/TIMCommon/Resources/PrivacyInfo.xcprivacy

@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>NSPrivacyTracking</key>
+	<false/>
+	<key>NSPrivacyTrackingDomains</key>
+	<array/>
+	<key>NSPrivacyCollectedDataTypes</key>
+	<array>
+		<dict>
+			<key>NSPrivacyCollectedDataType</key>
+			<string>NSPrivacyCollectedDataTypeUserID</string>
+			<key>NSPrivacyCollectedDataTypeLinked</key>
+			<false/>
+			<key>NSPrivacyCollectedDataTypeTracking</key>
+			<false/>
+			<key>NSPrivacyCollectedDataTypePurposes</key>
+			<array>
+				<string>NSPrivacyCollectedDataTypePurposeProductPersonalization</string>
+			</array>
+		</dict>
+	</array>
+	<key>NSPrivacyAccessedAPITypes</key>
+	<array>
+		<dict>
+			<key>NSPrivacyAccessedAPIType</key>
+			<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
+			<key>NSPrivacyAccessedAPITypeReasons</key>
+			<array>
+				<string>CA92.1</string>
+			</array>
+		</dict>
+	</array>
+</dict>
+</plist>

BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/chat_nav_more_menu@3x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/default_c2c_head@2x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/default_choose@3x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/default_fold_group@2x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/default_group_head@2x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/default_group_head_avchatroom@2x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/default_group_head_community@2x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/default_group_head_meeting@2x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/default_group_head_public@2x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/icon_avatar_selected@2x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/icon_cell_blue_normal@2x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/icon_cell_blue_normal@3x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/icon_flex_arrow@3x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/icon_offline_status@2x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/icon_offline_status@3x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/icon_online_status@2x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/icon_online_status@3x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/icon_secure_cancel_img@2x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/icon_secure_info_img@2x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/icon_security_strike@2x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/icon_select_normal@2x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/icon_select_normal@3x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/icon_select_pressed@2x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/icon_select_pressed@3x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/icon_select_selected@2x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/icon_select_selected@3x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/icon_select_selected_disable@2x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/icon_select_selected_disable@3x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/icon_translate@2x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/icon_translate@3x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/messageReplyIcon@2x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/message_translation_loading@2x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/message_translation_loading@3x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/message_translation_tips@2x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/message_translation_tips@3x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/more@2x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/more_customer_service_evaluation@2x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/more_customer_service_evaluation@3x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/more_group_note@2x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/more_group_note@3x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/more_poll@2x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/more_poll@3x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/more_video_call@2x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/more_video_call@3x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/more_voice_call@2x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/more_voice_call@3x.png


BIN
ThirdParty/TUIKit/TIMCommon/Resources/TIMCommon.bundle/nav_back@3x.png


Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä