Răsfoiți Sursa

feat: 官方消息

DoggyZhang 3 luni în urmă
părinte
comite
87bb00a5da
23 a modificat fișierele cu 1059 adăugiri și 63 ștergeri
  1. 8 0
      app/src/main/java/com/adealink/weparty/commonui/toast/util/ToastUtil.kt
  2. 12 0
      app/src/main/java/com/adealink/weparty/module/im/Router.kt
  3. 48 49
      app/src/main/java/com/adealink/weparty/url/UrlConfig.kt
  4. 20 1
      frame/tuikit/TUIConversation/tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/presenter/ConversationPresenter.java
  5. 5 0
      module/im/src/main/AndroidManifest.xml
  6. 9 0
      module/im/src/main/java/com/adealink/weparty/im/IMServiceImpl.kt
  7. 14 0
      module/im/src/main/java/com/adealink/weparty/im/constant/Data.kt
  8. 24 6
      module/im/src/main/java/com/adealink/weparty/im/list/SessionListFragment.kt
  9. 13 5
      module/im/src/main/java/com/adealink/weparty/im/list/adapter/SessionListAdapter.kt
  10. 2 1
      module/im/src/main/java/com/adealink/weparty/im/manager/IMLoginManager.kt
  11. 120 0
      module/im/src/main/java/com/adealink/weparty/im/session/OfficialSessionActivity.kt
  12. 313 0
      module/im/src/main/java/com/adealink/weparty/im/session/OfficialSessionFragment.kt
  13. 9 1
      module/im/src/main/java/com/adealink/weparty/im/session/adapter/SessionAdapter.kt
  14. 76 0
      module/im/src/main/java/com/adealink/weparty/im/session/adapter/viewbinder/BaseOfficialMessageViewBinder.kt
  15. 94 0
      module/im/src/main/java/com/adealink/weparty/im/session/adapter/viewbinder/OfficialImageTextMessageViewBinder.kt
  16. 19 0
      module/im/src/main/java/com/adealink/weparty/im/session/mesasge/CustomMessageViewHolder.kt
  17. 41 0
      module/im/src/main/java/com/adealink/weparty/im/session/mesasge/OfficialImageTextMessageBean.kt
  18. 25 0
      module/im/src/main/res/layout/activity_official_session.xml
  19. 17 0
      module/im/src/main/res/layout/fragment_official_session.xml
  20. 40 0
      module/im/src/main/res/layout/layout_session_message_official_base.xml
  21. 109 0
      module/im/src/main/res/layout/layout_session_message_official_image_text.xml
  22. 39 0
      module/im/src/main/res/layout/layout_session_official_top_bar.xml
  23. 2 0
      module/im/src/main/res/values/strings.xml

+ 8 - 0
app/src/main/java/com/adealink/weparty/commonui/toast/util/ToastUtil.kt

@@ -1,11 +1,13 @@
 package com.adealink.weparty.commonui.toast.util
 
+import android.widget.Toast
 import androidx.annotation.StringRes
 import com.adealink.frame.aab.util.getCompatString
 import com.adealink.frame.base.IError
 import com.adealink.frame.base.Rlt
 import com.adealink.frame.util.AppUtil
 import com.adealink.frame.util.runOnUiThread
+import com.adealink.weparty.BuildConfig
 import com.adealink.weparty.R
 import com.adealink.weparty.commonui.toast.ToastCompat
 
@@ -107,4 +109,10 @@ fun getFailedMsg(error: IError, default: String? = getCompatString(R.string.comm
     }
 
     return default
+}
+
+fun debugToast(msg: String) {
+    if (BuildConfig.DEBUG) {
+        showToast("DEBUG: $msg", Toast.LENGTH_LONG)
+    }
 }

+ 12 - 0
app/src/main/java/com/adealink/weparty/module/im/Router.kt

@@ -33,4 +33,16 @@ interface IM {
             const val EXTRA_CHAT_DRAFT_TIME = "extra_chat_draft_time"
         }
     }
+
+    interface OfficialSession {
+        companion object {
+            const val PATH = "${Common.PATH}/official_session"
+
+            const val EXTRA_CHAT_TYPE = "extra_chat_type"
+            const val EXTRA_CHAT_ID = "extra_chat_id"
+            const val EXTRA_CHAT_NAME = "extra_chat_name"
+            const val EXTRA_CHAT_DRAFT_TEXT = "extra_chat_draft_text"
+            const val EXTRA_CHAT_DRAFT_TIME = "extra_chat_draft_time"
+        }
+    }
 }

+ 48 - 49
app/src/main/java/com/adealink/weparty/url/UrlConfig.kt

@@ -7,206 +7,205 @@ import com.adealink.weparty.network.RELEASE_HOSTS
 
 /**
  * UrlConfig配置url
- * 获取url链接,通过urlConfigService.getH5Url方法
  */
 object UrlConfig {
 
     private val isProdEnv =
         AppBase.isRelease || RELEASE_HOSTS.contains(Uri.parse(DebugPrefs.httpUrl).host)
 
-    internal val thirdPayUrl = when {
+     val thirdPayUrl = when {
         isProdEnv -> "https://web.wenext.chat/web/yoki-wallet"
         else -> "http://web-test.wenext.chat/web/yoki-wallet"
     }
 
-    internal val faqGameCoinsUrl = when {
+     val faqGameCoinsUrl = when {
         isProdEnv -> "https://web.yoki.chat/coin-faq?projectname=yoki-room-game-help"
         else -> "https://web-test.yoki.chat/coin-faq?projectname=yoki-room-game-help"
     }
-    internal val luckyFruit = when {
+     val luckyFruit = when {
         isProdEnv -> "https://web.yoki.chat/index/?projectname=yoki-lucky-fruit&aspect_ratio=1.604"
         else -> "http://web-test.yoki.chat/index/?projectname=yoki-lucky-fruit&aspect_ratio=1.604"
     }
 
-    internal val serviceTerms = when {
+     val serviceTerms = when {
         isProdEnv -> "https://web.yoki.chat/service_terms/0?projectname=yoki-terms"
         else -> "http://web-test.yoki.chat/service_terms/0?projectname=yoki-terms"
     }
 
-    internal val privacyPolicy = when {
+     val privacyPolicy = when {
         isProdEnv -> "https://web.yoki.chat/privacy_policy/0?projectname=yoki-terms"
         else -> "http://web-test.yoki.chat/privacy_policy/0?projectname=yoki-terms"
     }
 
-    internal val policies = when {
+     val policies = when {
         isProdEnv -> "https://web.yoki.chat/policies/0?projectname=yoki-terms"
         else -> "http://web-test.yoki.chat/policies/0?projectname=yoki-terms"
     }
 
-    internal val vip = when {
+     val vip = when {
         isProdEnv -> "https://web.yoki.chat/web/yoki-vip?hideAppBar=true"
         else -> "http://web-test.yoki.chat/web/yoki-vip?hideAppBar=true"
     }
 
-    internal val sellCoins = when {
+     val sellCoins = when {
         isProdEnv -> "https://web.yoki.chat/coin-agent-center?projectname=yoki-payment&hideAppBar=true&vpResizable=true"
         else -> "http://web-test.yoki.chat/coin-agent-center?projectname=yoki-payment&hideAppBar=true&vpResizable=true"
     }
 
-    internal val superGift = when {
+     val superGift = when {
         isProdEnv -> "https://web.wenext.chat/web/h5-super-gift"
         else -> "http://web-test.wenext.chat/web/h5-super-gift"
     }
 
-    internal val slot = when {
+     val slot = when {
         isProdEnv -> "https://web.yoki.chat/index?projectname=yoki-jackpot"
         else -> "http://web-test.yoki.chat/index?projectname=yoki-jackpot"
     }
 
-    internal val coupleRules = when {
+     val coupleRules = when {
         isProdEnv -> "https://web.wenext.chat/web/yoki-couple-privilege"
         else -> "http://web-test.wenext.chat/web/yoki-couple-privilege"
     }
 
-    internal val coupleProtectRules = when {
+     val coupleProtectRules = when {
         isProdEnv -> "https://web.wenext.chat/web/yoki-couple-rule/protect"
         else -> "http://web-test.wenext.chat/web/yoki-couple-rule/protect"
     }
 
-    internal val familyRule = when {
+     val familyRule = when {
         isProdEnv -> "https://web.wenext.chat/web/h5-family-rule"
         else -> "http://web-test.wenext.chat/web/h5-family-rule"
     }
 
-    internal val luckyGiftRule = when {
+     val luckyGiftRule = when {
         isProdEnv -> "https://web.yoki.chat/lucky-gift-rule?projectname=yoki-common-page&hideAppBar=true"
         else -> "http://web-test.yoki.chat/lucky-gift-rule?projectname=yoki-common-page&hideAppBar=true"
     }
 
-    internal val medalRule = when {
+     val medalRule = when {
         isProdEnv -> "https://web.wenext.chat/web/h5-medal-rule"
         else -> "http://web-test.wenext.chat/web/h5-medal-rule"
     }
 
-    internal val treasureGiftRule = when {
+     val treasureGiftRule = when {
         isProdEnv -> "https://web.wenext.chat/web/h5-treasure-gift-rule"
         else -> "http://web-test.wenext.chat/web/h5-treasure-gift-rule"
     }
 
-    internal val micGrabRule = when {
+     val micGrabRule = when {
         isProdEnv -> "https://web.wenext.chat/web/h5-mic-grab-rule"
         else -> "http://web-test.wenext.chat/web/h5-mic-grab-rule"
     }
 
-    internal val singerTerms = when {
+     val singerTerms = when {
         isProdEnv -> "https://web.wenext.chat/web/h5-mic-grab-rule/singer_terms"
         else -> "http://web-test.wenext.chat/web/h5-mic-grab-rule/singer_terms"
     }
 
-    internal val teenPatti = when {
+     val teenPatti = when {
         isProdEnv -> "https://web.wenext.chat/web/game-teen-patti"
         else -> "http://web-test.wenext.chat/web/game-teen-patti"
     }
 
-    internal val greedyPro = when {
+     val greedyPro = when {
         isProdEnv -> "https://web.yoki.chat/index/?projectname=yoki-greedy-pro&aspect_ratio=1.571"
         else -> "http://web-test.yoki.chat/index/?projectname=yoki-greedy-pro&aspect_ratio=1.571"
     }
-    internal val jackpot = when {
+     val jackpot = when {
         isProdEnv -> "https://web.yoki.chat/index/?projectname=yoki-jackpot&aspect_ratio=1.733"
         else -> "http://web-test.yoki.chat/index/?projectname=yoki-jackpot&aspect_ratio=1.733"
     }
-    internal val adminActivity = when {
+     val adminActivity = when {
         isProdEnv -> "https://web.wenext.chat/web/h5-activity"
         else -> "http://web-test.wenext.chat/web/h5-activity"
     }
 
-    internal val svip = when {
+     val svip = when {
         isProdEnv -> "https://web.yoki.chat/web/yoki-vip/index/svip?hideAppBar=true"
         else -> "http://web-test.yoki.chat/web/yoki-vip/index/svip?hideAppBar=true"
     }
 
-    internal val familyTaskList = when {
+     val familyTaskList = when {
         isProdEnv -> "https://web.wenext.chat/web/yoki-family-task"
         else -> "http://web-test.wenext.chat/web/yoki-family-task"
     }
 
-    internal val familyMemberRankList = when {
+     val familyMemberRankList = when {
         isProdEnv -> "https://web.wenext.chat/web/yoki-family-member"
         else -> "http://web-test.wenext.chat/web/yoki-family-member"
     }
 
-    internal val familyLevel = when {
+     val familyLevel = when {
         isProdEnv -> "https://web.wenext.chat/web/yoki-family-level"
         else -> "http://web-test.wenext.chat/web/yoki-family-level"
     }
 
-    internal val weddingPrivilege = when {
+     val weddingPrivilege = when {
         isProdEnv -> "https://web.wenext.chat/web/h5-wedding-popularity?roomId="
         else -> "http://web-test.wenext.chat/web/h5-wedding-popularity?roomId="
     }
 
-    internal val luckyPro = when {
+     val luckyPro = when {
         isProdEnv -> "https://web.yoki.chat/index/?projectname=yoki-lucky-pro&aspect_ratio=1.604"
         else -> "http://web-test.yoki.chat/index/?projectname=yoki-lucky-pro&aspect_ratio=1.604"
     }
-    internal val unoRule = when (AppBase.isProdEnv) {
+     val unoRule = when (AppBase.isProdEnv) {
         true -> "https://web.wenext.chat/web/h5-uno-rule"
         else -> "http://web-test.wenext.chat/web/h5-uno-rule"
     }
 
-    internal val carromRule = when (AppBase.isProdEnv) {
+     val carromRule = when (AppBase.isProdEnv) {
         true -> "https://web.wenext.chat/web/h5-carrom-rule"
         else -> "http://web-test.wenext.chat/web/h5-carrom-rule"
     }
 
-    internal val dominoRule = when (AppBase.isProdEnv) {
+     val dominoRule = when (AppBase.isProdEnv) {
         true -> "https://web.wenext.chat/web/h5-domino-rule"
         else -> "http://web-test.wenext.chat/web/h5-domino-rule"
     }
 
-    internal val ludoRules = when {
+     val ludoRules = when {
         isProdEnv -> "https://web.wenext.chat/web/lama-ludo-rule"
         else -> "http://web-test.wenext.chat/web/lama-ludo-rule"
     }
 
-    internal val thirdPartyRecharge =
+     val thirdPartyRecharge =
         when {
             isProdEnv -> "https://web.yoki.chat/third-party-recharge?projectname=yoki-payment&hideAppBar=true&vpResizable=true"
             else -> "http://web-test.yoki.chat/third-party-recharge?projectname=yoki-payment&hideAppBar=true&vpResizable=true"
         }
 
-    internal val diamondWithdrawal =
+     val diamondWithdrawal =
         when {
             isProdEnv -> "https://web.yoki.chat/diamond-deal?projectname=yoki-payment&hideAppBar=true&vpResizable=true"
             else -> "http://web-test.yoki.chat/diamond-deal?projectname=yoki-payment&hideAppBar=true&vpResizable=true"
         }
 
-    internal val prefetchResource =
+     val prefetchResource =
         when {
             isProdEnv -> " https://web.yoki.chat/index?projectname=h5-preload"
             else -> "http://web-test.yoki.chat/index?projectname=h5-preload"
         }
 
-    internal val taskCenter =
+     val taskCenter =
         when {
             AppBase.isProdEnv -> "https://web.yoki.chat/index/?projectname=yoki-task-center&hideAppBar=true"
             else -> "https://web-test.yoki.chat/index/?projectname=yoki-task-center&hideAppBar=true"
         }
 
-    internal val chatAchievement =
+     val chatAchievement =
         when {
             AppBase.isProdEnv -> "https://web-new.yoki.chat/index?projectname=yoki-chat-achievement&hideAppBar=true"
             else -> "https://web-test-new.yoki.chat/index?projectname=yoki-chat-achievement&hideAppBar=true"
         }
 
-    internal val signatureAgreement =
+     val signatureAgreement =
         when {
             AppBase.isProdEnv -> "https://web.yoki.chat/signature_agreement/0?projectname=yoki-terms"
             else -> "https://web-test.yoki.chat/signature_agreement/0?projectname=yoki-terms"
         }
 
-    internal val childSafetyPolicy =
+     val childSafetyPolicy =
         when {
             AppBase.isProdEnv -> "https://web.yoki.chat/child_policy/0?projectname=yoki-terms"
             else -> "https://web-test.yoki.chat/child_policy/0?projectname=yoki-terms"
@@ -223,20 +222,20 @@ object UrlConfig {
      * 打开奖励记录弹窗:
      * showRecords=1
      */
-    internal val inviteActivity =
+     val inviteActivity =
         when {
             AppBase.isProdEnv -> "https://web.yoki.chat/index?projectname=yoki-invite&hideAppBar=true"
             else -> "https://web-test.yoki.chat/index?projectname=yoki-invite&hideAppBar=true"
         }
 
     // 外部引导下载的网页 -> 用于分享
-    internal val webGuideDownload =
+     val webGuideDownload =
         when {
             AppBase.isProdEnv -> "https://web.yoki.chat/invite?projectname=yoki-invite"
             else -> "https://web-test.yoki.chat/invite?projectname=yoki-invite"
         }
 
-    internal val anchorCenter =
+     val anchorCenter =
         when {
             AppBase.isProdEnv -> "https://web.yoki.chat/index?projectname=yoki-union-center&hideAppBar=true&vpResizable=true"
             else -> "http://web-test.yoki.chat/index?projectname=yoki-union-center&hideAppBar=true&vpResizable=true"
@@ -245,7 +244,7 @@ object UrlConfig {
     /**
      * Family Daily Task
      */
-    internal val familyDailyTask =
+     val familyDailyTask =
         when {
             AppBase.isProdEnv -> "https://web.yoki.chat/daily-task?projectname=yoki-family&hideAppBar=true"
             else -> "http://web-test.yoki.chat/daily-task?projectname=yoki-family&hideAppBar=true"
@@ -254,7 +253,7 @@ object UrlConfig {
     /**
      * Family Daily Task Dialog
      */
-    internal val familyDailyTaskDialog = when {
+     val familyDailyTaskDialog = when {
         AppBase.isProdEnv -> "https://web.yoki.chat/daily-task-fragment?projectname=yoki-family&hideAppBar=true"
         else -> "http://web-test.yoki.chat/daily-task-fragment?projectname=yoki-family&hideAppBar=true"
     }
@@ -262,25 +261,25 @@ object UrlConfig {
     /**
      * 家族等级说明
      */
-    internal val familyLevelDetail =
+     val familyLevelDetail =
         when {
             AppBase.isProdEnv -> "https://web.yoki.chat/rule?projectname=yoki-family&hideAppBar=true"
             else -> "http://web-test.yoki.chat/rule?projectname=yoki-family&hideAppBar=true"
         }
 
-    internal val dailyMsgLimitDetail =
+     val dailyMsgLimitDetail =
         when {
             AppBase.isProdEnv -> "https://web.yoki.chat/private-chat-task?projectname=yoki-common-page&hideAppBar=true"
             else -> "http://web-test.yoki.chat/private-chat-task?projectname=yoki-common-page&hideAppBar=true"
         }
 
-    internal val rankBoardRule =
+     val rankBoardRule =
         when {
             AppBase.isProdEnv -> "https://web.yoki.chat/rank-board-reward?projectname=yoki-rank-board&hideAppBar=true&tab_pos=%s"
             else -> "http://web-test.yoki.chat/rank-board-reward?projectname=yoki-rank-board&hideAppBar=true&tab_pos=%s"
         }
 
-    internal val greedyPersonal = when {
+     val greedyPersonal = when {
         isProdEnv -> "https://web.yoki.chat/index?projectname=yoki-greedy-personal&aspect_ratio=1.607"
         else -> "http://web-test.yoki.chat/index?projectname=yoki-greedy-personal&aspect_ratio=1.607"
     }

+ 20 - 1
frame/tuikit/TUIConversation/tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/presenter/ConversationPresenter.java

@@ -49,7 +49,7 @@ public class ConversationPresenter {
 
     protected int showType = SHOW_TYPE_CONVERSATION_LIST_WITH_FOLD;
 
-    protected static final int GET_CONVERSATION_COUNT = 100;
+    protected static final int GET_CONVERSATION_COUNT = 20; //最近20条
     protected static final int REFRESH_UNREAD_COUNT_DELAY = 200;
 
     protected ConversationEventListener conversationEventListener;
@@ -210,6 +210,25 @@ public class ConversationPresenter {
         });
     }
 
+    public void loadConversation(String conversationId) {
+        provider.getConversation(conversationId, new IUIKitCallback<ConversationInfo>() {
+            @Override
+            public void onSuccess(ConversationInfo data) {
+                ArrayList<ConversationInfo> list = new ArrayList<>();
+                list.add(data);
+                filterConversationList(list);
+                onLoadConversationCompleted(list);
+            }
+
+            @Override
+            public void onError(String module, int errCode, String errMsg) {
+//                if (adapter != null) {
+//                    adapter.onLoadingStateChanged(false);
+//                }
+            }
+        });
+    }
+
     private void filterConversationList(List<ConversationInfo> conversationInfoList) {
         if (isForwardMessage) {
             Iterator<ConversationInfo> iterator = conversationInfoList.listIterator();

+ 5 - 0
module/im/src/main/AndroidManifest.xml

@@ -24,6 +24,11 @@
             android:screenOrientation="portrait"
             android:theme="@style/AppTheme"
             android:windowSoftInputMode="adjustResize|adjustPan" />
+        <activity
+            android:name=".session.OfficialSessionActivity"
+            android:screenOrientation="portrait"
+            android:theme="@style/AppTheme"
+            android:windowSoftInputMode="adjustResize|adjustPan" />
     </application>
 
 </manifest>

+ 9 - 0
module/im/src/main/java/com/adealink/weparty/im/IMServiceImpl.kt

@@ -2,9 +2,13 @@ package com.adealink.weparty.im
 
 import android.app.Application
 import com.adealink.frame.spi.RegisterService
+import com.adealink.weparty.im.constant.OFFICIAL_IMAGE_TEXT_BUSINESS_ID
 import com.adealink.weparty.im.manager.imLoginManager
 import com.adealink.weparty.im.service.TIMAppService
+import com.adealink.weparty.im.session.mesasge.CustomMessageViewHolder
+import com.adealink.weparty.im.session.mesasge.OfficialImageTextMessageBean
 import com.adealink.weparty.module.im.IIMService
+import com.tencent.qcloud.tuikit.tuichat.config.TUIChatConfigs
 
 @RegisterService(value = IIMService::class)
 class IMServiceImpl : IIMService {
@@ -16,6 +20,11 @@ class IMServiceImpl : IIMService {
     }
 
     override fun appOnCreateMainTask(application: Application) {
+        //注册自定义消息类型
+        TUIChatConfigs.registerCustomMessage(
+            OFFICIAL_IMAGE_TEXT_BUSINESS_ID, OfficialImageTextMessageBean::class.java, CustomMessageViewHolder::class.java
+        )
+
         imLoginManager.init(application)
         TIMAppService().init(application)
     }

+ 14 - 0
module/im/src/main/java/com/adealink/weparty/im/constant/Data.kt

@@ -0,0 +1,14 @@
+package com.adealink.weparty.im.constant
+
+/**
+ * 官方用户ID
+ */
+const val OFFICIAL_UID = "10000" //官方uid
+const val OFFICIAL_CONVERSATION_ID = "c2c_${OFFICIAL_UID}"
+
+
+/**
+ * 自定义消息
+ * https://trtc.io/zh/document/50044?product=chat&menulabel=uikit&platform=android
+ */
+const val OFFICIAL_IMAGE_TEXT_BUSINESS_ID = "official_image_text"

+ 24 - 6
module/im/src/main/java/com/adealink/weparty/im/list/SessionListFragment.kt

@@ -19,6 +19,8 @@ import com.adealink.weparty.commonui.ext.dp
 import com.adealink.weparty.commonui.recycleview.itemdecoration.VerticalSpaceItemDecoration
 import com.adealink.weparty.im.R
 import com.adealink.weparty.im.comp.SessionPermissionComp
+import com.adealink.weparty.im.constant.OFFICIAL_CONVERSATION_ID
+import com.adealink.weparty.im.constant.OFFICIAL_UID
 import com.adealink.weparty.im.databinding.FragmentSessionListBinding
 import com.adealink.weparty.im.list.adapter.SessionListAdapter
 import com.adealink.weparty.im.list.adapter.viewbinder.OfficialListItemViewBinder
@@ -95,14 +97,9 @@ class SessionListFragment : BaseFragment(R.layout.fragment_session_list),
             .registerReceiver(unreadCountReceiver, unreadCountFilter)
     }
 
-    override fun loadData() {
-        super.loadData()
-//        binding.conversationLayout.loadConversation()
-//        binding.conversationLayout.loadMarkedConversation()
-    }
-
     override fun onResume() {
         super.onResume()
+        presenter.loadConversation(OFFICIAL_CONVERSATION_ID)
         binding.conversationLayout.loadConversation()
         binding.conversationLayout.loadMarkedConversation()
     }
@@ -121,6 +118,12 @@ class SessionListFragment : BaseFragment(R.layout.fragment_session_list),
         conversationInfo: ConversationInfo?
     ) {
         conversationInfo ?: return
+        // TODO: 测试阶段官方消息 userID为空
+        if (conversationInfo.conversation?.userID == OFFICIAL_UID) {
+            goOfficialSession(conversationInfo)
+            return
+        }
+
         activity?.let { act ->
             Router.build(act, IM.Session.PATH)
                 .putExtra(
@@ -135,6 +138,21 @@ class SessionListFragment : BaseFragment(R.layout.fragment_session_list),
         }
     }
 
+    private fun goOfficialSession(conversationInfo: ConversationInfo) {
+        activity?.let { act ->
+            Router.build(act, IM.OfficialSession.PATH)
+                .putExtra(
+                    IM.Session.EXTRA_CHAT_TYPE,
+                    if (conversationInfo.isGroup) V2TIMConversation.V2TIM_GROUP else V2TIMConversation.V2TIM_C2C
+                )
+                .putExtra(IM.Session.EXTRA_CHAT_ID, conversationInfo.id)
+                .putExtra(IM.Session.EXTRA_CHAT_NAME, conversationInfo.title)
+                .putExtra(IM.Session.EXTRA_CHAT_DRAFT_TEXT, conversationInfo.draft?.draftText)
+                .putExtra(IM.Session.EXTRA_CHAT_DRAFT_TIME, conversationInfo.draft?.draftTime)
+                .start()
+        }
+    }
+
     override fun onItemLongClick(
         view: View?,
         conversationInfo: ConversationInfo?

+ 13 - 5
module/im/src/main/java/com/adealink/weparty/im/list/adapter/SessionListAdapter.kt

@@ -1,6 +1,7 @@
 package com.adealink.weparty.im.list.adapter
 
 import com.adealink.weparty.commonui.recycleview.adapter.ExtMultiTypeAdapter
+import com.adealink.weparty.im.constant.OFFICIAL_CONVERSATION_ID
 import com.adealink.weparty.im.list.adapter.data.CommonSessionListItemData
 import com.adealink.weparty.im.list.adapter.data.OfficialSessionListItem
 import com.adealink.weparty.im.list.adapter.data.SessionListItemData
@@ -23,15 +24,22 @@ class SessionListAdapter : ExtMultiTypeAdapter(), IConversationListAdapter {
 
     override fun onDataSourceChanged(conversationInfoList: List<ConversationInfo?>?) {
         val itemList = mutableListOf<SessionListItemData>()
-        // TODO: 添加一个官方消息
-        itemList.add(
-            OfficialSessionListItem(ConversationInfo())
-        )
+        var officialConversation: ConversationInfo? = null
         conversationInfoList?.forEach {
             if (it != null) {
-                itemList.add(CommonSessionListItemData(it))
+                if (it.conversation.conversationID == OFFICIAL_CONVERSATION_ID) {
+                    officialConversation = it
+                    itemList.add(0, OfficialSessionListItem(it))
+                } else {
+                    itemList.add(CommonSessionListItemData(it))
+                }
             }
         }
+        if (officialConversation == null) {
+            itemList.add(0, OfficialSessionListItem(ConversationInfo().apply {
+                id = OFFICIAL_CONVERSATION_ID
+            }))
+        }
         items = itemList
     }
 

+ 2 - 1
module/im/src/main/java/com/adealink/weparty/im/manager/IMLoginManager.kt

@@ -57,12 +57,13 @@ class IMLoginManager : BaseFrame<ICallLoginListener>(),
             return
         }
         val uid = AccountModule.uid
+        //val uid = OFFICIAL_UID
         if (uid.isEmpty()) {
             Log.e(TAG_IM_LOGIN, "handleLogin fail, for $uid is invalid.")
             return
         }
         val userSig = GenerateUserSig.genTestUserSig(uid, IMConfig.APP_ID, IMConfig.SECRET_KEY)
-
+        Log.d(TAG_IM_LOGIN, "handleLogin, userSig:$userSig")
         val tuiLoginConfig: TUILoginConfig = TUIUtils.getLoginConfig()
         LoginWrapper.loginIMSDK(
             AppUtil.appContext,

+ 120 - 0
module/im/src/main/java/com/adealink/weparty/im/session/OfficialSessionActivity.kt

@@ -0,0 +1,120 @@
+package com.adealink.weparty.im.session
+
+import com.adealink.frame.base.fastLazy
+import com.adealink.frame.mvvm.view.viewBinding
+import com.adealink.frame.router.Router
+import com.adealink.frame.router.annotation.BindExtra
+import com.adealink.frame.router.annotation.RouterUri
+import com.adealink.frame.util.DisplayUtil.getStatusBarHeight
+import com.adealink.frame.util.onClick
+import com.adealink.weparty.commonui.BaseActivity
+import com.adealink.weparty.commonui.ext.dp
+import com.adealink.weparty.im.databinding.ActivityOfficialSessionBinding
+import com.adealink.weparty.module.im.IM
+import com.tencent.imsdk.v2.V2TIMConversation
+import com.tencent.qcloud.tuikit.tuichat.bean.C2CChatInfo
+import com.tencent.qcloud.tuikit.tuichat.bean.ChatInfo
+import com.tencent.qcloud.tuikit.tuichat.bean.DraftInfo
+import com.tencent.qcloud.tuikit.tuichat.bean.GroupChatInfo
+
+@RouterUri(path = [IM.OfficialSession.PATH], desc = "会话详情")
+class OfficialSessionActivity : BaseActivity() {
+
+    companion object {
+        private const val TAG = "OfficialSessionActivity"
+    }
+
+    @BindExtra(IM.Session.EXTRA_CHAT_TYPE)
+    var chatType: Int? = null
+
+    @BindExtra(IM.Session.EXTRA_CHAT_ID)
+    var chatID: String? = null
+
+    @BindExtra(IM.Session.EXTRA_CHAT_NAME)
+    var chatName: String? = null
+
+    @BindExtra(IM.Session.EXTRA_CHAT_DRAFT_TEXT)
+    var chatDraftText: String? = null
+
+    @BindExtra(IM.Session.EXTRA_CHAT_DRAFT_TIME)
+    var chatDraftTime: Long? = null
+
+    private val binding by viewBinding(ActivityOfficialSessionBinding::inflate)
+
+    private val sessionFragment: OfficialSessionFragment by fastLazy { OfficialSessionFragment() }
+
+    override fun onBeforeCreate() {
+        super.onBeforeCreate()
+        Router.bind(this)
+    }
+
+    override fun initViews() {
+        super.initViews()
+        setContentView(binding.root)
+        binding.topBar.root.setPadding(
+            0,
+            getStatusBarHeight(this@OfficialSessionActivity) + 5.dp(),
+            0,
+            5.dp()
+        )
+        binding.topBar.ivBack.onClick {
+            finish()
+        }
+        inflateSessionFragment()
+    }
+
+    private fun inflateSessionFragment() {
+        if (sessionFragment.isAdded) {
+            return
+        }
+        sessionFragment.setChatInfo(getChatInfo())
+        supportFragmentManager.beginTransaction()
+            .replace(binding.flContent.id, sessionFragment, IM.OfficialSession.PATH)
+            .commitAllowingStateLoss()
+    }
+
+    private fun getChatInfo(): ChatInfo? {
+        if (chatID.isNullOrEmpty()) {
+            return null
+        }
+
+        when (chatType) {
+            V2TIMConversation.V2TIM_C2C -> {
+                val chatInfo = C2CChatInfo()
+                chatInfo.type = ChatInfo.TYPE_C2C
+                chatInfo.id = chatID
+                chatInfo.setChatName(chatName)
+
+                if (!chatDraftText.isNullOrEmpty()) {
+                    chatInfo.draft = DraftInfo().apply {
+                        draftText = chatDraftText
+                        draftTime = chatDraftTime ?: 0
+                    }
+                }
+                return chatInfo
+            }
+
+            V2TIMConversation.V2TIM_GROUP -> {
+                val chatInfo = GroupChatInfo()
+                chatInfo.type = ChatInfo.TYPE_GROUP
+                chatInfo.id = chatID
+                chatInfo.setChatName(chatName)
+
+                if (!chatDraftText.isNullOrEmpty()) {
+                    chatInfo.draft = DraftInfo().apply {
+                        draftText = chatDraftText
+                        draftTime = chatDraftTime ?: 0
+                    }
+                }
+                return chatInfo
+            }
+
+            else -> {
+                return null
+            }
+        }
+
+    }
+
+
+}

+ 313 - 0
module/im/src/main/java/com/adealink/weparty/im/session/OfficialSessionFragment.kt

@@ -0,0 +1,313 @@
+package com.adealink.weparty.im.session
+
+import android.view.MotionEvent
+import android.view.View
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.updatePadding
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.adealink.frame.base.fastLazy
+import com.adealink.frame.ext.safeSubList
+import com.adealink.frame.log.Log
+import com.adealink.frame.mvvm.view.viewBinding
+import com.adealink.weparty.commonui.BaseFragment
+import com.adealink.weparty.commonui.ext.dp
+import com.adealink.weparty.commonui.ext.fitSystemWindows
+import com.adealink.weparty.commonui.ext.onWindowInsets
+import com.adealink.weparty.commonui.recycleview.itemdecoration.VerticalSpaceItemDecoration
+import com.adealink.weparty.im.R
+import com.adealink.weparty.im.databinding.FragmentOfficialSessionBinding
+import com.adealink.weparty.im.session.adapter.SessionAdapter
+import com.adealink.weparty.module.im.data.TAG_IM_SESSION
+import com.tencent.qcloud.tuicore.TUIConstants
+import com.tencent.qcloud.tuicore.TUICore
+import com.tencent.qcloud.tuikit.timcommon.bean.TUIMessageBean
+import com.tencent.qcloud.tuikit.timcommon.component.interfaces.IUIKitCallback
+import com.tencent.qcloud.tuikit.timcommon.interfaces.OnItemClickListener
+import com.tencent.qcloud.tuikit.tuichat.TUIChatConstants
+import com.tencent.qcloud.tuikit.tuichat.bean.C2CChatInfo
+import com.tencent.qcloud.tuikit.tuichat.bean.ChatInfo
+import com.tencent.qcloud.tuikit.tuichat.bean.GroupChatInfo
+import com.tencent.qcloud.tuikit.tuichat.component.audio.AudioPlayer
+import com.tencent.qcloud.tuikit.tuichat.component.audio.AudioRecorder
+import com.tencent.qcloud.tuikit.tuichat.interfaces.OnGestureScrollListener
+import com.tencent.qcloud.tuikit.tuichat.presenter.C2CChatPresenter
+import com.tencent.qcloud.tuikit.tuichat.presenter.ChatPresenter
+import com.tencent.qcloud.tuikit.tuichat.presenter.GroupChatPresenter
+
+class OfficialSessionFragment : BaseFragment(R.layout.fragment_official_session) {
+
+    private var chatInfo: ChatInfo? = null
+
+    private val binding by viewBinding(FragmentOfficialSessionBinding::bind)
+
+    private val sessionAdapter: SessionAdapter by fastLazy { SessionAdapter() }
+    private var sessionPresenter: ChatPresenter? = null
+
+    private var scrollDirection = 0
+
+    fun setChatInfo(chatInfo: ChatInfo?) {
+        this.chatInfo = chatInfo
+    }
+
+    override fun initViews() {
+        super.initViews()
+        when (chatInfo?.type) {
+            ChatInfo.TYPE_C2C -> {
+                sessionPresenter = C2CChatPresenter().also {
+                    it.initListener()
+                    it.chatInfo = chatInfo as C2CChatInfo
+                }
+            }
+
+            ChatInfo.TYPE_GROUP -> {
+                sessionPresenter = GroupChatPresenter().also {
+                    it.initListener()
+                    it.setGroupInfo(chatInfo as? GroupChatInfo)
+                }
+            }
+
+            else -> {
+                Log.d(TAG_IM_SESSION, "unknown chat info type:${chatInfo?.type}")
+            }
+        }
+        binding.rvMessage.setPresenter(sessionPresenter)
+        binding.rvMessage.setAdapter(sessionAdapter)
+        sessionAdapter.setOnItemClickListener(onMessageItemClickListener)
+        binding.rvMessage.addItemDecoration(VerticalSpaceItemDecoration(8.dp(), 24.dp(), 20.dp()))
+        binding.rvMessage.setOnGestureScrollListener(object : OnGestureScrollListener {
+            override fun onScroll(
+                m1: MotionEvent?,
+                m2: MotionEvent?,
+                distanceX: Float,
+                distanceY: Float
+            ) {
+                if (distanceY < 0) {
+                    scrollDirection = -1
+                } else if (distanceY > 0) {
+                    scrollDirection = 1
+                } else {
+                    scrollDirection = 0
+                }
+            }
+        })
+
+        binding.rvMessage.addOnScrollListener(object : RecyclerView.OnScrollListener() {
+            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
+                val linearLayoutManager =
+                    binding.rvMessage.layoutManager as LinearLayoutManager? ?: return
+                val firstVisiblePosition = linearLayoutManager.findFirstVisibleItemPosition()
+                val lastVisiblePosition = linearLayoutManager.findLastVisibleItemPosition()
+                sendMsgReadReceipt(firstVisiblePosition, lastVisiblePosition)
+                notifyMessageDisplayed(firstVisiblePosition, lastVisiblePosition)
+            }
+
+            override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
+                if (newState == RecyclerView.SCROLL_STATE_IDLE) {
+                    if (scrollDirection == -1) {
+                        if (!binding.rvMessage.canScrollVertically(-1)) {
+                            //加载
+                            //sessionAdapter.showLoading()
+                            loadMessages(TUIChatConstants.GET_MESSAGE_FORWARD)
+                        }
+                    } else if (scrollDirection == 1) {
+                        if (!binding.rvMessage.canScrollVertically(1)) {
+                            loadMessages(TUIChatConstants.GET_MESSAGE_BACKWARD)
+//                            displayBackToLastMessage(false)
+//                            displayBackToNewMessage(false, "", 0)
+                            sessionPresenter?.resetCurrentChatUnreadCount()
+                        }
+                    }
+                    scrollDirection = 0
+
+//                    if (binding.rvMessage.isDisplayJumpMessageLayout()) {
+//                        displayBackToLastMessage(true)
+//                    } else {
+//                        displayBackToLastMessage(false)
+//                    }
+                } else if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
+//                    hideBackToAtMessages()
+                }
+            }
+        })
+        //sessionAdapter.register()
+        //对方正在输入中
+        ///sessionPresenter.setTypingListener
+        sessionPresenter?.setMessageListAdapter(sessionAdapter)
+        sessionPresenter?.setMessageRecycleView(binding.rvMessage)
+
+        fitWindow()
+    }
+
+    private fun fitWindow() {
+        // 关键:只在需要的时候设置
+        activity?.window?.fitSystemWindows(false)
+        // 内容布局处理键盘
+        binding.root.onWindowInsets { view, insets ->
+            val ime = insets.getInsets(WindowInsetsCompat.Type.ime())
+            val nav = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
+
+            // 键盘显示时使用键盘高度,否则使用导航栏高度
+            val bottomInset = if (ime.bottom > 0) ime.bottom else nav.bottom
+            view.updatePadding(bottom = bottomInset)
+//            binding.svContent.doOnPreDraw {
+//                binding.svContent.smoothScrollTo(0, bottomInset)
+//            }
+            insets
+        }
+    }
+
+    override fun loadData() {
+        super.loadData()
+        sessionPresenter?.loadMessage(
+            if (chatInfo?.locateMessage == null) {
+                TUIChatConstants.GET_MESSAGE_FORWARD
+            } else {
+                TUIChatConstants.GET_MESSAGE_TWO_WAY
+            },
+            chatInfo?.locateMessage
+        )
+    }
+
+
+    fun loadMessages(lastMessage: TUIMessageBean?, type: Int) {
+        sessionPresenter?.loadMessage(type, lastMessage)
+    }
+
+    fun loadMessages(type: Int) {
+        if (type == TUIChatConstants.GET_MESSAGE_FORWARD) {
+            loadMessages(
+                sessionAdapter.firstMessageBean,
+                type
+            )
+        } else if (type == TUIChatConstants.GET_MESSAGE_BACKWARD) {
+            loadMessages(
+                sessionAdapter.lastMessageBean,
+                type
+            )
+        }
+    }
+
+    override fun onResume() {
+        super.onResume()
+        sessionPresenter?.isChatFragmentShow = true
+    }
+
+    override fun onPause() {
+        super.onPause()
+        sessionPresenter?.isChatFragmentShow = false
+        AudioPlayer.getInstance().stopPlay()
+    }
+
+    override fun onDestroyView() {
+        super.onDestroyView()
+        //收起输入法
+        AudioRecorder.cancelRecord()
+        AudioPlayer.getInstance().stopPlay()
+        sessionPresenter?.markMessageAsRead(chatInfo, false)
+    }
+
+    private fun sendMsgReadReceipt(firstPosition: Int, lastPosition: Int) {
+        if (sessionPresenter == null) {
+            return
+        }
+
+        val tuiMessageBeans =
+            sessionAdapter.items.safeSubList(firstPosition, lastPosition) as List<TUIMessageBean>
+        sessionPresenter?.sendMessageReadReceipt(tuiMessageBeans, object : IUIKitCallback<Void?>() {
+            override fun onSuccess(data: Void?) {}
+
+            override fun onError(module: String?, errCode: Int, errMsg: String?) {
+//                if (errCode == TUIConstants.BuyingFeature.ERR_SDK_INTERFACE_NOT_SUPPORT) {
+//                    showNotSupportDialog()
+//                }
+            }
+        })
+    }
+
+    private fun notifyMessageDisplayed(firstPosition: Int, lastPosition: Int) {
+        // *******************************
+
+        // *******************************
+//        markCallingMsgRead(firstPosition, lastPosition)
+
+        // *******************************
+        // *******************************
+        if (sessionPresenter == null) {
+            return
+        }
+        for (bean in sessionAdapter.items.safeSubList(firstPosition, lastPosition)) {
+            val param: MutableMap<String?, Any?> = HashMap<String?, Any?>()
+            param.put(TUIConstants.TUIChat.MESSAGE_BEAN, bean)
+            TUICore.notifyEvent(
+                TUIConstants.TUIChat.EVENT_KEY_MESSAGE_EVENT,
+                TUIConstants.TUIChat.EVENT_SUB_KEY_DISPLAY_MESSAGE_BEAN,
+                param
+            )
+        }
+    }
+
+    private val onMessageItemClickListener = object : OnItemClickListener() {
+        override fun onMessageLongClick(view: View?, messageBean: TUIMessageBean?) {
+            view ?: return
+            messageBean ?: return
+            onMessageLongClicked(view, messageBean)
+        }
+
+        override fun onMessageClick(view: View?, messageBean: TUIMessageBean?) {
+            view ?: return
+            messageBean ?: return
+        }
+
+        override fun onUserIconClick(view: View?, messageBean: TUIMessageBean?) {
+            view ?: return
+            messageBean ?: return
+        }
+
+        override fun onUserIconLongClick(view: View?, messageBean: TUIMessageBean?) {
+            view ?: return
+            messageBean ?: return
+        }
+
+        override fun onReEditRevokeMessage(view: View?, messageBean: TUIMessageBean?) {
+            view ?: return
+            messageBean ?: return
+        }
+
+        override fun onRecallClick(view: View?, messageBean: TUIMessageBean?) {
+            view ?: return
+            messageBean ?: return
+        }
+
+        override fun onReplyMessageClick(view: View?, messageBean: TUIMessageBean?) {
+            view ?: return
+            messageBean ?: return
+        }
+
+        override fun onReplyDetailClick(messageBean: TUIMessageBean?) {
+            messageBean ?: return
+        }
+
+        override fun onSendFailBtnClick(view: View?, messageBean: TUIMessageBean?) {
+            view ?: return
+            messageBean ?: return
+        }
+
+        override fun onTextSelected(view: View?, position: Int, messageBean: TUIMessageBean?) {
+            view ?: return
+            messageBean ?: return
+        }
+
+        override fun onMessageReadStatusClick(view: View?, messageBean: TUIMessageBean?) {
+            view ?: return
+            messageBean ?: return
+        }
+    }
+
+
+    private fun onMessageLongClicked(view: View, message: TUIMessageBean?) {
+        //展示长按弹窗
+        //chatView.getMessageLayout().showItemPopMenu(message, view)
+    }
+
+}

+ 9 - 1
module/im/src/main/java/com/adealink/weparty/im/session/adapter/SessionAdapter.kt

@@ -5,10 +5,12 @@ import androidx.recyclerview.widget.RecyclerView
 import com.adealink.weparty.commonui.recycleview.adapter.ExtMultiTypeAdapter
 import com.adealink.weparty.im.session.adapter.data.UnSupportMessageBean
 import com.adealink.weparty.im.session.adapter.viewbinder.ImageMessageViewBinder
+import com.adealink.weparty.im.session.adapter.viewbinder.OfficialImageTextMessageViewBinder
 import com.adealink.weparty.im.session.adapter.viewbinder.SoundMessageViewBinder
 import com.adealink.weparty.im.session.adapter.viewbinder.TextMessageViewBinder
 import com.adealink.weparty.im.session.adapter.viewbinder.TipsMessageViewBinder
 import com.adealink.weparty.im.session.adapter.viewbinder.UnSupportMessageViewBinder
+import com.adealink.weparty.im.session.mesasge.OfficialImageTextMessageBean
 import com.adealink.weparty.im.session.widget.MessageRecyclerView
 import com.tencent.qcloud.tuikit.timcommon.bean.TUIMessageBean
 import com.tencent.qcloud.tuikit.timcommon.component.highlight.HighlightPresenter
@@ -113,16 +115,22 @@ class SessionAdapter : ExtMultiTypeAdapter(),
         SoundMessageBean::class.java,
         TipsMessageBean::class.java,
         UnSupportMessageBean::class.java,
+
+        OfficialImageTextMessageBean::class.java,
     )
 
     init {
+        //官方消息
+        register(OfficialImageTextMessageBean::class.java,
+            OfficialImageTextMessageViewBinder(onMessageItemClick)
+        )
+        //普通消息
         register(TextMessageBean::class.java, TextMessageViewBinder(onMessageItemClick))
         register(ImageMessageBean::class.java, ImageMessageViewBinder(onMessageItemClick))
         register(SoundMessageBean::class.java, SoundMessageViewBinder(onMessageItemClick))
         register(TipsMessageBean::class.java, TipsMessageViewBinder(onMessageItemClick))
         register(UnSupportMessageBean::class.java, UnSupportMessageViewBinder(onMessageItemClick))
 
-
 //        addMessageType(FaceMessageBean::class.java, FaceMessageHolder::class.java)
 //        addMessageType(FileMessageBean::class.java, FileMessageHolder::class.java)
 //        addMessageType(LocationMessageBean::class.java, LocationMessageHolder::class.java)

+ 76 - 0
module/im/src/main/java/com/adealink/weparty/im/session/adapter/viewbinder/BaseOfficialMessageViewBinder.kt

@@ -0,0 +1,76 @@
+package com.adealink.weparty.im.session.adapter.viewbinder
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.annotation.CallSuper
+import androidx.viewbinding.ViewBinding
+import com.adealink.frame.util.onClick
+import com.adealink.weparty.commonui.recycleview.adapter.BindingViewHolder
+import com.adealink.weparty.commonui.recycleview.adapter.multitype.ItemViewBinder
+import com.adealink.weparty.im.databinding.LayoutSessionMessageOfficialBaseBinding
+import com.tencent.qcloud.tuikit.timcommon.bean.TUIMessageBean
+import com.tencent.qcloud.tuikit.timcommon.interfaces.OnItemClickListener
+
+
+/**
+ * 对照: ImageMessageHolder
+ */
+abstract class BaseOfficialMessageViewBinder<T : TUIMessageBean, V : ViewBinding, MH : OfficialMessageViewHolder<T, V>>(
+    protected var onItemClickListener: OnItemClickListener?
+) : ItemViewBinder<T, MH>() {
+
+    override fun onBindViewHolder(holder: MH, item: T) {
+        initView(holder, item)
+        setTimeTitle(holder, item)
+        holder.onBindMessage(holder.messageBinding, item)
+    }
+
+    @CallSuper
+    open fun initView(
+        holder: MH, msg: T
+    ) {
+        holder.binding.root.onClick {
+            onItemClickListener?.onMessageLongClick(holder.binding.root, msg)
+            true
+        }
+        if (msg.status == TUIMessageBean.MSG_STATUS_SEND_FAIL) {
+            //消息发送失败触发长按
+            holder.binding.root.onClick {
+                onItemClickListener?.onMessageLongClick(holder.binding.root, msg)
+                true
+            }
+        } else {
+            holder.binding.root.onClick {
+                onItemClickListener?.onMessageClick(holder.binding.root, msg)
+                true
+            }
+        }
+        holder.initView(holder.messageBinding, msg)
+    }
+
+    private fun setTimeTitle(holder: MH, msg: T) {
+
+    }
+
+    override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): MH {
+        val rootBinding = LayoutSessionMessageOfficialBaseBinding.inflate(inflater, parent, false)
+        return onCreateMessageHolder(rootBinding, inflater, rootBinding.messageContent)
+    }
+
+    abstract fun onCreateMessageHolder(
+        rootBinder: LayoutSessionMessageOfficialBaseBinding,
+        inflater: LayoutInflater,
+        parent: ViewGroup
+    ): MH
+
+}
+
+abstract class OfficialMessageViewHolder<T : TUIMessageBean, V : ViewBinding>(
+    rootBinding: LayoutSessionMessageOfficialBaseBinding,
+    val messageBinding: V
+) : BindingViewHolder<LayoutSessionMessageOfficialBaseBinding>(rootBinding) {
+
+    open fun initView(binding: V, msg: T) {}
+
+    abstract fun onBindMessage(binding: V, msg: T)
+}

+ 94 - 0
module/im/src/main/java/com/adealink/weparty/im/session/adapter/viewbinder/OfficialImageTextMessageViewBinder.kt

@@ -0,0 +1,94 @@
+package com.adealink.weparty.im.session.adapter.viewbinder
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import com.adealink.frame.util.onClick
+import com.adealink.weparty.commonui.ext.getActivity
+import com.adealink.weparty.commonui.ext.gone
+import com.adealink.weparty.commonui.ext.show
+import com.adealink.weparty.im.databinding.LayoutSessionMessageOfficialBaseBinding
+import com.adealink.weparty.im.databinding.LayoutSessionMessageOfficialImageTextBinding
+import com.adealink.weparty.im.session.mesasge.OfficialImageTextMessageBean
+import com.adealink.weparty.util.goLocalLinkPage
+import com.tencent.qcloud.tuikit.timcommon.component.face.FaceManager
+import com.tencent.qcloud.tuikit.timcommon.interfaces.OnItemClickListener
+
+/**
+ * 对照: TextMessageHolder
+ */
+class OfficialImageTextMessageViewHolder(
+    rootBinding: LayoutSessionMessageOfficialBaseBinding,
+    binding: LayoutSessionMessageOfficialImageTextBinding
+) : OfficialMessageViewHolder<OfficialImageTextMessageBean, LayoutSessionMessageOfficialImageTextBinding>(
+    rootBinding,
+    binding
+) {
+
+    override fun onBindMessage(
+        binding: LayoutSessionMessageOfficialImageTextBinding,
+        msg: OfficialImageTextMessageBean
+    ) {
+
+        if (!msg.image.isNullOrEmpty()) {
+            binding.ivImg.show()
+            binding.ivImg.setImageUrl(msg.image)
+        } else {
+            binding.ivImg.gone()
+        }
+
+        if (!msg.title.isNullOrEmpty()) {
+            binding.tvTitle.show()
+            FaceManager.handlerEmojiText(
+                binding.tvTitle,
+                msg.title,
+                false
+            )
+        } else {
+            binding.tvTitle.gone()
+        }
+
+        if (!msg.content.isNullOrEmpty()) {
+            binding.tvContent.show()
+            FaceManager.handlerEmojiText(
+                binding.tvContent,
+                msg.content,
+                false
+            )
+        } else {
+            binding.tvContent.gone()
+        }
+
+        if (!msg.link.isNullOrEmpty()) {
+            binding.vLink.show()
+            binding.vLink.onClick {
+                val activity = it.context.getActivity() ?: return@onClick
+                goLocalLinkPage(activity, msg.link)
+            }
+        } else {
+            binding.vLink.gone()
+        }
+    }
+
+}
+
+class OfficialImageTextMessageViewBinder(
+    onItemClickListener: OnItemClickListener
+) : BaseOfficialMessageViewBinder<OfficialImageTextMessageBean, LayoutSessionMessageOfficialImageTextBinding, OfficialImageTextMessageViewHolder>(
+    onItemClickListener
+) {
+    override fun onCreateMessageHolder(
+        rootBinder: LayoutSessionMessageOfficialBaseBinding,
+        inflater: LayoutInflater,
+        parent: ViewGroup
+    ): OfficialImageTextMessageViewHolder {
+        return OfficialImageTextMessageViewHolder(
+            rootBinder,
+            LayoutSessionMessageOfficialImageTextBinding.inflate(
+                inflater,
+                parent,
+                true
+            )
+        )
+    }
+
+}

+ 19 - 0
module/im/src/main/java/com/adealink/weparty/im/session/mesasge/CustomMessageViewHolder.kt

@@ -0,0 +1,19 @@
+package com.adealink.weparty.im.session.mesasge
+
+import android.view.View
+import com.tencent.qcloud.tuikit.timcommon.bean.TUIMessageBean
+import com.tencent.qcloud.tuikit.timcommon.minimalistui.widget.message.MessageContentHolder
+
+/**
+ * 用于自定义消息
+ */
+class CustomMessageViewHolder(itemView: View) : MessageContentHolder(itemView) {
+
+    override fun getVariableLayout(): Int {
+        return 0
+    }
+
+    override fun layoutVariableViews(msg: TUIMessageBean?, position: Int) {
+    }
+
+}

+ 41 - 0
module/im/src/main/java/com/adealink/weparty/im/session/mesasge/OfficialImageTextMessageBean.kt

@@ -0,0 +1,41 @@
+package com.adealink.weparty.im.session.mesasge
+
+import com.adealink.frame.data.json.froJsonErrorNull
+import com.google.gson.annotations.SerializedName
+import com.tencent.imsdk.v2.V2TIMMessage
+import com.tencent.qcloud.tuikit.timcommon.bean.TUIMessageBean
+import com.tencent.qcloud.tuikit.timcommon.bean.TUIReplyQuoteBean
+import com.tencent.qcloud.tuikit.tuichat.bean.message.reply.TextReplyQuoteBean
+
+data class OfficialImageTextData(
+    @SerializedName("title") val title: String? = null, //标题
+    @SerializedName("image") val image: String? = null, //配图
+    @SerializedName("content") var content: String? = null, //正文
+    @SerializedName("link") var link: String? = null, //链接
+)
+
+class OfficialImageTextMessageBean : TUIMessageBean() {
+
+    var title: String? = null //标题
+    var image: String? = null //配图
+    var content: String? = null //正文
+    var link: String? = null //链接
+    override fun onGetDisplayString(): String? {
+        return title
+    }
+
+    override fun onProcessMessage(v2TIMMessage: V2TIMMessage) {
+        val byte = v2TIMMessage.customElem?.data ?: return
+        val dataStr = String(byte)
+        val messageData = froJsonErrorNull<OfficialImageTextData>(dataStr)
+        //填充数据
+        this.title = messageData?.title
+        this.image = messageData?.image
+        this.content = messageData?.content
+        this.link = messageData?.link
+    }
+
+    override fun getReplyQuoteBeanClass(): Class<out TUIReplyQuoteBean<*>?> {
+        return TextReplyQuoteBean::class.java
+    }
+}

+ 25 - 0
module/im/src/main/res/layout/activity_official_session.xml

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <include
+        android:id="@+id/top_bar"
+        layout="@layout/layout_session_official_top_bar"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <androidx.fragment.app.FragmentContainerView
+        android:id="@+id/fl_content"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/top_bar" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 17 - 0
module/im/src/main/res/layout/fragment_official_session.xml

@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <com.adealink.weparty.im.session.widget.MessageRecyclerView
+        android:id="@+id/rv_message"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:background="@color/color_FFF1F2F5"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 40 - 0
module/im/src/main/res/layout/layout_session_message_official_base.xml

@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    tools:background="@color/color_FFF1F2F5">
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/tv_time"
+        android:layout_width="wrap_content"
+        android:layout_height="22dp"
+        android:background="@drawable/im_message_time_bg"
+        android:ellipsize="end"
+        android:gravity="center"
+        android:includeFontPadding="false"
+        android:paddingHorizontal="8dp"
+        android:singleLine="true"
+        android:textColor="@color/color_FF86909C"
+        android:textSize="11sp"
+        android:visibility="gone"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        tools:text="11/20"
+        tools:visibility="visible" />
+
+    <androidx.appcompat.widget.LinearLayoutCompat
+        android:id="@+id/message_content"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginHorizontal="16dp"
+        android:layout_marginTop="10dp"
+        android:orientation="horizontal"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/tv_time"
+        app:layout_goneMarginTop="0dp" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 109 - 0
module/im/src/main/res/layout/layout_session_message_official_image_text.xml

@@ -0,0 +1,109 @@
+<?xml version="1.0" encoding="utf-8"?>
+<com.adealink.weparty.commonui.widget.constrainlayout.RoundCornerConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    app:constraint_layout_round_corner="12dp">
+
+    <com.adealink.frame.image.view.NetworkImageView
+        android:id="@+id/iv_img"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        app:layout_constraintDimensionRatio="343:130"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:padding="12dp"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/iv_img">
+
+        <!--标题-->
+        <androidx.appcompat.widget.AppCompatTextView
+            android:id="@+id/tv_title"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:fontFamily="@font/poppins_semibold"
+            android:gravity="start"
+            android:includeFontPadding="false"
+            android:textColor="@color/color_FF1D2129"
+            android:textSize="14sp"
+            android:visibility="gone"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:text="Mobile Mobile Mobile Mobile Mobile Mobile Mobile Mobile "
+            tools:visibility="visible" />
+
+        <!--内容-->
+        <androidx.appcompat.widget.AppCompatTextView
+            android:id="@+id/tv_content"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="6dp"
+            android:gravity="start"
+            android:includeFontPadding="false"
+            android:textColor="@color/color_FF1D2129"
+            android:textSize="11sp"
+            android:visibility="gone"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@id/tv_title"
+            app:layout_goneMarginTop="0dp"
+            tools:text="Mobile Mobile Mobile Mobile Mobile Mobile Mobile Mobile "
+            tools:visibility="visible" />
+
+        <!--链接-->
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:id="@+id/v_link"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="6dp"
+            android:visibility="gone"
+            app:layout_constraintTop_toBottomOf="@id/tv_content"
+            app:layout_goneMarginTop="0dp"
+            tools:visibility="visible">
+
+            <View
+                android:layout_width="0dp"
+                android:layout_height="1dp"
+                android:background="@color/color_FFF2F3F5"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="parent" />
+
+            <androidx.appcompat.widget.AppCompatTextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginVertical="6dp"
+                android:layout_marginTop="6dp"
+                android:fontFamily="@font/poppins_semibold"
+                android:gravity="start"
+                android:includeFontPadding="false"
+                android:text="@string/im_official_view_now"
+                android:textColor="@color/color_FF3FBFBD"
+                android:textSize="14sp"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="parent"
+                app:layout_goneMarginTop="0dp" />
+
+            <androidx.appcompat.widget.AppCompatImageView
+                android:layout_width="16dp"
+                android:layout_height="16dp"
+                android:rotationY="@integer/locale_mirror_flip"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintTop_toTopOf="parent"
+                app:srcCompat="@drawable/common_go_ic" />
+
+        </androidx.constraintlayout.widget.ConstraintLayout>
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+</com.adealink.weparty.commonui.widget.constrainlayout.RoundCornerConstraintLayout>

+ 39 - 0
module/im/src/main/res/layout/layout_session_official_top_bar.xml

@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    app:layout_constraintEnd_toEndOf="parent"
+    app:layout_constraintStart_toStartOf="parent"
+    app:layout_constraintTop_toTopOf="parent"
+    tools:paddingBottom="5dp"
+    tools:paddingTop="5dp">
+
+    <androidx.appcompat.widget.AppCompatImageView
+        android:id="@+id/iv_back"
+        android:layout_width="24dp"
+        android:layout_height="24dp"
+        android:layout_marginStart="12dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:srcCompat="@drawable/commonui_back_black_48_ic" />
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/tv_user_name"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="10dp"
+        android:fontFamily="@font/poppins_semibold"
+        android:gravity="start|center_vertical"
+        android:includeFontPadding="false"
+        android:text="@string/im_official_session_title"
+        android:textColor="@color/color_FF1D2129"
+        android:textSize="18sp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintStart_toEndOf="@id/iv_back"
+        app:layout_constraintTop_toTopOf="parent" />
+
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 2 - 0
module/im/src/main/res/values/strings.xml

@@ -11,4 +11,6 @@
     <string name="im_mic_is_being_used_cant_record">Microphone is being used by another function, unable to record.</string>
     <string name="im_record_audio_failed">Audio recording failed.</string>
     <string name="im_sound_play_tip_for_voice_not_download">Voice file not downloaded.</string>
+    <string name="im_official_view_now">View now</string>
+    <string name="im_official_session_title">System Message</string>
 </resources>