Преглед на файлове

feat: 完成IM消息改造

DoggyZhang преди 3 месеца
родител
ревизия
ca5e4bb4b5
променени са 87 файла, в които са добавени 2731 реда и са изтрити 241 реда
  1. 15 0
      app/src/main/java/com/adealink/weparty/commonui/ext/ViewExt.kt
  2. 0 53
      app/src/main/java/com/adealink/weparty/commonui/recycleview/adapter/ScrollMultiTypeAdapter.kt
  3. 0 17
      app/src/main/java/com/adealink/weparty/imageselect/takePhoto/CameraExtensions.kt
  4. 6 0
      app/src/main/java/com/adealink/weparty/module/im/Router.kt
  5. 4 0
      app/src/main/java/com/adealink/weparty/module/im/data/Tags.kt
  6. 2 0
      app/src/main/java/com/adealink/weparty/util/SoftInputUtil.kt
  7. 23 13
      app/src/main/java/com/adealink/weparty/util/TimeUtil.kt
  8. 12 0
      app/src/main/res/values/styles.xml
  9. 1 1
      frame/tuikit/TUIChat/tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/config/GeneralConfig.java
  10. 0 7
      frame/tuikit/TUIConversation/tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/interfaces/IConversationListAdapter.java
  11. 2 1
      module/account/src/main/AndroidManifest.xml
  12. 25 0
      module/account/src/main/java/com/adealink/weparty/account/register/fragment/CompleteUserInfoFragment.kt
  13. 1 0
      module/account/src/main/res/layout/fragment_register_complete_userinfo.xml
  14. 1 0
      module/account/src/main/res/layout/fragment_register_select_category.xml
  15. 5 1
      module/im/src/main/AndroidManifest.xml
  16. 0 4
      module/im/src/main/java/com/adealink/weparty/im/adapter/data/BaseSessionData.kt
  17. 0 8
      module/im/src/main/java/com/adealink/weparty/im/adapter/data/SessionListData.kt
  18. 0 49
      module/im/src/main/java/com/adealink/weparty/im/data/UserInfo.kt
  19. 3 2
      module/im/src/main/java/com/adealink/weparty/im/list/SessionHomeListFragment.kt
  20. 70 52
      module/im/src/main/java/com/adealink/weparty/im/list/SessionListFragment.kt
  21. 18 16
      module/im/src/main/java/com/adealink/weparty/im/list/adapter/SessionListAdapter.kt
  22. 14 0
      module/im/src/main/java/com/adealink/weparty/im/list/adapter/data/SessionListData.kt
  23. 84 0
      module/im/src/main/java/com/adealink/weparty/im/list/adapter/viewbinder/OfficialListItemViewBinder.kt
  24. 89 0
      module/im/src/main/java/com/adealink/weparty/im/list/adapter/viewbinder/SessionListItemViewBinder.kt
  25. 11 0
      module/im/src/main/java/com/adealink/weparty/im/list/viewmodel/SessionListViewModel.kt
  26. 10 0
      module/im/src/main/java/com/adealink/weparty/im/list/widget/ISessionListLayout.java
  27. 74 0
      module/im/src/main/java/com/adealink/weparty/im/list/widget/SessionListLayout.kt
  28. 1 1
      module/im/src/main/java/com/adealink/weparty/im/manager/LoginWrapper.kt
  29. 128 0
      module/im/src/main/java/com/adealink/weparty/im/session/SessionActivity.kt
  30. 110 0
      module/im/src/main/java/com/adealink/weparty/im/session/SessionFragment.kt
  31. 268 0
      module/im/src/main/java/com/adealink/weparty/im/session/adapter/SessionAdapter.kt
  32. 13 0
      module/im/src/main/java/com/adealink/weparty/im/session/adapter/data/SessionData.kt
  33. 91 0
      module/im/src/main/java/com/adealink/weparty/im/session/adapter/viewbinder/BaseMessageViewBinder.kt
  34. 52 0
      module/im/src/main/java/com/adealink/weparty/im/session/adapter/viewbinder/ImageMessageViewBinder.kt
  35. 60 0
      module/im/src/main/java/com/adealink/weparty/im/session/adapter/viewbinder/SoundMessageViewBinder.kt
  36. 74 0
      module/im/src/main/java/com/adealink/weparty/im/session/adapter/viewbinder/TextMessageViewBinder.kt
  37. 91 0
      module/im/src/main/java/com/adealink/weparty/im/session/adapter/viewbinder/TipsMessageViewBinder.kt
  38. 43 0
      module/im/src/main/java/com/adealink/weparty/im/session/adapter/viewbinder/UnSupportMessageViewBinder.kt
  39. 57 0
      module/im/src/main/java/com/adealink/weparty/im/session/comp/SessionBottomComp.kt
  40. 105 0
      module/im/src/main/java/com/adealink/weparty/im/session/comp/SessionBottomInputComp.kt
  41. 63 0
      module/im/src/main/java/com/adealink/weparty/im/session/comp/SessionBottomVoiceComp.kt
  42. 29 0
      module/im/src/main/java/com/adealink/weparty/im/session/comp/SessionTopComp.kt
  43. 19 0
      module/im/src/main/java/com/adealink/weparty/im/session/comp/viewmodel/SessionBottomViewModel.kt
  44. 14 0
      module/im/src/main/java/com/adealink/weparty/im/session/viewmodel/SessionViewModel.kt
  45. 255 0
      module/im/src/main/java/com/adealink/weparty/im/session/widget/MessageRecyclerView.kt
  46. 44 0
      module/im/src/main/java/com/adealink/weparty/im/util/IMUIUtil.kt
  47. 4 1
      module/im/src/main/java/com/adealink/weparty/im/viewmodel/IMViewModelFactory.kt
  48. 0 11
      module/im/src/main/java/com/adealink/weparty/im/viewmodel/SessionListViewModel.kt
  49. BIN
      module/im/src/main/res/drawable-xhdpi/im_bubble_me_arrow_ic.png
  50. BIN
      module/im/src/main/res/drawable-xhdpi/im_bubble_other_arrow_ic.png
  51. BIN
      module/im/src/main/res/drawable-xhdpi/im_session_call_ic.png
  52. BIN
      module/im/src/main/res/drawable-xhdpi/im_session_emoji_ic.png
  53. BIN
      module/im/src/main/res/drawable-xhdpi/im_session_follow_ic.png
  54. BIN
      module/im/src/main/res/drawable-xhdpi/im_session_message_read_ic.png
  55. BIN
      module/im/src/main/res/drawable-xhdpi/im_session_message_unread_ic.png
  56. BIN
      module/im/src/main/res/drawable-xhdpi/im_session_more_ic.png
  57. BIN
      module/im/src/main/res/drawable-xhdpi/im_session_photo_ic.png
  58. BIN
      module/im/src/main/res/drawable-xhdpi/im_session_sound_play_ic.png
  59. BIN
      module/im/src/main/res/drawable-xhdpi/im_session_sound_stop_ic.png
  60. BIN
      module/im/src/main/res/drawable-xhdpi/im_session_voice_ic.png
  61. BIN
      module/im/src/main/res/drawable-xhdpi/im_session_voice_record_delete_ic.png
  62. BIN
      module/im/src/main/res/drawable-xhdpi/im_session_voice_record_pause_ic.png
  63. BIN
      module/im/src/main/res/drawable-xhdpi/im_session_voice_record_resume_ic.png
  64. BIN
      module/im/src/main/res/drawable-xhdpi/im_session_voice_record_send_ic.png
  65. 21 0
      module/im/src/main/res/drawable/im_message_bubble_other_bg.xml
  66. 21 0
      module/im/src/main/res/drawable/im_message_bubble_self_bg.xml
  67. 6 0
      module/im/src/main/res/drawable/im_message_time_bg.xml
  68. 8 0
      module/im/src/main/res/drawable/im_session_input_bg.xml
  69. 25 0
      module/im/src/main/res/layout/activity_session.xml
  70. 26 0
      module/im/src/main/res/layout/fragment_session.xml
  71. 1 0
      module/im/src/main/res/layout/fragment_session_home_list.xml
  72. 1 1
      module/im/src/main/res/layout/fragment_session_list.xml
  73. 26 0
      module/im/src/main/res/layout/layout_session_bottom_bar.xml
  74. 73 0
      module/im/src/main/res/layout/layout_session_bottom_input_bar.xml
  75. 76 0
      module/im/src/main/res/layout/layout_session_bottom_voice_bar.xml
  76. 3 3
      module/im/src/main/res/layout/layout_session_list_item.xml
  77. 0 0
      module/im/src/main/res/layout/layout_session_list_official_item.xml
  78. 40 0
      module/im/src/main/res/layout/layout_session_message_base.xml
  79. 33 0
      module/im/src/main/res/layout/layout_session_message_image.xml
  80. 67 0
      module/im/src/main/res/layout/layout_session_message_sound.xml
  81. 68 0
      module/im/src/main/res/layout/layout_session_message_text.xml
  82. 68 0
      module/im/src/main/res/layout/layout_session_message_tips.xml
  83. 55 0
      module/im/src/main/res/layout/layout_session_message_unsupport.xml
  84. 101 0
      module/im/src/main/res/layout/layout_session_top_bar.xml
  85. 14 0
      module/im/src/main/res/values/dimens.xml
  86. 3 0
      module/im/src/main/res/values/strings.xml
  87. 4 0
      module/im/src/main/res/values/styles.xml

+ 15 - 0
app/src/main/java/com/adealink/weparty/commonui/ext/ViewExt.kt

@@ -15,8 +15,12 @@ import android.view.TouchDelegate
 import android.view.View
 import android.view.ViewGroup
 import android.view.ViewParent
+import android.view.Window
 import android.view.animation.CycleInterpolator
 import android.view.animation.Interpolator
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsCompat
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.findViewTreeLifecycleOwner
 import androidx.recyclerview.widget.RecyclerView
@@ -250,4 +254,15 @@ fun ViewGroup.addViewSafe(view: View?) {
     if (view == null) return          // 空对象直接返回
     if (view.parent != null) return   // 已经有父 View,避免 IllegalStateException
     this.addView(view)
+}
+
+fun View.onWindowInsets(action: (View, WindowInsetsCompat) -> WindowInsetsCompat) {
+    ViewCompat.requestApplyInsets(this)
+    ViewCompat.setOnApplyWindowInsetsListener(this) { v, insets ->
+        action(v, insets)
+    }
+}
+
+fun Window.fitSystemWindows(decorFitsSystemWindows: Boolean) {
+    WindowCompat.setDecorFitsSystemWindows(this, decorFitsSystemWindows)
 }

+ 0 - 53
app/src/main/java/com/adealink/weparty/commonui/recycleview/adapter/ScrollMultiTypeAdapter.kt

@@ -1,53 +0,0 @@
-package com.adealink.weparty.commonui.recycleview.adapter
-
-import androidx.recyclerview.widget.RecyclerView
-import com.adealink.weparty.commonui.recycleview.adapter.multitype.MultiTypeAdapter
-
-/**
- * 无限循环
- */
-open class ScrollMultiTypeAdapter : MultiTypeAdapter() {
-
-    private var canScroll = true
-
-    fun setCanScroll(scroll: Boolean) {
-        canScroll = scroll
-    }
-
-    override fun getItemCount(): Int {
-        return if (canScroll) {
-            Int.MAX_VALUE
-        } else {
-            items.size
-        }
-    }
-
-    override fun getItemId(position: Int): Long {
-        if (items.isEmpty()) {
-            return RecyclerView.NO_ID
-        }
-        val item = items[position % items.size]
-        val itemViewType = getItemViewType(position)
-        return types.getType<Any>(itemViewType).delegate.getItemId(item)
-    }
-
-    override fun getItemViewType(position: Int): Int {
-        if (items.isEmpty()) {
-            return 0
-        }
-        return indexInTypesOf(position, items[position % items.size])
-    }
-
-    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: List<Any>) {
-        if (items.isEmpty()) {
-            return
-        }
-        val item = items[position % items.size]
-        getOutDelegateByViewHolder(holder).onBindViewHolder(holder, item, payloads)
-    }
-
-    fun getRealItemCount(): Int {
-        return items.size
-    }
-
-}

+ 0 - 17
app/src/main/java/com/adealink/weparty/imageselect/takePhoto/CameraExtensions.kt

@@ -1,19 +1,14 @@
 package com.adealink.weparty.imageselect.takePhoto
 
 import android.content.res.Configuration
-import android.view.View
 import android.view.View.GONE
 import android.view.View.VISIBLE
 import android.view.ViewAnimationUtils
 import android.view.ViewGroup
-import android.view.Window
 import android.widget.ImageButton
 import androidx.annotation.DrawableRes
 import androidx.core.animation.doOnEnd
 import androidx.core.animation.doOnStart
-import androidx.core.view.ViewCompat
-import androidx.core.view.WindowCompat
-import androidx.core.view.WindowInsetsCompat
 
 /**
  * @param flag 当前按钮状态
@@ -65,15 +60,3 @@ fun ViewGroup.circularClose(button: ImageButton, action: () -> Unit = {}) {
         doOnEnd { visibility = GONE }
     }.start()
 }
-
-fun View.onWindowInsets(action: (View, WindowInsetsCompat) -> Unit) {
-    ViewCompat.requestApplyInsets(this)
-    ViewCompat.setOnApplyWindowInsetsListener(this) { v, insets ->
-        action(v, insets)
-        insets
-    }
-}
-
-fun Window.fitSystemWindows() {
-    WindowCompat.setDecorFitsSystemWindows(this, false)
-}

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

@@ -24,6 +24,12 @@ interface IM {
     interface Session {
         companion object {
             const val PATH = "${Common.PATH}/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"
         }
     }
 }

+ 4 - 0
app/src/main/java/com/adealink/weparty/module/im/data/Tags.kt

@@ -0,0 +1,4 @@
+package com.adealink.weparty.module.im.data
+
+const val TAG_IM = "tag_im"
+const val TAG_IM_SESSION ="${TAG_IM}_session"

+ 2 - 0
app/src/main/java/com/adealink/weparty/util/SoftInputUtil.kt

@@ -0,0 +1,2 @@
+package com.adealink.weparty.util
+

+ 23 - 13
app/src/main/java/com/adealink/weparty/util/TimeUtil.kt

@@ -5,22 +5,14 @@ import com.adealink.frame.util.ONE_HOUR
 import com.adealink.frame.util.ONE_MINUTE
 import com.adealink.frame.util.ONE_SECOND
 import java.text.SimpleDateFormat
+import java.util.Calendar
 import java.util.Date
 import java.util.Locale
 
-@SuppressLint("SimpleDateFormat")
-fun timeToEnYYMMDD(timestamp: Long): String {
-    return SimpleDateFormat("yyyyMMdd", Locale.ENGLISH).format(Date(timestamp))
-}
-
-@SuppressLint("SimpleDateFormat")
-fun timeToEnYM(timestamp: Long): String {
-    return SimpleDateFormat("yyyy.MM", Locale.ENGLISH).format(Date(timestamp))
-}
 
 @SuppressLint("SimpleDateFormat")
-fun millis2String(timestamp: Long, pattern: String): String {
-    return SimpleDateFormat(pattern, Locale.ENGLISH).format(Date(timestamp))
+fun formatTimeTo(timestamp: Long, format: String, locale: Locale = Locale.ENGLISH): String {
+    return SimpleDateFormat(format, locale).format(Date(timestamp))
 }
 
 /**
@@ -33,9 +25,27 @@ fun formatCountdownTime(millis: Long): String {
     val leftMS = millis % ONE_HOUR
     val minutes = leftMS / ONE_MINUTE
     val seconds = (leftMS % ONE_MINUTE) / ONE_SECOND
-    
+
     fun formatTime(time: Long) = if (time < 10) "0$time" else "$time"
-    
+
     return "${formatTime(hours)}h : ${formatTime(minutes)}m : ${formatTime(seconds)}s"
 }
 
+fun isSameDay(timestamp1: Long, timestamp2: Long): Boolean {
+    return (timestamp1 / 86400) == (timestamp2 / 86400)
+}
+
+//快速计算是否同一年(不适用精准场景)
+fun isSameYearApprox(timestamp1: Long, timestamp2: Long): Boolean {
+    // 按平均一年365.2425天计算
+    //31,556,952,000 =365.2425 * 24 * 60 * 60 * 1000
+    return timestamp1 / 31_556_952_000 == timestamp2 / 31_556_952_000
+}
+
+
+// 精确判断(考虑闰年)
+fun isSameYearExact(timestamp1: Long, timestamp2: Long): Boolean {
+    val cal1 = Calendar.getInstance().apply { timeInMillis = timestamp1 }
+    val cal2 = Calendar.getInstance().apply { timeInMillis = timestamp2 }
+    return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR)
+}

+ 12 - 0
app/src/main/res/values/styles.xml

@@ -146,4 +146,16 @@
         <item name="android:layout_width">wrap_content</item>
         <item name="android:layout_height">wrap_content</item>
     </style>
+
+    <style name="CommonVerticalFade">
+        <item name="android:fadingEdgeLength">24dp</item>
+        <item name="android:requiresFadingEdge">vertical</item>
+        <item name="android:fadingEdge">vertical</item>
+    </style>
+
+    <style name="CommonHorizontalFade">
+        <item name="android:fadingEdgeLength">24dp</item>
+        <item name="android:requiresFadingEdge">horizontal</item>
+        <item name="android:fadingEdge">horizontal</item>
+    </style>
 </resources>

+ 1 - 1
frame/tuikit/TUIChat/tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/config/GeneralConfig.java

@@ -4,7 +4,7 @@ import com.tencent.qcloud.tuicore.util.SPUtils;
 import com.tencent.qcloud.tuikit.tuichat.TUIChatConstants;
 
 public class GeneralConfig {
-    public static final int DEFAULT_AUDIO_RECORD_MAX_TIME = 60;
+    public static final int DEFAULT_AUDIO_RECORD_MAX_TIME = 5 * 60;
     public static final int DEFAULT_VIDEO_RECORD_MAX_TIME = 15;
     public static final int FILE_MAX_SIZE = 100 * 1024 * 1024;
     public static final int VIDEO_MAX_SIZE = 100 * 1024 * 1024;

+ 0 - 7
frame/tuikit/TUIConversation/tuiconversation/src/main/java/com/tencent/qcloud/tuikit/tuiconversation/interfaces/IConversationListAdapter.java

@@ -5,13 +5,6 @@ import com.tencent.qcloud.tuikit.tuiconversation.bean.ConversationInfo;
 import java.util.List;
 
 public interface IConversationListAdapter {
-    /**
-     * Get the entry data of the adapter, which returns the ConversationInfo object or its sub-objects
-     *
-     * @param position
-     * @return ConversationInfo
-     */
-    ConversationInfo getItem(int position);
 
     void onLoadingStateChanged(boolean isLoading);
 

+ 2 - 1
module/account/src/main/AndroidManifest.xml

@@ -55,7 +55,8 @@
         <activity
             android:name=".register.RegisterProfileActivity"
             android:screenOrientation="portrait"
-            android:theme="@style/AppTheme" />
+            android:theme="@style/AppTheme"
+            android:windowSoftInputMode="adjustResize|adjustPan" />
 
     </application>
 

+ 25 - 0
module/account/src/main/java/com/adealink/weparty/account/register/fragment/CompleteUserInfoFragment.kt

@@ -1,7 +1,10 @@
 package com.adealink.weparty.account.register.fragment
 
 import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.doOnPreDraw
 import androidx.core.view.updateLayoutParams
+import androidx.core.view.updatePadding
 import androidx.fragment.app.activityViewModels
 import com.adealink.frame.aab.util.getCompatString
 import com.adealink.frame.base.Rlt
@@ -16,6 +19,8 @@ import com.adealink.weparty.account.register.dialog.ModifyAvatarDialog
 import com.adealink.weparty.account.register.viewmodel.RegisterProfileViewModel
 import com.adealink.weparty.account.viewModel.AccountViewModelFactory
 import com.adealink.weparty.commonui.BaseFragment
+import com.adealink.weparty.commonui.ext.fitSystemWindows
+import com.adealink.weparty.commonui.ext.onWindowInsets
 import com.adealink.weparty.commonui.toast.util.showFailedToast
 import com.adealink.weparty.commonui.toast.util.showToast
 import com.adealink.weparty.commonui.widget.wheel.WheelDatePickerDialog
@@ -48,6 +53,26 @@ class CompleteUserInfoFragment : BaseFragment(R.layout.fragment_register_complet
         binding.btnNext.onClick {
             nextStep()
         }
+
+        fitWindow()
+    }
+
+    private fun fitWindow() {
+        // 关键:只在需要的时候设置
+        activity?.window?.fitSystemWindows(false)
+        // 内容布局处理键盘
+        binding.svContent.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 observeViewModel() {

+ 1 - 0
module/account/src/main/res/layout/fragment_register_complete_userinfo.xml

@@ -54,6 +54,7 @@
         app:layout_constraintTop_toBottomOf="@id/tv_nice" />
 
     <androidx.core.widget.NestedScrollView
+        android:id="@+id/sv_content"
         android:layout_width="match_parent"
         android:layout_height="0dp"
         android:orientation="vertical"

+ 1 - 0
module/account/src/main/res/layout/fragment_register_select_category.xml

@@ -72,6 +72,7 @@
 
     <androidx.recyclerview.widget.RecyclerView
         android:id="@+id/rv_category"
+        style="@style/CommonVerticalFade"
         android:layout_width="0dp"
         android:layout_height="0dp"
         android:layout_marginTop="20dp"

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

@@ -19,7 +19,11 @@
     </dist:module>
 
     <application>
-
+        <activity
+            android:name=".session.SessionActivity"
+            android:screenOrientation="portrait"
+            android:theme="@style/AppTheme"
+            android:windowSoftInputMode="adjustResize|adjustPan" />
     </application>
 
 </manifest>

+ 0 - 4
module/im/src/main/java/com/adealink/weparty/im/adapter/data/BaseSessionData.kt

@@ -1,4 +0,0 @@
-package com.adealink.weparty.im.adapter.data
-
-class BaseSessionData {
-}

+ 0 - 8
module/im/src/main/java/com/adealink/weparty/im/adapter/data/SessionListData.kt

@@ -1,8 +0,0 @@
-package com.adealink.weparty.im.adapter.data
-
-import com.adealink.weparty.commonui.recycleview.diffutil.BaseListItemData
-import com.tencent.qcloud.tuikit.tuiconversation.bean.ConversationInfo
-
-sealed class SessionListItem :
-    ConversationInfo(),
-    BaseListItemData

+ 0 - 49
module/im/src/main/java/com/adealink/weparty/im/data/UserInfo.kt

@@ -8,17 +8,8 @@ import java.io.Serializable
 
 class UserInfo private constructor() : Serializable {
     var sdkAppId: Int = 0
-    private var zone: String? = null
-    var phone: String? = null
-        set(userPhone) {
-            field = userPhone
-            setUserInfo(this)
-        }
-    private var token: String? = null
     private var userId: String? = null
     private var userSig: String? = null
-    private var name: String? = null
-    private var avatar: String? = null
     private var autoLogin = false
     var isDebugLogin: Boolean = false
         set(debugLogin) {
@@ -40,15 +31,6 @@ class UserInfo private constructor() : Serializable {
         setUserInfo(this)
     }
 
-    fun getName(): String? {
-        return this.name
-    }
-
-    fun setName(name: String?) {
-        this.name = name
-        setUserInfo(this)
-    }
-
     fun getUserId(): String? {
         return this.userId
     }
@@ -58,24 +40,6 @@ class UserInfo private constructor() : Serializable {
         setUserInfo(this)
     }
 
-    fun getToken(): String? {
-        return this.token
-    }
-
-    fun setToken(token: String?) {
-        this.token = token
-        setUserInfo(this)
-    }
-
-    fun getZone(): String? {
-        return this.zone
-    }
-
-    fun setZone(zone: String?) {
-        this.zone = zone
-        setUserInfo(this)
-    }
-
     fun isAutoLogin(): Boolean {
         return this.autoLogin
     }
@@ -85,15 +49,6 @@ class UserInfo private constructor() : Serializable {
         setUserInfo(this)
     }
 
-    fun getAvatar(): String? {
-        return this.avatar
-    }
-
-    fun setAvatar(url: String?) {
-        this.avatar = url
-        setUserInfo(this)
-    }
-
     fun getLastLoginCode(): Int {
         return lastLoginCode
     }
@@ -105,12 +60,8 @@ class UserInfo private constructor() : Serializable {
 
     fun cleanUserInfo() {
         sdkAppId = 0
-        zone = ""
-        token = ""
         userId = ""
         userSig = ""
-        name = ""
-        avatar = ""
         autoLogin = false
         lastLoginCode = BaseConstants.ERR_SUCC
         setUserInfo(this)

+ 3 - 2
module/im/src/main/java/com/adealink/weparty/im/SessionHomeListFragment.kt → module/im/src/main/java/com/adealink/weparty/im/list/SessionHomeListFragment.kt

@@ -1,4 +1,4 @@
-package com.adealink.weparty.im
+package com.adealink.weparty.im.list
 
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.core.view.updateLayoutParams
@@ -9,9 +9,10 @@ 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.BaseFragment
+import com.adealink.weparty.im.R
 import com.adealink.weparty.im.databinding.FragmentSessionHomeListBinding
+import com.adealink.weparty.im.list.viewmodel.SessionListViewModel
 import com.adealink.weparty.im.viewmodel.IMViewModelFactory
-import com.adealink.weparty.im.viewmodel.SessionListViewModel
 import com.adealink.weparty.module.im.IM
 import com.adealink.weparty.module.profile.Profile
 

+ 70 - 52
module/im/src/main/java/com/adealink/weparty/im/SessionListFragment.kt → module/im/src/main/java/com/adealink/weparty/im/list/SessionListFragment.kt

@@ -1,4 +1,4 @@
-package com.adealink.weparty.im
+package com.adealink.weparty.im.list
 
 import android.content.BroadcastReceiver
 import android.content.Context
@@ -11,21 +11,24 @@ import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
 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.RouterUri
 import com.adealink.frame.util.AppUtil
 import com.adealink.weparty.commonui.BaseFragment
+import com.adealink.weparty.im.R
 import com.adealink.weparty.im.comp.SessionPermissionComp
 import com.adealink.weparty.im.databinding.FragmentSessionListBinding
+import com.adealink.weparty.im.list.adapter.SessionListAdapter
+import com.adealink.weparty.im.list.adapter.viewbinder.OfficialListItemViewBinder
+import com.adealink.weparty.im.list.adapter.viewbinder.SessionListItemViewBinder
+import com.adealink.weparty.im.list.viewmodel.SessionListViewModel
 import com.adealink.weparty.im.viewmodel.IMViewModelFactory
-import com.adealink.weparty.im.viewmodel.SessionListViewModel
 import com.adealink.weparty.module.im.IM
+import com.tencent.imsdk.v2.V2TIMConversation
 import com.tencent.qcloud.tuicore.TUIConstants
-import com.tencent.qcloud.tuikit.timcommon.component.swipe.Attributes
 import com.tencent.qcloud.tuikit.tuiconversation.bean.ConversationInfo
-import com.tencent.qcloud.tuikit.tuiconversation.interfaces.IConversationListAdapter
 import com.tencent.qcloud.tuikit.tuiconversation.minimalistui.interfaces.OnConversationAdapterListener
 import com.tencent.qcloud.tuikit.tuiconversation.minimalistui.util.TUIConversationUtils
-import com.tencent.qcloud.tuikit.tuiconversation.minimalistui.widget.ConversationListAdapter
 import com.tencent.qcloud.tuikit.tuiconversation.presenter.ConversationPresenter
 
 
@@ -33,15 +36,14 @@ import com.tencent.qcloud.tuikit.tuiconversation.presenter.ConversationPresenter
     path = [IM.SessionList.PATH],
     desc = "IM会话列表"
 )
-class SessionListFragment : BaseFragment(R.layout.fragment_session_list) {
+class SessionListFragment : BaseFragment(R.layout.fragment_session_list),
+    OnConversationAdapterListener {
 
     private val binding by viewBinding(FragmentSessionListBinding::bind)
 
     private val viewModel by viewModels<SessionListViewModel> { IMViewModelFactory() }
     private val presenter: ConversationPresenter by fastLazy { ConversationPresenter() }
-    private val sessionAdapter: IConversationListAdapter by fastLazy { ConversationListAdapter().apply {
-        mode = Attributes.Mode.Single
-    } }
+    private val sessionAdapter: SessionListAdapter by fastLazy { SessionListAdapter() }
     private lateinit var unreadCountReceiver: BroadcastReceiver
 
     override fun initViews() {
@@ -59,53 +61,14 @@ class SessionListFragment : BaseFragment(R.layout.fragment_session_list) {
         binding.conversationLayout.layoutManager =
             LinearLayoutManager(context, RecyclerView.VERTICAL, false)
 
+        sessionAdapter.register(OfficialListItemViewBinder(this))
+        sessionAdapter.register(SessionListItemViewBinder(this))
+
         presenter.setAdapter(sessionAdapter)
         presenter.setConversationListener()
         presenter.setShowType(ConversationPresenter.SHOW_TYPE_CONVERSATION_LIST_WITH_FOLD)
-        binding.conversationLayout.setAdapter(sessionAdapter)
+        binding.conversationLayout.adapter = sessionAdapter
         binding.conversationLayout.setPresenter(presenter)
-
-        binding.conversationLayout.setOnConversationAdapterListener(object :
-            OnConversationAdapterListener {
-            override fun onItemClick(
-                view: View?,
-                viewType: Int,
-                conversationInfo: ConversationInfo?
-            ) {
-                TUIConversationUtils.startChatActivity(conversationInfo)
-            }
-
-            override fun onItemLongClick(
-                view: View?,
-                conversationInfo: ConversationInfo?
-            ) {
-            }
-
-            override fun onConversationChanged(dataSource: List<ConversationInfo?>?) {
-            }
-
-            override fun onMarkConversationUnread(
-                view: View?,
-                conversationInfo: ConversationInfo?,
-                markUnread: Boolean
-            ) {
-            }
-
-            override fun onMarkConversationHidden(
-                view: View?,
-                conversationInfo: ConversationInfo?
-            ) {
-            }
-
-            override fun onClickMoreView(
-                view: View?,
-                conversationInfo: ConversationInfo?
-            ) {
-            }
-
-            override fun onSwipeConversationChanged(conversationInfo: ConversationInfo?) {
-            }
-        })
     }
 
     private fun initUnreadCountReceiver() {
@@ -146,4 +109,59 @@ class SessionListFragment : BaseFragment(R.layout.fragment_session_list) {
         presenter.destroy()
     }
 
+    override fun onItemClick(
+        view: View?,
+        viewType: Int,
+        conversationInfo: ConversationInfo?
+    ) {
+        conversationInfo ?: return
+        activity?.let { act ->
+            Router.build(act, IM.Session.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?
+    ) {
+        TUIConversationUtils.startChatActivity(conversationInfo)
+    }
+
+    override fun onConversationChanged(dataSource: List<ConversationInfo?>?) {
+
+    }
+
+    override fun onMarkConversationUnread(
+        view: View?,
+        conversationInfo: ConversationInfo?,
+        markUnread: Boolean
+    ) {
+
+    }
+
+    override fun onMarkConversationHidden(
+        view: View?,
+        conversationInfo: ConversationInfo?
+    ) {
+
+    }
+
+    override fun onClickMoreView(
+        view: View?,
+        conversationInfo: ConversationInfo?
+    ) {
+
+    }
+
+    override fun onSwipeConversationChanged(conversationInfo: ConversationInfo?) {
+
+    }
+
 }

+ 18 - 16
module/im/src/main/java/com/adealink/weparty/im/adapter/SessionListAdapter.kt → module/im/src/main/java/com/adealink/weparty/im/list/adapter/SessionListAdapter.kt

@@ -1,23 +1,20 @@
-package com.adealink.weparty.im.adapter
+package com.adealink.weparty.im.list.adapter
 
-import com.adealink.weparty.commonui.recycleview.adapter.MultiTypeListAdapter
-import com.adealink.weparty.im.adapter.data.SessionListItem
+import com.adealink.weparty.commonui.recycleview.adapter.ExtMultiTypeAdapter
+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
 import com.tencent.qcloud.tuikit.tuiconversation.bean.ConversationInfo
 import com.tencent.qcloud.tuikit.tuiconversation.interfaces.IConversationListAdapter
 import com.tencent.qcloud.tuikit.tuiconversation.minimalistui.interfaces.OnConversationAdapterListener
-import com.tencent.qcloud.tuikit.tuiconversation.minimalistui.widget.ConversationListAdapter
 
 /**
  * 参考 ConversationListAdapter, 抽象出业务逻辑
  */
-class SessionListAdapter : MultiTypeListAdapter<SessionListItem>(), IConversationListAdapter {
+class SessionListAdapter : ExtMultiTypeAdapter(), IConversationListAdapter {
 
     private var mIsLoading = false
-    private var mOnConversationAdapterListener: OnConversationAdapterListener? = null
-
-    fun setOnConversationAdapterListener(listener: OnConversationAdapterListener?) {
-        this.mOnConversationAdapterListener = listener
-    }
+    var mOnConversationAdapterListener: OnConversationAdapterListener? = null
 
     override fun onLoadingStateChanged(isLoading: Boolean) {
         this.mIsLoading = isLoading
@@ -25,7 +22,17 @@ class SessionListAdapter : MultiTypeListAdapter<SessionListItem>(), IConversatio
     }
 
     override fun onDataSourceChanged(conversationInfoList: List<ConversationInfo?>?) {
-        items = conversationInfoList?.mapNotNull { it } ?: emptyList()
+        val itemList = mutableListOf<SessionListItemData>()
+        // TODO: 添加一个官方消息
+        itemList.add(
+            OfficialSessionListItem(ConversationInfo())
+        )
+        conversationInfoList?.forEach {
+            if (it != null) {
+                itemList.add(CommonSessionListItemData(it))
+            }
+        }
+        items = itemList
     }
 
     override fun onViewNeedRefresh() {
@@ -36,11 +43,6 @@ class SessionListAdapter : MultiTypeListAdapter<SessionListItem>(), IConversatio
         notifyItemRemoved(position)
     }
 
-    private fun getItemIndexInAdapter(index: Int): Int {
-        val itemIndex: Int = index + ConversationListAdapter.HEADER_COUNT
-        return itemIndex
-    }
-
     override fun onItemInserted(position: Int) {
         notifyItemInserted(position)
     }

+ 14 - 0
module/im/src/main/java/com/adealink/weparty/im/list/adapter/data/SessionListData.kt

@@ -0,0 +1,14 @@
+package com.adealink.weparty.im.list.adapter.data
+
+import com.adealink.weparty.commonui.recycleview.diffutil.BaseListItemData
+import com.tencent.qcloud.tuikit.tuiconversation.bean.ConversationInfo
+
+sealed class SessionListItemData() : BaseListItemData
+
+data class OfficialSessionListItem(
+    val data: ConversationInfo
+) : SessionListItemData()
+
+data class CommonSessionListItemData(
+    val data: ConversationInfo
+) : SessionListItemData()

+ 84 - 0
module/im/src/main/java/com/adealink/weparty/im/list/adapter/viewbinder/OfficialListItemViewBinder.kt

@@ -0,0 +1,84 @@
+package com.adealink.weparty.im.list.adapter.viewbinder
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import com.adealink.frame.data.json.froJsonErrorNull
+import com.adealink.frame.dot.NumDot
+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.LayoutSessionListOfficialItemBinding
+import com.adealink.weparty.im.list.adapter.data.OfficialSessionListItem
+import com.adealink.weparty.im.util.formatSessionTime
+import com.tencent.qcloud.tuikit.tuiconversation.bean.DraftInfo
+import com.tencent.qcloud.tuikit.tuiconversation.minimalistui.interfaces.OnConversationAdapterListener
+import com.tencent.qcloud.tuikit.tuiconversation.presenter.ConversationPresenter
+
+
+class OfficialListItemViewBinder(
+    val listener: OnConversationAdapterListener
+) :
+    ItemViewBinder<OfficialSessionListItem, BindingViewHolder<LayoutSessionListOfficialItemBinding>>() {
+
+    override fun onBindViewHolder(
+        holder: BindingViewHolder<LayoutSessionListOfficialItemBinding>,
+        item: OfficialSessionListItem,
+    ) {
+        holder.binding.root.onClick {
+            listener.onItemClick(it, item.data.type, item.data)
+        }
+        holder.binding.tvTitle.text = item.data.title
+        setLastMessageAndStatus(holder, item)
+    }
+
+    private fun setLastMessageAndStatus(
+        holder: BindingViewHolder<LayoutSessionListOfficialItemBinding>,
+        item: OfficialSessionListItem,
+    ) {
+        val conversation = item.data
+
+        val draftInfo: DraftInfo? = conversation.draft
+        if (draftInfo != null) {
+            //草稿
+            val draftText =
+                (froJsonErrorNull<HashMap<*, *>>(draftInfo.draftText)?.get("content") as? String)
+                    ?: draftInfo.draftText
+
+            holder.binding.tvDesc.text = draftText
+            setLastMessageTime(holder, draftInfo.draftTime * 1000)
+        } else {
+            //上一条消息
+            val lasTUIMessageBean = conversation.lastTUIMessageBean
+            if (lasTUIMessageBean != null) {
+                val displayString = ConversationPresenter.getMessageDisplayString(lasTUIMessageBean)
+                holder.binding.tvDesc.text = displayString
+            }
+            if (conversation.lastMessage != null) {
+                setLastMessageTime(holder, conversation.lastMessageTime * 1000)
+            }
+        }
+
+        holder.binding.vDot.show(NumDot(conversation.unRead))
+    }
+
+    private fun setLastMessageTime(
+        holder: BindingViewHolder<LayoutSessionListOfficialItemBinding>,
+        timeTs: Long
+    ) {
+
+        holder.binding.tvTime.text = timeTs.formatSessionTime()
+    }
+
+    override fun onCreateViewHolder(
+        inflater: LayoutInflater,
+        parent: ViewGroup,
+    ): BindingViewHolder<LayoutSessionListOfficialItemBinding> {
+        return BindingViewHolder(
+            LayoutSessionListOfficialItemBinding.inflate(
+                inflater,
+                parent,
+                false
+            )
+        )
+    }
+}

+ 89 - 0
module/im/src/main/java/com/adealink/weparty/im/list/adapter/viewbinder/SessionListItemViewBinder.kt

@@ -0,0 +1,89 @@
+package com.adealink.weparty.im.list.adapter.viewbinder
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import com.adealink.frame.data.json.froJsonErrorNull
+import com.adealink.frame.dot.NumDot
+import com.adealink.frame.log.Log
+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.LayoutSessionListItemBinding
+import com.adealink.weparty.im.list.adapter.data.CommonSessionListItemData
+import com.adealink.weparty.im.util.formatSessionTime
+import com.tencent.qcloud.tuikit.tuiconversation.bean.DraftInfo
+import com.tencent.qcloud.tuikit.tuiconversation.minimalistui.interfaces.OnConversationAdapterListener
+import com.tencent.qcloud.tuikit.tuiconversation.presenter.ConversationPresenter
+
+
+class SessionListItemViewBinder(
+    val listener: OnConversationAdapterListener
+) :
+    ItemViewBinder<CommonSessionListItemData, BindingViewHolder<LayoutSessionListItemBinding>>() {
+
+    override fun onBindViewHolder(
+        holder: BindingViewHolder<LayoutSessionListItemBinding>,
+        item: CommonSessionListItemData,
+    ) {
+        Log.d("zhangfei", "onBindViewHolder, $item")
+        holder.binding.root.onClick {
+            listener.onItemClick(it, item.data.type, item.data)
+        }
+
+        // TODO: 长按测试用
+        holder.binding.root.setOnLongClickListener {
+            listener.onItemLongClick(it, item.data)
+            true
+        }
+
+        holder.binding.ivAvatar.onClick {
+
+        }
+        holder.binding.tvTitle.text = item.data.title
+        setLastMessageAndStatus(holder, item)
+    }
+
+    private fun setLastMessageAndStatus(
+        holder: BindingViewHolder<LayoutSessionListItemBinding>,
+        item: CommonSessionListItemData,
+    ) {
+        val conversation = item.data
+
+        val draftInfo: DraftInfo? = conversation.draft
+        if (draftInfo != null) {
+            //草稿
+            val draftText =
+                (froJsonErrorNull<HashMap<*, *>>(draftInfo.draftText)?.get("content") as? String)
+                    ?: draftInfo.draftText
+
+            holder.binding.tvDesc.text = draftText
+            setLastMessageTime(holder, draftInfo.draftTime)
+        } else {
+            //上一条消息
+            val lasTUIMessageBean = conversation.lastTUIMessageBean
+            if (lasTUIMessageBean != null) {
+                val displayString = ConversationPresenter.getMessageDisplayString(lasTUIMessageBean)
+                holder.binding.tvDesc.text = displayString
+            }
+            if (conversation.lastMessage != null) {
+                setLastMessageTime(holder, conversation.lastMessageTime * 1000)
+            }
+        }
+
+        holder.binding.vDot.show(NumDot(conversation.unRead))
+    }
+
+    private fun setLastMessageTime(
+        holder: BindingViewHolder<LayoutSessionListItemBinding>,
+        timeTs: Long
+    ) {
+        holder.binding.tvTime.text = timeTs.formatSessionTime()
+    }
+
+    override fun onCreateViewHolder(
+        inflater: LayoutInflater,
+        parent: ViewGroup,
+    ): BindingViewHolder<LayoutSessionListItemBinding> {
+        return BindingViewHolder(LayoutSessionListItemBinding.inflate(inflater, parent, false))
+    }
+}

+ 11 - 0
module/im/src/main/java/com/adealink/weparty/im/list/viewmodel/SessionListViewModel.kt

@@ -0,0 +1,11 @@
+package com.adealink.weparty.im.list.viewmodel
+
+import com.adealink.frame.mvvm.viewmodel.BaseViewModel
+
+class SessionListViewModel : BaseViewModel() {
+
+    fun clearMessage() {
+
+    }
+
+}

+ 10 - 0
module/im/src/main/java/com/adealink/weparty/im/list/widget/ISessionListLayout.java

@@ -0,0 +1,10 @@
+package com.adealink.weparty.im.list.widget;
+
+public interface ISessionListLayout {
+
+    /**
+     * Do not display the switch for the number of unread messages with the small red dot
+     */
+    void disableItemUnreadDot(boolean flag);
+
+}

+ 74 - 0
module/im/src/main/java/com/adealink/weparty/im/list/widget/SessionListLayout.kt

@@ -0,0 +1,74 @@
+package com.adealink.weparty.im.list.widget
+
+import android.content.Context
+import android.util.AttributeSet
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.SimpleItemAnimator
+import com.adealink.weparty.im.list.adapter.SessionListAdapter
+import com.tencent.qcloud.tuikit.timcommon.component.CustomLinearLayoutManager
+import com.tencent.qcloud.tuikit.tuiconversation.presenter.ConversationPresenter
+
+class SessionListLayout @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyleAttr: Int = 0
+) : RecyclerView(context, attrs, defStyleAttr), ISessionListLayout {
+    private var mAdapter: SessionListAdapter? = null
+    private var presenter: ConversationPresenter? = null
+
+    init {
+        isLayoutFrozen = false
+        setItemViewCacheSize(0)
+        setHasFixedSize(true)
+        setFocusableInTouchMode(false)
+        val linearLayoutManager = CustomLinearLayoutManager(context)
+        linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL)
+        setLayoutManager(linearLayoutManager)
+        val animator = itemAnimator as SimpleItemAnimator?
+        animator?.supportsChangeAnimations = false
+    }
+
+    fun setPresenter(presenter: ConversationPresenter?) {
+        this.presenter = presenter
+    }
+
+    override fun setAdapter(adapter: Adapter<*>?) {
+        super.setAdapter(adapter)
+        this.mAdapter = adapter as? SessionListAdapter
+    }
+
+    override fun disableItemUnreadDot(flag: Boolean) {
+//        mAdapter!!.disableItemUnreadDot(flag)
+    }
+
+    override fun onScrollStateChanged(state: Int) {
+        super.onScrollStateChanged(state)
+        if (state == SCROLL_STATE_IDLE) {
+            val layoutManager = layoutManager as LinearLayoutManager?
+            if (layoutManager == null) {
+                return
+            }
+            mAdapter?.let { adapter ->
+                val lastPosition = layoutManager.findLastCompletelyVisibleItemPosition()
+                if (lastPosition == adapter.itemCount - 1 && !this.isLoadCompleted) {
+                    adapter.onLoadingStateChanged(true)
+                    presenter?.loadMoreConversation()
+                }
+            }
+        }
+    }
+
+    fun loadConversation() {
+        presenter?.loadMoreConversation()
+    }
+
+    fun loadMarkedConversation() {
+        presenter?.loadMarkedConversation()
+    }
+
+    val isLoadCompleted: Boolean
+        get() {
+            return presenter?.isLoadFinished ?: false
+        }
+}

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

@@ -156,7 +156,7 @@ object LoginWrapper {
                         }
 
                         override fun onError(errorCode: Int, errorMessage: String?) {
-                            loginUserInfo!!.setLastLoginCode(errorCode)
+                            loginUserInfo?.setLastLoginCode(errorCode)
                             Log.e(TAG_IM_LOGIN, "tryToAutoLogin error:" + errorCode)
                             //错误码详见:
                             //https://trtc.io/zh/document/34348?product=chat&platform=android

+ 128 - 0
module/im/src/main/java/com/adealink/weparty/im/session/SessionActivity.kt

@@ -0,0 +1,128 @@
+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.weparty.commonui.BaseActivity
+import com.adealink.weparty.commonui.ext.dp
+import com.adealink.weparty.im.databinding.ActivitySessionBinding
+import com.adealink.weparty.im.session.comp.SessionTopComp
+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.Session.PATH], desc = "会话详情")
+class SessionActivity : BaseActivity() {
+
+    companion object {
+        private const val TAG = "SessionActivity"
+    }
+
+    @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(ActivitySessionBinding::inflate)
+
+    private val sessionFragment: SessionFragment by fastLazy { SessionFragment() }
+
+    override fun onBeforeCreate() {
+        super.onBeforeCreate()
+        Router.bind(this)
+    }
+
+    override fun initViews() {
+        super.initViews()
+        setContentView(binding.root)
+        binding.topBar.root.setPadding(
+            0,
+            getStatusBarHeight(this@SessionActivity) + 5.dp(),
+            0,
+            5.dp()
+        )
+        inflateSessionFragment()
+    }
+
+    override fun initComponents() {
+        super.initComponents()
+        SessionTopComp(this, binding.topBar).attach()
+    }
+
+    private fun inflateSessionFragment() {
+        if (sessionFragment.isAdded) {
+            return
+        }
+        sessionFragment.setChatInfo(getChatInfo())
+        supportFragmentManager.beginTransaction()
+            .replace(binding.flContent.id, sessionFragment, IM.Session.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
+            }
+        }
+
+
+//        val v2TIMMessage =
+//            intent.getSerializableExtra(TUIConstants.TUIChat.LOCATE_MESSAGE) as V2TIMMessage?
+//        val messageInfo = ChatMessageBuilder.buildMessage(v2TIMMessage)
+//        chatInfo.setLocateMessage(messageInfo)
+//        chatInfo.setAtInfoList(intent.getSerializableExtra(TUIConstants.TUIChat.AT_INFO_LIST) as MutableList<V2TIMGroupAtInfo?>?)
+//        chatInfo.setFaceUrl(intent.getStringExtra(TUIConstants.TUIChat.FACE_URL))
+    }
+
+}

+ 110 - 0
module/im/src/main/java/com/adealink/weparty/im/session/SessionFragment.kt

@@ -0,0 +1,110 @@
+package com.adealink.weparty.im.session
+
+import com.adealink.frame.base.fastLazy
+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.recycleview.itemdecoration.VerticalSpaceItemDecoration
+import com.adealink.weparty.im.R
+import com.adealink.weparty.im.databinding.FragmentSessionBinding
+import com.adealink.weparty.im.session.adapter.SessionAdapter
+import com.adealink.weparty.im.session.comp.SessionBottomComp
+import com.adealink.weparty.module.im.data.TAG_IM_SESSION
+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.presenter.C2CChatPresenter
+import com.tencent.qcloud.tuikit.tuichat.presenter.ChatPresenter
+import com.tencent.qcloud.tuikit.tuichat.presenter.GroupChatPresenter
+
+class SessionFragment : BaseFragment(R.layout.fragment_session) {
+
+    private var chatInfo: ChatInfo? = null
+
+    private val binding by viewBinding(FragmentSessionBinding::bind)
+
+    private val sessionAdapter: SessionAdapter by fastLazy { SessionAdapter() }
+    private var sessionPresenter: ChatPresenter? = null
+
+    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)
+        binding.rvMessage.addItemDecoration(VerticalSpaceItemDecoration(8.dp(), 24.dp(), 20.dp()))
+        //sessionAdapter.register()
+        //对方正在输入中
+        ///sessionPresenter.setTypingListener
+        sessionPresenter?.setMessageListAdapter(sessionAdapter)
+        sessionPresenter?.setMessageRecycleView(binding.rvMessage)
+
+
+    }
+
+    override fun loadData() {
+        super.loadData()
+        sessionPresenter?.loadMessage(
+            if (chatInfo?.locateMessage == null) {
+                TUIChatConstants.GET_MESSAGE_FORWARD
+            } else {
+                TUIChatConstants.GET_MESSAGE_TWO_WAY
+            },
+            chatInfo?.locateMessage
+        )
+    }
+
+    override fun initComponents() {
+        super.initComponents()
+        SessionBottomComp(
+            this,
+            binding.bottomBar,
+            chatInfo as? C2CChatInfo
+        ).attach()
+    }
+
+    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)
+    }
+
+}

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

@@ -0,0 +1,268 @@
+package com.adealink.weparty.im.session.adapter
+
+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.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.widget.MessageRecyclerView
+import com.tencent.qcloud.tuikit.timcommon.bean.TUIMessageBean
+import com.tencent.qcloud.tuikit.timcommon.component.highlight.HighlightPresenter
+import com.tencent.qcloud.tuikit.timcommon.interfaces.ICommonMessageAdapter
+import com.tencent.qcloud.tuikit.timcommon.interfaces.UserFaceUrlCache
+import com.tencent.qcloud.tuikit.timcommon.minimalistui.widget.message.MessageBaseHolder
+import com.tencent.qcloud.tuikit.tuichat.bean.message.ImageMessageBean
+import com.tencent.qcloud.tuikit.tuichat.bean.message.SoundMessageBean
+import com.tencent.qcloud.tuikit.tuichat.bean.message.TextMessageBean
+import com.tencent.qcloud.tuikit.tuichat.bean.message.TipsMessageBean
+import com.tencent.qcloud.tuikit.tuichat.interfaces.IMessageAdapter
+import com.tencent.qcloud.tuikit.tuichat.interfaces.IMessageRecyclerView
+
+class SessionAdapter : ExtMultiTypeAdapter(), IMessageAdapter, ICommonMessageAdapter {
+
+    companion object {
+        private const val ITEM_POSITION_UNKNOWN: Int = -1
+    }
+
+    private var mLoading: Boolean = true
+    private var mRecycleView: MessageRecyclerView? = null
+
+    private val supportMessageTypes = setOf(
+        TextMessageBean::class.java,
+        ImageMessageBean::class.java,
+        SoundMessageBean::class.java,
+        TipsMessageBean::class.java,
+        UnSupportMessageBean::class.java,
+    )
+
+    init {
+        register(TextMessageBean::class.java, TextMessageViewBinder())
+        register(ImageMessageBean::class.java, ImageMessageViewBinder())
+        register(SoundMessageBean::class.java, SoundMessageViewBinder())
+        register(TipsMessageBean::class.java, TipsMessageViewBinder())
+        register(UnSupportMessageBean::class.java, UnSupportMessageViewBinder())
+
+
+//        addMessageType(FaceMessageBean::class.java, FaceMessageHolder::class.java)
+//        addMessageType(FileMessageBean::class.java, FileMessageHolder::class.java)
+//        addMessageType(LocationMessageBean::class.java, LocationMessageHolder::class.java)
+//        addMessageType(MergeMessageBean::class.java, MergeMessageHolder::class.java)
+//        addMessageType(TextAtMessageBean::class.java, TextMessageHolder::class.java)
+//        addMessageType(VideoMessageBean::class.java, VideoMessageHolder::class.java)
+//        addMessageType(ReplyMessageBean::class.java, ReplyMessageHolder::class.java)
+//        addMessageType(QuoteMessageBean::class.java, QuoteMessageHolder::class.java)
+//        addMessageType(CallingMessageBean::class.java, CallingMessageHolder::class.java)
+//        addMessageType(CallingTipsMessageBean::class.java, TipsMessageHolder::class.java, true)
+//        addMessageType(CustomLinkMessageBean::class.java, CustomLinkMessageHolder::class.java)
+//        addMessageType(
+//            CustomEvaluationMessageBean::class.java,
+//            CustomEvaluationMessageHolder::class.java
+//        )
+//        addMessageType(CustomOrderMessageBean::class.java, CustomOrderMessageHolder::class.java)
+//        addMessageType(MessageTypingBean::class.java, null)
+//        addMessageType(EmptyMessageBean::class.java, EmptyMessageHolder::class.java, true)
+//        addMessageType(ChatbotMessageBean::class.java, ChatbotMessageHolder::class.java)
+//        addMessageType(
+//            ChatbotPlaceholderMessageBean::class.java,
+//            ChatbotPlaceholderMessageHolder::class.java
+//        )
+    }
+
+    override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
+        super.onAttachedToRecyclerView(recyclerView)
+        mRecycleView = recyclerView as MessageRecyclerView
+        recyclerView.setItemViewCacheSize(5)
+    }
+
+    override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
+        // TODO
+        if (holder is MessageBaseHolder) {
+            holder.setMessageBubbleBackground(null)
+            holder.onRecycled()
+        }
+    }
+
+    fun showLoading() {
+        if (mLoading) {
+            return
+        }
+        mLoading = true
+        notifyItemChanged(0)
+    }
+
+    private fun refreshLoadView() {
+        notifyItemChanged(0)
+    }
+
+    override fun onDataSourceChanged(dataSource: List<TUIMessageBean?>?) {
+        val itemList = mutableListOf<TUIMessageBean>()
+        // TODO: 添加一个官方消息
+        dataSource?.forEach { data ->
+            data ?: return@forEach
+            if (supportMessageTypes.contains(data.javaClass)) {
+                itemList.add(data)
+            } else {
+                itemList.add(UnSupportMessageBean(data))
+            }
+        }
+        items = itemList
+    }
+
+    override fun onViewNeedRefresh(
+        type: Int,
+        locateMessage: TUIMessageBean?
+    ) {
+        mLoading = false
+        refreshLoadView()
+        if (type == IMessageRecyclerView.DATA_CHANGE_LOCATE_TO_POSITION) {
+            notifyDataSetChanged()
+            val position: Int = getMessagePosition(locateMessage)
+            if (position == ITEM_POSITION_UNKNOWN) {
+                return
+            }
+            mRecycleView?.smoothScrollToPosition(position)
+            locateMessage?.id?.let { id ->
+                HighlightPresenter.startHighlight(id)
+            }
+        } else if (type == IMessageRecyclerView.SCROLL_TO_POSITION) {
+            val position: Int = getMessagePosition(locateMessage)
+            if (position == ITEM_POSITION_UNKNOWN) {
+                return
+            }
+            mRecycleView?.smoothScrollToPosition(position)
+            locateMessage?.id?.let { id ->
+                HighlightPresenter.startHighlight(id)
+            }
+            mRecycleView?.scrollMessageFinish()
+        } else if (type == IMessageRecyclerView.DATA_CHANGE_SCROLL_TO_POSITION) {
+            notifyDataSetChanged()
+            val position: Int = getMessagePosition(locateMessage)
+            if (position == ITEM_POSITION_UNKNOWN) {
+                return
+            }
+            mRecycleView?.scrollToEnd()
+            mRecycleView?.smoothScrollToPosition(position)
+            locateMessage?.id?.let { id ->
+                HighlightPresenter.startHighlight(id)
+            }
+            mRecycleView?.scrollMessageFinish()
+        } else if (type == IMessageRecyclerView.DATA_CHANGE_TYPE_UPDATE) {
+            val position: Int = getMessagePosition(locateMessage)
+            if (position == ITEM_POSITION_UNKNOWN) {
+                return
+            }
+            onItemChanged(position)
+        } else if (type == IMessageRecyclerView.DATA_CHANGE_SCROLL_TO_POSITION_WITHOUT_HIGH_LIGHT) {
+            notifyDataSetChanged()
+            val position: Int = getMessagePosition(locateMessage)
+            if (position == ITEM_POSITION_UNKNOWN) {
+                return
+            }
+            mRecycleView?.smoothScrollToPosition(position)
+        }
+    }
+
+    override fun onViewNeedRefresh(type: Int, value: Int) {
+        mLoading = false
+        if (type == IMessageRecyclerView.DATA_CHANGE_TYPE_REFRESH) {
+            notifyDataSetChanged()
+            mRecycleView?.scrollToEnd()
+        } else if (type == IMessageRecyclerView.DATA_CHANGE_TYPE_ADD_BACK) {
+            onItemInsert(items.size + 1, value)
+        } else if (type == IMessageRecyclerView.DATA_CHANGE_NEW_MESSAGE) {
+            onItemInsert(items.size + 1, value)
+            mRecycleView?.onMsgAddBack()
+        } else if (type == IMessageRecyclerView.DATA_CHANGE_TYPE_UPDATE) {
+            notifyDataSetChanged()
+        } else if (type == IMessageRecyclerView.DATA_CHANGE_TYPE_ADD_FRONT) {
+            // The number of loaded entries is 0, only the animation is updated
+            if (value != 0) {
+                // During the loading process, it is possible that the time interval between the first item before
+                // and the last item newly loaded is not more than 5 minutes, and the time entry needs to be removed,
+                // so the refresh here needs one more entry
+                onItemInsert(0, value)
+            }
+        } else if (type == IMessageRecyclerView.DATA_CHANGE_TYPE_LOAD) {
+            notifyDataSetChanged()
+            mRecycleView?.scrollToEnd()
+            mRecycleView?.loadMessageFinish()
+        } else if (type == IMessageRecyclerView.DATA_CHANGE_TYPE_DELETE) {
+            if (value == ITEM_POSITION_UNKNOWN) {
+                return
+            }
+            onItemRemove(value)
+        }
+        refreshLoadView()
+    }
+
+
+    private fun getMessagePosition(message: TUIMessageBean?): Int {
+        message ?: return ITEM_POSITION_UNKNOWN
+        var position = ITEM_POSITION_UNKNOWN
+        if (items.isEmpty()) {
+            return position
+        }
+
+        position = items.indexOfFirst { it == message }
+        if (position == -1) {
+            return ITEM_POSITION_UNKNOWN
+        }
+        return position
+    }
+
+    override fun getItem(position: Int): TUIMessageBean? {
+        return items.getOrNull(position) as? TUIMessageBean
+    }
+
+    override fun getFirstMessageBean(): TUIMessageBean? {
+        return items.firstOrNull() as? TUIMessageBean
+    }
+
+    override fun getLastMessageBean(): TUIMessageBean? {
+        return items.lastOrNull() as? TUIMessageBean
+    }
+
+    override fun onItemRefresh(messageBean: TUIMessageBean?) {
+        onViewNeedRefresh(IMessageRecyclerView.DATA_CHANGE_TYPE_UPDATE, messageBean)
+    }
+
+    private fun onItemChanged(position: Int) {
+        var start = position - 1
+        var end = position + 1
+        if (start < 0) {
+            start = 0
+        }
+        if (end > getItemCount()) {
+            end = position
+        }
+        val count = end - start
+        notifyItemRangeChanged(start, count)
+    }
+
+    private fun onItemInsert(start: Int, count: Int) {
+        notifyItemRangeInserted(start, count)
+        val startTemp = start - 2
+        val endTemp = start + count + 2
+        notifyItemRangeChanged(startTemp, endTemp - startTemp)
+    }
+
+    private fun onItemRemove(position: Int) {
+        val start = position - 1
+        val end = position + 1
+        if (start >= 0) {
+            notifyItemChanged(start)
+        }
+        if (end <= getItemCount()) {
+            notifyItemChanged(end)
+        }
+        notifyItemRemoved(position)
+    }
+
+    override fun getUserFaceUrlCache(): UserFaceUrlCache? {
+        // TODO:
+        return null
+    }
+}

+ 13 - 0
module/im/src/main/java/com/adealink/weparty/im/session/adapter/data/SessionData.kt

@@ -0,0 +1,13 @@
+package com.adealink.weparty.im.session.adapter.data
+
+import com.tencent.imsdk.v2.V2TIMMessage
+import com.tencent.qcloud.tuikit.timcommon.bean.TUIMessageBean
+
+
+data class UnSupportMessageBean(
+    val data: TUIMessageBean
+) : TUIMessageBean() {
+    override fun onProcessMessage(v2TIMMessage: V2TIMMessage?) {
+    }
+
+}

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

@@ -0,0 +1,91 @@
+package com.adealink.weparty.im.session.adapter.viewbinder
+
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.viewbinding.ViewBinding
+import com.adealink.frame.aab.util.getCompatDimension
+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.R
+import com.adealink.weparty.im.databinding.LayoutSessionMessageBaseBinding
+import com.tencent.qcloud.tuikit.timcommon.bean.TUIMessageBean
+
+
+open class MessageBinding<T : ViewBinding>(
+    binding: LayoutSessionMessageBaseBinding,
+    val messageBinding: T
+) : BindingViewHolder<LayoutSessionMessageBaseBinding>(binding)
+
+/**
+ * 对照: ImageMessageHolder
+ */
+abstract class BaseMessageViewBinder<T : TUIMessageBean, MB : ViewBinding>(
+
+) : ItemViewBinder<T, MessageBinding<MB>>() {
+
+    override fun onBindViewHolder(
+        holder: MessageBinding<MB>,
+        item: T
+    ) {
+        holder.binding.root.onClick {
+
+        }
+        if (item.isSelf) {
+            applySelfStyle(holder)
+            onBindSelfMessage(holder.messageBinding, item)
+        } else {
+            applyOtherStyle(holder)
+            onBindOtherMessage(holder.messageBinding, item)
+        }
+    }
+
+    private fun applySelfStyle(holder: MessageBinding<MB>) {
+        holder.binding.messageContent.gravity = Gravity.END
+        holder.messageBinding.root.setBackgroundResource(R.drawable.im_message_bubble_self_bg)
+        holder.messageBinding.root.setPaddingRelative(
+            getCompatDimension(R.dimen.im_message_bubble_self_padding_start).toInt(),
+            getCompatDimension(R.dimen.im_message_bubble_self_padding_top).toInt(),
+            getCompatDimension(R.dimen.im_message_bubble_self_padding_end).toInt(),
+            getCompatDimension(R.dimen.im_message_bubble_self_padding_bottom).toInt(),
+        )
+    }
+
+    private fun applyOtherStyle(holder: MessageBinding<MB>) {
+        holder.binding.messageContent.gravity = Gravity.START
+        holder.messageBinding.root.setBackgroundResource(R.drawable.im_message_bubble_other_bg)
+        holder.messageBinding.root.setPaddingRelative(
+            getCompatDimension(R.dimen.im_message_bubble_other_padding_start).toInt(),
+            getCompatDimension(R.dimen.im_message_bubble_other_padding_top).toInt(),
+            getCompatDimension(R.dimen.im_message_bubble_other_padding_end).toInt(),
+            getCompatDimension(R.dimen.im_message_bubble_other_padding_bottom).toInt(),
+        )
+    }
+
+    abstract fun onBindSelfMessage(binding: MB, item: T)
+
+    abstract fun onBindOtherMessage(binding: MB, item: T)
+
+    private fun setTimeTitle(
+        holder: BindingViewHolder<LayoutSessionMessageBaseBinding>,
+        item: T
+    ) {
+
+    }
+
+    override fun onCreateViewHolder(
+        inflater: LayoutInflater,
+        parent: ViewGroup
+    ): MessageBinding<MB> {
+        val binding = LayoutSessionMessageBaseBinding.inflate(inflater, parent, false)
+        val messageBinding = onCreateMessageContent(inflater, binding.messageContent)
+        return MessageBinding(binding, messageBinding)
+    }
+
+    abstract fun onCreateMessageContent(
+        inflater: LayoutInflater,
+        parent: ViewGroup
+    ): MB
+
+}

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

@@ -0,0 +1,52 @@
+package com.adealink.weparty.im.session.adapter.viewbinder
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import com.adealink.weparty.im.databinding.LayoutSessionMessageImageBinding
+import com.adealink.weparty.util.formatTimeTo
+import com.tencent.qcloud.tuikit.tuichat.bean.message.ImageMessageBean
+import com.tencent.qcloud.tuikit.tuichat.presenter.ChatFileDownloadPresenter
+
+/**
+ * 对照: ImageMessageHolder
+ */
+class ImageMessageViewBinder() :
+    BaseMessageViewBinder<ImageMessageBean, LayoutSessionMessageImageBinding>() {
+    override fun onBindSelfMessage(
+        binding: LayoutSessionMessageImageBinding,
+        item: ImageMessageBean
+    ) {
+        setImage(binding, item)
+        setMessageTime(binding, item)
+    }
+
+    override fun onBindOtherMessage(
+        binding: LayoutSessionMessageImageBinding,
+        item: ImageMessageBean
+    ) {
+        setImage(binding, item)
+        setMessageTime(binding, item)
+    }
+
+    private fun setImage(
+        binding: LayoutSessionMessageImageBinding,
+        item: ImageMessageBean
+    ) {
+        val imagePath = ChatFileDownloadPresenter.getImagePath(item)
+        binding.ivImg.setImageUrl(imagePath)
+    }
+
+    private fun setMessageTime(
+        binding: LayoutSessionMessageImageBinding,
+        item: ImageMessageBean
+    ) {
+        binding.vText.tvText.text = formatTimeTo((item.messageTime * 1000), "HH:mm")
+    }
+
+    override fun onCreateMessageContent(
+        inflater: LayoutInflater,
+        parent: ViewGroup
+    ): LayoutSessionMessageImageBinding {
+        return LayoutSessionMessageImageBinding.inflate(inflater, parent, true)
+    }
+}

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

@@ -0,0 +1,60 @@
+package com.adealink.weparty.im.session.adapter.viewbinder
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import com.adealink.weparty.im.databinding.LayoutSessionMessageSoundBinding
+import com.adealink.weparty.util.formatTimeTo
+import com.tencent.qcloud.tuikit.tuichat.bean.message.SoundMessageBean
+import java.util.Timer
+
+/**
+ * 对照: SoundMessageHolder
+ */
+class SoundMessageViewBinder() :
+    BaseMessageViewBinder<SoundMessageBean, LayoutSessionMessageSoundBinding>() {
+
+    private var mTimer: Timer? = null
+
+    override fun onBindSelfMessage(
+        binding: LayoutSessionMessageSoundBinding,
+        item: SoundMessageBean
+    ) {
+        setSound(binding, item)
+        setMessageTime(binding, item)
+    }
+
+    override fun onBindOtherMessage(
+        binding: LayoutSessionMessageSoundBinding,
+        item: SoundMessageBean
+    ) {
+        setSound(binding, item)
+        setMessageTime(binding, item)
+    }
+
+    private fun setSound(
+        binding: LayoutSessionMessageSoundBinding,
+        message: SoundMessageBean
+    ) {
+        val duration = message.getDuration()
+        binding.tvDuration.text = duration.toString()
+    }
+
+    private fun setMessageTime(
+        binding: LayoutSessionMessageSoundBinding,
+        item: SoundMessageBean
+    ) {
+        binding.tvTime.text = formatTimeTo((item.messageTime * 1000), "HH:mm")
+    }
+
+    override fun onCreateMessageContent(
+        inflater: LayoutInflater,
+        parent: ViewGroup
+    ): LayoutSessionMessageSoundBinding {
+        return LayoutSessionMessageSoundBinding.inflate(inflater, parent, true)
+    }
+
+    override fun onViewRecycled(holder: MessageBinding<LayoutSessionMessageSoundBinding>) {
+        super.onViewRecycled(holder)
+    }
+
+}

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

@@ -0,0 +1,74 @@
+package com.adealink.weparty.im.session.adapter.viewbinder
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import com.adealink.frame.aab.util.getCompatString
+import com.adealink.weparty.im.R
+import com.adealink.weparty.im.databinding.LayoutSessionMessageTextBinding
+import com.adealink.weparty.util.formatTimeTo
+import com.tencent.qcloud.tuikit.timcommon.component.face.FaceManager
+import com.tencent.qcloud.tuikit.timcommon.util.TextUtil
+import com.tencent.qcloud.tuikit.tuichat.bean.message.TextMessageBean
+
+/**
+ * 对照: TextMessageHolder
+ */
+class TextMessageViewBinder() :
+    BaseMessageViewBinder<TextMessageBean, LayoutSessionMessageTextBinding>() {
+    override fun onBindSelfMessage(
+        binding: LayoutSessionMessageTextBinding,
+        item: TextMessageBean
+    ) {
+        setText(binding, item)
+        setMessageTime(binding, item)
+    }
+
+    override fun onBindOtherMessage(
+        binding: LayoutSessionMessageTextBinding,
+        item: TextMessageBean
+    ) {
+        setText(binding, item)
+        setMessageTime(binding, item)
+    }
+
+    private fun setText(
+        binding: LayoutSessionMessageTextBinding,
+        item: TextMessageBean
+    ) {
+        if (item.text != null) {
+            FaceManager.handlerEmojiText(
+                binding.tvText,
+                item.text,
+                false
+            )
+        } else if (!item.extra.isNullOrEmpty()) {
+            FaceManager.handlerEmojiText(
+                binding.tvText,
+                item.extra,
+                false
+            )
+        } else {
+            FaceManager.handlerEmojiText(
+                binding.tvText,
+                getCompatString(R.string.im_no_suport_message),
+                false
+            )
+        }
+        TextUtil.linkifyUrls(binding.tvText)
+    }
+
+    private fun setMessageTime(
+        binding: LayoutSessionMessageTextBinding,
+        item: TextMessageBean
+    ) {
+        binding.tvTime.text = formatTimeTo((item.messageTime * 1000), "HH:mm")
+    }
+
+    override fun onCreateMessageContent(
+        inflater: LayoutInflater,
+        parent: ViewGroup
+    ): LayoutSessionMessageTextBinding {
+        return LayoutSessionMessageTextBinding.inflate(inflater, parent, true)
+    }
+
+}

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

@@ -0,0 +1,91 @@
+package com.adealink.weparty.im.session.adapter.viewbinder
+
+import android.text.SpannableStringBuilder
+import android.text.Spanned
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import com.adealink.weparty.im.databinding.LayoutSessionMessageTipsBinding
+import com.adealink.weparty.util.formatTimeTo
+import com.tencent.qcloud.tuikit.timcommon.R
+import com.tencent.qcloud.tuikit.timcommon.TIMCommonService
+import com.tencent.qcloud.tuikit.timcommon.util.TextUtil.ForegroundColorClickableSpan
+import com.tencent.qcloud.tuikit.tuichat.bean.message.TipsMessageBean
+
+/**
+ * 对照: TipsMessageHolder
+ */
+class TipsMessageViewBinder() :
+    BaseMessageViewBinder<TipsMessageBean, LayoutSessionMessageTipsBinding>() {
+    override fun onBindSelfMessage(
+        binding: LayoutSessionMessageTipsBinding,
+        item: TipsMessageBean
+    ) {
+        setTips(binding, item)
+        setMessageTime(binding, item)
+    }
+
+    override fun onBindOtherMessage(
+        binding: LayoutSessionMessageTipsBinding,
+        item: TipsMessageBean
+    ) {
+        setTips(binding, item)
+        setMessageTime(binding, item)
+    }
+
+    private fun setTips(
+        binding: LayoutSessionMessageTipsBinding,
+        msg: TipsMessageBean
+    ) {
+        val targetUserNameMap = msg.targetUserMap
+        val operationUserPair = msg.operationUserPair
+        val text = msg.text
+        val builder = SpannableStringBuilder(msg.text)
+        val color = TIMCommonService.getAppContext().getResources()
+            .getColor(R.color.common_quote_user_name_color)
+        if (!targetUserNameMap.isEmpty()) {
+            for (entry in targetUserNameMap.entries) {
+                val start = text.indexOf(entry.value!!)
+                if (start != -1) {
+                    val end = start + entry.value!!.length
+                    val span = ForegroundColorClickableSpan(color, object : View.OnClickListener {
+                        override fun onClick(v: View?) {
+                            // TODO:
+//                            onUserClick(entry.key)
+                        }
+                    })
+                    builder.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+                }
+            }
+        }
+        if (operationUserPair != null) {
+            val start = text.indexOf(operationUserPair.second!!)
+            if (start != -1) {
+                val end = start + operationUserPair.second!!.length
+                val span = ForegroundColorClickableSpan(color, object : View.OnClickListener {
+                    override fun onClick(v: View?) {
+                        // TODO:
+//                        onUserClick(operationUserPair.first)
+                    }
+                })
+                builder.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+            }
+        }
+        binding.tvText.text = builder
+    }
+
+    private fun setMessageTime(
+        binding: LayoutSessionMessageTipsBinding,
+        item: TipsMessageBean
+    ) {
+        binding.tvTime.text = formatTimeTo((item.messageTime * 1000), "HH:mm")
+    }
+
+    override fun onCreateMessageContent(
+        inflater: LayoutInflater,
+        parent: ViewGroup
+    ): LayoutSessionMessageTipsBinding {
+        return LayoutSessionMessageTipsBinding.inflate(inflater, parent, true)
+    }
+
+}

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

@@ -0,0 +1,43 @@
+package com.adealink.weparty.im.session.adapter.viewbinder
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import com.adealink.weparty.im.databinding.LayoutSessionMessageUnsupportBinding
+import com.adealink.weparty.im.session.adapter.data.UnSupportMessageBean
+import com.adealink.weparty.util.formatTimeTo
+
+/**
+ * 对照: UnSupportMessageBean
+ */
+class UnSupportMessageViewBinder() :
+    BaseMessageViewBinder<UnSupportMessageBean, LayoutSessionMessageUnsupportBinding>() {
+    override fun onBindSelfMessage(
+        binding: LayoutSessionMessageUnsupportBinding,
+        item: UnSupportMessageBean
+    ) {
+        setMessageTime(binding, item)
+    }
+
+    override fun onBindOtherMessage(
+        binding: LayoutSessionMessageUnsupportBinding,
+        item: UnSupportMessageBean
+    ) {
+        setMessageTime(binding, item)
+    }
+
+    private fun setMessageTime(
+        binding: LayoutSessionMessageUnsupportBinding,
+        item: UnSupportMessageBean
+    ) {
+        binding.tvTime.text = formatTimeTo((item.data.messageTime * 1000), "HH:mm")
+    }
+
+
+    override fun onCreateMessageContent(
+        inflater: LayoutInflater,
+        parent: ViewGroup
+    ): LayoutSessionMessageUnsupportBinding {
+        return LayoutSessionMessageUnsupportBinding.inflate(inflater, parent, true)
+    }
+
+}

+ 57 - 0
module/im/src/main/java/com/adealink/weparty/im/session/comp/SessionBottomComp.kt

@@ -0,0 +1,57 @@
+package com.adealink.weparty.im.session.comp
+
+import androidx.lifecycle.LifecycleOwner
+import com.adealink.frame.mvvm.view.ViewComponent
+import com.adealink.frame.mvvm.viewmodel.viewModels
+import com.adealink.weparty.commonui.ext.gone
+import com.adealink.weparty.commonui.ext.show
+import com.adealink.weparty.im.databinding.LayoutSessionBottomBarBinding
+import com.adealink.weparty.im.session.comp.viewmodel.SessionBottomStyle
+import com.adealink.weparty.im.session.comp.viewmodel.SessionBottomViewModel
+import com.tencent.qcloud.tuikit.tuichat.bean.C2CChatInfo
+import com.tencent.qcloud.tuikit.tuichat.presenter.C2CChatPresenter
+
+class SessionBottomComp(
+    lifecycleOwner: LifecycleOwner,
+    val bottomBar: LayoutSessionBottomBarBinding,
+    val chatInfo: C2CChatInfo? = null,
+    val presenter: C2CChatPresenter? = null
+) : ViewComponent(lifecycleOwner) {
+
+    private val bottomBarViewModel by viewModels<SessionBottomViewModel>()
+
+    override fun onCreate() {
+        super.onCreate()
+        initView()
+        initComponent()
+        observeViewModel()
+    }
+
+    private fun initView() {
+        bottomBar.vVoiceBar.root.gone()
+        bottomBar.vInputBar.root.show()
+        bottomBarViewModel.changeStyle(SessionBottomStyle.INPUT)
+    }
+
+    private fun initComponent() {
+        SessionBottomInputComp(lifecycleOwner, bottomBar.vInputBar, chatInfo, presenter).attach()
+        SessionBottomVoiceComp(lifecycleOwner, bottomBar.vVoiceBar, chatInfo, presenter).attach()
+    }
+
+    private fun observeViewModel() {
+        bottomBarViewModel.styleLD.observe(viewLifecycleOwner) { style ->
+            when (style) {
+                SessionBottomStyle.INPUT -> {
+                    bottomBar.vVoiceBar.root.gone()
+                    bottomBar.vInputBar.root.show()
+                }
+
+                SessionBottomStyle.VOICE -> {
+                    bottomBar.vVoiceBar.root.show()
+                    bottomBar.vInputBar.root.gone()
+                }
+            }
+        }
+    }
+
+}

+ 105 - 0
module/im/src/main/java/com/adealink/weparty/im/session/comp/SessionBottomInputComp.kt

@@ -0,0 +1,105 @@
+package com.adealink.weparty.im.session.comp
+
+import androidx.lifecycle.LifecycleOwner
+import com.adealink.frame.data.json.froJsonErrorNull
+import com.adealink.frame.log.Log
+import com.adealink.frame.mvvm.view.ViewComponent
+import com.adealink.frame.mvvm.viewmodel.viewModels
+import com.adealink.frame.util.onClick
+import com.adealink.weparty.im.databinding.LayoutSessionBottomInputBarBinding
+import com.adealink.weparty.im.session.comp.viewmodel.SessionBottomStyle
+import com.adealink.weparty.im.session.comp.viewmodel.SessionBottomViewModel
+import com.adealink.weparty.module.im.data.TAG_IM_SESSION
+import com.tencent.qcloud.tuikit.tuichat.bean.C2CChatInfo
+import com.tencent.qcloud.tuikit.tuichat.presenter.C2CChatPresenter
+
+class SessionBottomInputComp(
+    lifecycleOwner: LifecycleOwner,
+    val inputBar: LayoutSessionBottomInputBarBinding,
+    val chatInfo: C2CChatInfo? = null,
+    val presenter: C2CChatPresenter? = null
+) : ViewComponent(lifecycleOwner) {
+
+    private val bottomBarViewModel by viewModels<SessionBottomViewModel>()
+    override fun onCreate() {
+        super.onCreate()
+        initView()
+        observeViewModel()
+    }
+
+    override fun onPause() {
+        super.onPause()
+        saveDraft()
+    }
+
+    private fun initView() {
+        inputBar.btnEmoji.onClick {
+            clickEmoji()
+        }
+        inputBar.btnPhoto.onClick {
+            clickPhoto()
+        }
+        inputBar.btnVoice.onClick {
+            clickVoice()
+        }
+        initInputText()
+    }
+
+    private fun initInputText() {
+        var inputContent: String? = null
+
+        val draftInfo = chatInfo?.getDraft()
+        if (draftInfo != null && !draftInfo.draftText.isNullOrEmpty()) {
+            try {
+                val draftJsonMap = froJsonErrorNull<HashMap<*, *>>(draftInfo.draftText)
+                if (draftJsonMap != null) {
+                    inputContent = draftJsonMap["content"] as String?
+//                    val replayStr = draftJsonMap["reply"] as String?
+//                    val replayPreview = froJsonErrorNull(replayStr, ReplyPreviewBean::class.java)
+//                    if (replayPreview != null) {
+//                        showReplyPreview(replayPreview)
+//                    }
+                }
+            } catch (e: Exception) {
+                Log.e(TAG_IM_SESSION, " getCustomJsonMap error:${e.message}", e)
+            }
+        }
+        inputBar.etInputMessage.setText(inputContent)
+        inputBar.etInputMessage.setSelection(inputBar.etInputMessage.getText()?.length ?: 0)
+    }
+
+    fun saveDraft() {
+        if (chatInfo == null) {
+            Log.e(TAG_IM_SESSION, "set drafts error :  chatInfo is null")
+            return
+        }
+
+        var draftText = inputBar.etInputMessage.getText().toString()
+//        if ((isQuoteModel || isReplyModel) && replyPreviewBean != null) {
+//            val gson = Gson()
+//            val draftMap: MutableMap<String?, String?> = java.util.HashMap<String?, String?>()
+//            draftMap.put("content", draftText)
+//            draftMap.put("reply", gson.toJson(replyPreviewBean))
+//            draftText = gson.toJson(draftMap)
+//        }
+        presenter?.setDraft(draftText)
+
+    }
+
+    private fun observeViewModel() {
+
+    }
+
+    private fun clickEmoji() {
+
+    }
+
+    private fun clickPhoto() {
+
+    }
+
+    private fun clickVoice() {
+        bottomBarViewModel.changeStyle(SessionBottomStyle.VOICE)
+    }
+
+}

+ 63 - 0
module/im/src/main/java/com/adealink/weparty/im/session/comp/SessionBottomVoiceComp.kt

@@ -0,0 +1,63 @@
+package com.adealink.weparty.im.session.comp
+
+import androidx.lifecycle.LifecycleOwner
+import com.adealink.frame.mvvm.view.ViewComponent
+import com.adealink.frame.mvvm.viewmodel.viewModels
+import com.adealink.frame.util.onClick
+import com.adealink.weparty.im.R
+import com.adealink.weparty.im.databinding.LayoutSessionBottomVoiceBarBinding
+import com.adealink.weparty.im.session.comp.viewmodel.SessionBottomStyle
+import com.adealink.weparty.im.session.comp.viewmodel.SessionBottomViewModel
+import com.tencent.qcloud.tuikit.tuichat.bean.C2CChatInfo
+import com.tencent.qcloud.tuikit.tuichat.presenter.C2CChatPresenter
+
+class SessionBottomVoiceComp(
+    lifecycleOwner: LifecycleOwner,
+    val voiceBar: LayoutSessionBottomVoiceBarBinding,
+    val chatInfo: C2CChatInfo? = null,
+    val presenter: C2CChatPresenter? = null
+) : ViewComponent(lifecycleOwner) {
+
+    private val bottomBarViewModel by viewModels<SessionBottomViewModel>()
+
+    private var isPause = false
+    override fun onCreate() {
+        super.onCreate()
+        initView()
+        observeViewModel()
+    }
+
+
+    private fun initView() {
+        voiceBar.btnRemove.onClick {
+            clickDelete()
+        }
+        voiceBar.btnPause.onClick {
+            clickPause()
+        }
+        voiceBar.btnSend.onClick {
+            clickSend()
+        }
+    }
+
+    private fun observeViewModel() {
+
+    }
+
+    private fun clickDelete() {
+        bottomBarViewModel.changeStyle(SessionBottomStyle.INPUT)
+    }
+
+    private fun clickPause() {
+        isPause = !isPause
+        if (isPause) {
+            voiceBar.btnPause.setImageResource(R.drawable.im_session_voice_record_resume_ic)
+        } else {
+            voiceBar.btnPause.setImageResource(R.drawable.im_session_voice_record_pause_ic)
+        }
+    }
+
+    private fun clickSend() {
+        bottomBarViewModel.changeStyle(SessionBottomStyle.INPUT)
+    }
+}

+ 29 - 0
module/im/src/main/java/com/adealink/weparty/im/session/comp/SessionTopComp.kt

@@ -0,0 +1,29 @@
+package com.adealink.weparty.im.session.comp
+
+import androidx.lifecycle.LifecycleOwner
+import com.adealink.frame.mvvm.view.ViewComponent
+import com.adealink.frame.util.onClick
+import com.adealink.weparty.im.databinding.LayoutSessionTopBarBinding
+
+class SessionTopComp(
+    lifecycleOwner: LifecycleOwner,
+    val topBar: LayoutSessionTopBarBinding
+) : ViewComponent(lifecycleOwner) {
+
+    override fun onCreate() {
+        super.onCreate()
+        initView()
+        observeViewModel()
+    }
+
+    private fun initView() {
+        topBar.ivBack.onClick {
+            activity?.finish()
+        }
+    }
+
+    private fun observeViewModel() {
+
+    }
+
+}

+ 19 - 0
module/im/src/main/java/com/adealink/weparty/im/session/comp/viewmodel/SessionBottomViewModel.kt

@@ -0,0 +1,19 @@
+package com.adealink.weparty.im.session.comp.viewmodel
+
+import androidx.lifecycle.MutableLiveData
+import com.adealink.frame.mvvm.viewmodel.BaseViewModel
+
+enum class SessionBottomStyle {
+    INPUT,
+    VOICE
+}
+
+class SessionBottomViewModel : BaseViewModel() {
+
+    val styleLD = MutableLiveData<SessionBottomStyle>()
+
+    fun changeStyle(style: SessionBottomStyle) {
+        styleLD.send(style)
+    }
+
+}

+ 14 - 0
module/im/src/main/java/com/adealink/weparty/im/session/viewmodel/SessionViewModel.kt

@@ -0,0 +1,14 @@
+package com.adealink.weparty.im.session.viewmodel
+
+import androidx.lifecycle.MutableLiveData
+import com.adealink.frame.mvvm.viewmodel.BaseViewModel
+import com.adealink.weparty.module.profile.data.UserInfo
+
+class SessionViewModel : BaseViewModel() {
+
+    val senderLD = MutableLiveData<UserInfo>()
+
+    fun setSession(chatID: String) {
+
+    }
+}

+ 255 - 0
module/im/src/main/java/com/adealink/weparty/im/session/widget/MessageRecyclerView.kt

@@ -0,0 +1,255 @@
+package com.adealink.weparty.im.session.widget
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.GestureDetector
+import android.view.GestureDetector.SimpleOnGestureListener
+import android.view.MotionEvent
+import android.view.View
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.SimpleItemAnimator
+import com.adealink.frame.log.Log
+import com.adealink.frame.util.isMainThread
+import com.adealink.frame.util.runOnUiThread
+import com.adealink.weparty.im.session.adapter.SessionAdapter
+import com.tencent.qcloud.tuikit.timcommon.bean.TUIMessageBean
+import com.tencent.qcloud.tuikit.timcommon.component.CustomLinearLayoutManager
+import com.tencent.qcloud.tuikit.timcommon.component.scroller.CenteredSmoothScroller
+import com.tencent.qcloud.tuikit.tuichat.TUIChatService
+import com.tencent.qcloud.tuikit.tuichat.bean.ChatInfo
+import com.tencent.qcloud.tuikit.tuichat.interfaces.IMessageRecyclerView
+import com.tencent.qcloud.tuikit.tuichat.interfaces.OnEmptySpaceClickListener
+import com.tencent.qcloud.tuikit.tuichat.interfaces.OnGestureScrollListener
+import com.tencent.qcloud.tuikit.tuichat.presenter.ChatPresenter
+
+class MessageRecyclerView @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyle: Int = 0
+) : RecyclerView(context, attrs, defStyle), IMessageRecyclerView {
+
+    private var chatDelegate: ChatDelegate? = null
+    var emptySpaceClickListener: OnEmptySpaceClickListener? = null
+    private var onGestureScrollListener: OnGestureScrollListener? = null
+    private var mAdapter: SessionAdapter? = null
+    private var linearLayoutManager: LinearLayoutManager? = null
+    private var presenter: ChatPresenter? = null
+
+    init {
+        initView(context, attrs)
+    }
+
+    private fun initView(context: Context, attrs: AttributeSet? = null) {
+        if (isInEditMode) {
+            return
+        }
+        Log.d(TAG, "init()")
+        isLayoutFrozen = false
+        setItemViewCacheSize(0)
+        setHasFixedSize(true)
+        setFocusableInTouchMode(false)
+        setFocusable(true)
+        isClickable = true
+        linearLayoutManager =
+            CustomLinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
+        setLayoutManager(linearLayoutManager)
+        (itemAnimator as SimpleItemAnimator?)?.supportsChangeAnimations = false
+        setItemAnimator(null)
+        setClickEmptySpaceEvent()
+        addOnLayoutChangeListener(object : OnLayoutChangeListener {
+            override fun onLayoutChange(
+                v: View?,
+                left: Int,
+                top: Int,
+                right: Int,
+                bottom: Int,
+                oldLeft: Int,
+                oldTop: Int,
+                oldRight: Int,
+                oldBottom: Int
+            ) {
+                // When the view above the message list expands, scroll down the corresponding distance.
+                val oldHeight = oldBottom - oldTop
+                val newHeight = bottom - top
+                if (oldHeight != 0 && oldHeight > newHeight && oldTop < top) {
+                    scrollBy(0, oldHeight - newHeight)
+                }
+            }
+        })
+    }
+
+    fun setPresenter(presenter: ChatPresenter?) {
+        this.presenter = presenter
+    }
+
+    override fun setAdapter(adapter: Adapter<*>?) {
+        super.setAdapter(adapter)
+        mAdapter = adapter as? SessionAdapter
+    }
+
+    private fun setClickEmptySpaceEvent() {
+        val gestureListener: GestureDetector.OnGestureListener =
+            object : SimpleOnGestureListener() {
+
+                override fun onSingleTapUp(e: MotionEvent): Boolean {
+                    return emptySpaceClickListener?.let {
+                        it.onClick()
+                        true
+                    } ?: false
+                }
+
+                override fun onDown(e: MotionEvent): Boolean {
+                    return emptySpaceClickListener?.let {
+                        it.onClick()
+                        true
+                    } ?: false
+                }
+
+                override fun onScroll(
+                    e1: MotionEvent?,
+                    e2: MotionEvent,
+                    distanceX: Float,
+                    distanceY: Float
+                ): Boolean {
+                    onGestureScrollListener?.onScroll(e1, e2, distanceX, distanceY)
+                    return super.onScroll(e1, e2, distanceX, distanceY)
+                }
+            }
+
+        val gestureDetector = GestureDetector(context, gestureListener)
+        setOnTouchListener(object : OnTouchListener {
+            override fun onTouch(v: View?, event: MotionEvent): Boolean {
+                if (v is RecyclerView) {
+                    gestureDetector.onTouchEvent(event)
+                }
+                return false
+            }
+        })
+    }
+
+    // Always return last visible item for laying out other items from tail to head.
+    override fun getFocusedChild(): View? {
+        if (linearLayoutManager != null) {
+            val position = linearLayoutManager!!.findLastVisibleItemPosition()
+            return linearLayoutManager!!.findViewByPosition(position)
+        }
+        return super.getFocusedChild()
+    }
+
+    fun onMsgAddBack() {
+        if (mAdapter != null) {
+            if (this.isLastItemVisibleCompleted) {
+                scrollToEnd()
+            }
+        }
+    }
+
+    override fun isDisplayJumpMessageLayout(): Boolean {
+        Log.d(
+            TAG,
+            ("computeVerticalScrollRange() = " + computeVerticalScrollRange() + ", computeVerticalScrollExtent() = " + computeVerticalScrollExtent()
+                    + ", computeVerticalScrollOffset() = " + computeVerticalScrollOffset())
+        )
+        val toBottom =
+            computeVerticalScrollRange() - computeVerticalScrollExtent() - computeVerticalScrollOffset()
+        Log.d(TAG, "toBottom = " + toBottom)
+        if (toBottom > 0 && toBottom >= 2 * computeVerticalScrollExtent()) {
+            return true
+        } else {
+            return false
+        }
+    }
+
+    val isLastItemVisibleCompleted: Boolean
+        get() {
+            val linearLayoutManager =
+                getLayoutManager() as LinearLayoutManager?
+            if (linearLayoutManager == null) {
+                return false
+            }
+            val lastPosition =
+                linearLayoutManager.findLastCompletelyVisibleItemPosition()
+            val childCount = linearLayoutManager.getChildCount()
+            val firstPosition = linearLayoutManager.findFirstVisibleItemPosition()
+            if (lastPosition >= firstPosition + childCount - 1) {
+                return true
+            }
+            return false
+        }
+
+    private fun isDefaultMessage(messageBean: TUIMessageBean): Boolean {
+        val extensionMessageClassSet = TUIChatService.getInstance().getExtensionMessageClassSet()
+        return !extensionMessageClassSet.contains(messageBean.javaClass)
+    }
+
+    override fun displayBackToNewMessage(display: Boolean, messageId: String?, count: Int) {
+        if (chatDelegate != null) {
+            chatDelegate!!.displayBackToNewMessage(display, messageId, count)
+        }
+    }
+
+    override fun scrollToEnd() {
+        if (!isMainThread()) {
+            runOnUiThread {
+                scrollToEnd()
+            }
+            return
+        }
+
+        adapter?.let { adapter ->
+            val layoutManager = getLayoutManager()
+            val itemCount = adapter.itemCount
+            if (layoutManager is LinearLayoutManager && itemCount > 0) {
+                layoutManager.scrollToPositionWithOffset(itemCount - 1, SCROLL_TO_END_OFFSET)
+            }
+        }
+    }
+
+    override fun smoothScrollToPosition(position: Int) {
+        val adapter = adapter ?: return
+        if (position < adapter.itemCount) {
+            val smoothScroller = CenteredSmoothScroller(context)
+            smoothScroller.targetPosition = position
+            layoutManager?.startSmoothScroll(smoothScroller)
+        }
+    }
+
+    fun setChatDelegate(chatDelegate: ChatDelegate?) {
+        this.chatDelegate = chatDelegate
+    }
+
+    fun setOnGestureScrollListener(onGestureScrollListener: OnGestureScrollListener?) {
+        this.onGestureScrollListener = onGestureScrollListener
+    }
+
+    fun loadMessageFinish() {
+        chatDelegate?.loadMessageFinish()
+    }
+
+    fun scrollMessageFinish() {
+        chatDelegate?.scrollMessageFinish()
+    }
+
+    private val chatInfo: ChatInfo?
+        get() = presenter?.chatInfo
+
+    interface ChatDelegate {
+        fun displayBackToLastMessage(display: Boolean)
+
+        fun displayBackToNewMessage(display: Boolean, messageId: String?, count: Int)
+
+        fun loadMessageFinish()
+
+        fun scrollMessageFinish()
+
+        fun hideSoftInput()
+    }
+
+    companion object {
+        private val TAG: String = MessageRecyclerView::class.java.getSimpleName()
+
+        // Take a large enough offset to scroll to the bottom at one time
+        private const val SCROLL_TO_END_OFFSET = -999999
+    }
+}

+ 44 - 0
module/im/src/main/java/com/adealink/weparty/im/util/IMUIUtil.kt

@@ -0,0 +1,44 @@
+package com.adealink.weparty.im.util
+
+import com.adealink.frame.aab.util.getCompatString
+import com.adealink.weparty.im.R
+import com.adealink.weparty.util.formatTimeTo
+import com.adealink.weparty.util.isSameDay
+import com.adealink.weparty.util.isSameYearApprox
+import java.util.Date
+import kotlin.math.abs
+
+
+private const val JUST_NOW = 900_000 //15x60x1000 //15分钟内
+private const val JUST_ONE_DAY = 86400_000 //24x60x60x1000 //24小时
+
+/**
+ * IM时间格式化
+ *
+ * ≤15min,现在
+ * >15min且小于24h,并且是今天内,显示实际的时分,如10:21, (格式:HH:mm)
+ * >15min且小于24h,并且不是今天内,显示日期,如28/11 (格式:dd/MM)
+ * ≥24h,显示日期,28/11 (格式:dd/MM)
+ * 跨年,显示日月年,28/11/2025 (格式:dd/MM/yyyy)
+ */
+fun Long.formatSessionTime(): String {
+    val now = Date()
+    val ts = Date(this)
+    val tsOffset = abs(now.time - ts.time)
+    return if (tsOffset <= JUST_NOW) {
+        //≤15min,现在
+        getCompatString(R.string.im_time_just_now)
+    } else if (tsOffset <= JUST_ONE_DAY && isSameDay(now.time, ts.time)) {
+        //>15min且小于24h,并且是今天内,显示实际的时分,如10:21, (格式:HH:mm)
+        formatTimeTo(this, "HH:mm")
+    } else if (tsOffset <= JUST_ONE_DAY) {
+        //>15min且小于24h,并且不是今天内,显示日期,如28/11 (格式:dd/MM)
+        formatTimeTo(this, "dd/MM")
+    } else if (isSameYearApprox(now.time, ts.time)) {
+        //≥24h,显示日期,28/11 (格式:dd/MM)
+        formatTimeTo(this, "dd/MM")
+    } else {
+        //跨年,显示日月年,28/11/2025 (格式:dd/MM/yyyy)
+        formatTimeTo(this, "dd/MM/yyyy")
+    }
+}

+ 4 - 1
module/im/src/main/java/com/adealink/weparty/im/viewmodel/IMViewModelFactory.kt

@@ -2,6 +2,8 @@ package com.adealink.weparty.im.viewmodel
 
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.ViewModelProvider
+import com.adealink.weparty.im.list.viewmodel.SessionListViewModel
+import com.adealink.weparty.im.session.viewmodel.SessionViewModel
 
 @Suppress("UNCHECKED_CAST")
 class IMViewModelFactory : ViewModelProvider.NewInstanceFactory() {
@@ -12,7 +14,8 @@ class IMViewModelFactory : ViewModelProvider.NewInstanceFactory() {
                 isAssignableFrom(SessionListViewModel::class.java) ->
                     SessionListViewModel()
 
-
+                isAssignableFrom(SessionViewModel::class.java) ->
+                    SessionViewModel()
                 else ->
                     throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}")
             } as T

+ 0 - 11
module/im/src/main/java/com/adealink/weparty/im/viewmodel/SessionListViewModel.kt

@@ -1,11 +0,0 @@
-package com.adealink.weparty.im.viewmodel
-
-import com.adealink.frame.mvvm.viewmodel.BaseViewModel
-
-class SessionListViewModel: BaseViewModel() {
-
-    fun clearMessage(){
-
-    }
-
-}

BIN
module/im/src/main/res/drawable-xhdpi/im_bubble_me_arrow_ic.png


BIN
module/im/src/main/res/drawable-xhdpi/im_bubble_other_arrow_ic.png


BIN
module/im/src/main/res/drawable-xhdpi/im_session_call_ic.png


BIN
module/im/src/main/res/drawable-xhdpi/im_session_emoji_ic.png


BIN
module/im/src/main/res/drawable-xhdpi/im_session_follow_ic.png


BIN
module/im/src/main/res/drawable-xhdpi/im_session_message_read_ic.png


BIN
module/im/src/main/res/drawable-xhdpi/im_session_message_unread_ic.png


BIN
module/im/src/main/res/drawable-xhdpi/im_session_more_ic.png


BIN
module/im/src/main/res/drawable-xhdpi/im_session_photo_ic.png


BIN
module/im/src/main/res/drawable-xhdpi/im_session_sound_play_ic.png


BIN
module/im/src/main/res/drawable-xhdpi/im_session_sound_stop_ic.png


BIN
module/im/src/main/res/drawable-xhdpi/im_session_voice_ic.png


BIN
module/im/src/main/res/drawable-xhdpi/im_session_voice_record_delete_ic.png


BIN
module/im/src/main/res/drawable-xhdpi/im_session_voice_record_pause_ic.png


BIN
module/im/src/main/res/drawable-xhdpi/im_session_voice_record_resume_ic.png


BIN
module/im/src/main/res/drawable-xhdpi/im_session_voice_record_send_ic.png


+ 21 - 0
module/im/src/main/res/drawable/im_message_bubble_other_bg.xml

@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item
+        android:width="8dp"
+        android:height="12.5dp"
+        android:gravity="left|top">
+        <bitmap android:src="@drawable/im_bubble_other_arrow_ic" />
+    </item>
+    <item
+        android:gravity="start|top"
+        android:left="8dp">
+        <shape android:shape="rectangle">
+            <solid android:color="@color/white" />
+            <corners
+                android:bottomLeftRadius="12dp"
+                android:bottomRightRadius="12dp"
+                android:topLeftRadius="0dp"
+                android:topRightRadius="12dp" />
+        </shape>
+    </item>
+</layer-list>

+ 21 - 0
module/im/src/main/res/drawable/im_message_bubble_self_bg.xml

@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item
+        android:width="8dp"
+        android:height="12.5dp"
+        android:gravity="right|top">
+        <bitmap android:src="@drawable/im_bubble_me_arrow_ic" />
+    </item>
+    <item
+        android:gravity="right|top"
+        android:right="8dp">
+        <shape android:shape="rectangle">
+            <solid android:color="@color/color_FFE6FFFA" />
+            <corners
+                android:bottomLeftRadius="12dp"
+                android:bottomRightRadius="12dp"
+                android:topLeftRadius="12dp"
+                android:topRightRadius="0dp" />
+        </shape>
+    </item>
+</layer-list>

+ 6 - 0
module/im/src/main/res/drawable/im_message_time_bg.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="@color/white" />
+    <corners android:radius="100dp" />
+</shape>

+ 8 - 0
module/im/src/main/res/drawable/im_session_input_bg.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+
+    <solid android:color="@color/color_FFF2F3F5" />
+    <corners android:radius="8dp" />
+
+</shape>

+ 25 - 0
module/im/src/main/res/layout/activity_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_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>

+ 26 - 0
module/im/src/main/res/layout/fragment_session.xml

@@ -0,0 +1,26 @@
+<?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_toTopOf="@id/bottom_bar"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <include
+        android:id="@+id/bottom_bar"
+        layout="@layout/layout_session_bottom_bar"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 1 - 0
module/im/src/main/res/layout/fragment_session_home_list.xml

@@ -71,6 +71,7 @@
         android:id="@+id/fl_content"
         android:layout_width="0dp"
         android:layout_height="0dp"
+        android:layout_marginTop="14dp"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent"

+ 1 - 1
module/im/src/main/res/layout/fragment_session_list.xml

@@ -18,7 +18,7 @@
         app:layout_constraintTop_toTopOf="parent"
         tools:visibility="visible" />
 
-    <com.tencent.qcloud.tuikit.tuiconversation.minimalistui.widget.ConversationListLayout
+    <com.adealink.weparty.im.list.widget.SessionListLayout
         android:id="@+id/conversation_layout"
         android:layout_width="0dp"
         android:layout_height="0dp"

+ 26 - 0
module/im/src/main/res/layout/layout_session_bottom_bar.xml

@@ -0,0 +1,26 @@
+<?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="wrap_content"
+    android:background="@color/white">
+
+    <include
+        android:id="@+id/v_voice_bar"
+        layout="@layout/layout_session_bottom_voice_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" />
+
+    <include
+        android:id="@+id/v_input_bar"
+        layout="@layout/layout_session_bottom_input_bar"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/v_voice_bar" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 73 - 0
module/im/src/main/res/layout/layout_session_bottom_input_bar.xml

@@ -0,0 +1,73 @@
+<?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"
+    android:background="@color/white"
+    android:paddingHorizontal="12dp"
+    android:paddingTop="8dp"
+    android:paddingBottom="10dp">
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/cl_input"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="10dp"
+        android:background="@drawable/im_session_input_bg"
+        android:minHeight="40dp"
+        android:paddingHorizontal="10dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toStartOf="@id/btn_voice"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent">
+
+        <androidx.appcompat.widget.AppCompatImageView
+            android:id="@+id/btn_emoji"
+            android:layout_width="24dp"
+            android:layout_height="24dp"
+            android:layout_marginBottom="8dp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:srcCompat="@drawable/im_session_emoji_ic" />
+
+        <androidx.appcompat.widget.AppCompatEditText
+            android:id="@+id/et_input_message"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_marginHorizontal="6dp"
+            android:layout_marginVertical="8dp"
+            android:background="@null"
+            android:gravity="start|center_vertical"
+            android:hint="@string/im_session_input_message"
+            android:includeFontPadding="false"
+            android:textColor="@color/color_FF1D2129"
+            android:textColorHint="@color/color_FFC9CDD4"
+            android:textSize="14sp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toStartOf="@id/btn_photo"
+            app:layout_constraintHorizontal_bias="0"
+            app:layout_constraintStart_toEndOf="@id/btn_emoji"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:text="asdfoasdfoasdfoasdfoasdfoasdfoasdfoasdfoasdfoasdfoasdfoasdfoasdfoasdfoasdfoasdfoasdfoasdfoasdfoasdfoasdfoasdfoasdfoasdfoasdfoasdfoasdfoasdfo" />
+
+        <androidx.appcompat.widget.AppCompatImageView
+            android:id="@+id/btn_photo"
+            android:layout_width="24dp"
+            android:layout_height="24dp"
+            android:layout_marginBottom="8dp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:srcCompat="@drawable/im_session_photo_ic" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+    <androidx.appcompat.widget.AppCompatImageView
+        android:id="@+id/btn_voice"
+        android:layout_width="40dp"
+        android:layout_height="40dp"
+        app:layout_constraintBottom_toBottomOf="@id/cl_input"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:srcCompat="@drawable/im_session_voice_ic" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 76 - 0
module/im/src/main/res/layout/layout_session_bottom_voice_bar.xml

@@ -0,0 +1,76 @@
+<?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"
+    android:background="@color/white">
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/cl_voice_record"
+        android:layout_width="match_parent"
+        android:layout_height="40dp"
+        android:paddingHorizontal="16dp"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent">
+
+        <androidx.appcompat.widget.AppCompatTextView
+            android:id="@+id/tv_record_time"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:fontFamily="@font/poppins_semibold"
+            android:includeFontPadding="false"
+            android:textColor="@color/color_FF1D2129"
+            android:textSize="18sp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:text="0:03" />
+
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/cl_op"
+        android:layout_width="0dp"
+        android:layout_height="58dp"
+        android:paddingHorizontal="12dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/cl_voice_record">
+
+        <androidx.appcompat.widget.AppCompatImageView
+            android:id="@+id/btn_remove"
+            android:layout_width="40dp"
+            android:layout_height="40dp"
+            app:layout_constraintBottom_toBottomOf="@id/cl_op"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="@id/cl_op"
+            app:srcCompat="@drawable/im_session_voice_record_delete_ic" />
+
+        <androidx.appcompat.widget.AppCompatImageView
+            android:id="@+id/btn_pause"
+            android:layout_width="40dp"
+            android:layout_height="40dp"
+            app:layout_constraintBottom_toBottomOf="@id/cl_op"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="@id/cl_op"
+            app:srcCompat="@drawable/im_session_voice_record_pause_ic" />
+
+        <androidx.appcompat.widget.AppCompatImageView
+            android:id="@+id/btn_send"
+            android:layout_width="40dp"
+            android:layout_height="40dp"
+            app:layout_constraintBottom_toBottomOf="@id/cl_op"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toTopOf="@id/cl_op"
+            app:srcCompat="@drawable/im_session_voice_record_send_ic" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 3 - 3
module/im/src/main/res/layout/layout_session_list_item.xml

@@ -7,7 +7,7 @@
     android:paddingHorizontal="16dp">
 
     <com.adealink.weparty.commonui.imageview.AvatarView
-        android:id="@+id/iv_close"
+        android:id="@+id/iv_avatar"
         android:layout_width="46dp"
         android:layout_height="46dp"
         app:layout_constraintBottom_toBottomOf="parent"
@@ -29,7 +29,7 @@
         app:layout_constraintBottom_toTopOf="@id/tv_desc"
         app:layout_constraintEnd_toStartOf="@id/barrier_right"
         app:layout_constraintHorizontal_bias="0"
-        app:layout_constraintStart_toEndOf="@id/iv_close"
+        app:layout_constraintStart_toEndOf="@id/iv_avatar"
         app:layout_constraintTop_toTopOf="parent"
         app:layout_constraintVertical_chainStyle="packed"
         tools:text="UserName, Super Beautiful Girl" />
@@ -49,7 +49,7 @@
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintEnd_toStartOf="@id/barrier_right"
         app:layout_constraintHorizontal_bias="0"
-        app:layout_constraintStart_toEndOf="@id/iv_close"
+        app:layout_constraintStart_toEndOf="@id/iv_avatar"
         app:layout_constraintTop_toBottomOf="@id/tv_title"
         tools:text="@string/im_open_notification_title_desc" />
 

+ 0 - 0
module/im/src/main/res/layout/layout_session_list_system_item.xml → module/im/src/main/res/layout/layout_session_list_official_item.xml


+ 40 - 0
module/im/src/main/res/layout/layout_session_message_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>

+ 33 - 0
module/im/src/main/res/layout/layout_session_message_image.xml

@@ -0,0 +1,33 @@
+<?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="wrap_content"
+    android:layout_height="wrap_content"
+    tools:background="@drawable/im_message_bubble_self_bg"
+    tools:paddingBottom="@dimen/im_message_bubble_self_padding_bottom"
+    tools:paddingEnd="@dimen/im_message_bubble_self_padding_end"
+    tools:paddingStart="@dimen/im_message_bubble_self_padding_start"
+    tools:paddingTop="@dimen/im_message_bubble_self_padding_top">
+
+    <com.adealink.frame.image.view.NetworkImageView
+        android:id="@+id/iv_img"
+        android:layout_width="@dimen/im_message_bubble_max_width"
+        android:layout_height="0dp"
+        app:layout_constraintDimensionRatio="282:214"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:roundedCornerRadius="12dp" />
+
+    <include
+        android:id="@+id/v_text"
+        layout="@layout/layout_session_message_text"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="4dp"
+        app:layout_constraintEnd_toEndOf="@id/iv_img"
+        app:layout_constraintStart_toStartOf="@id/iv_img"
+        app:layout_constraintTop_toBottomOf="@id/iv_img" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 67 - 0
module/im/src/main/res/layout/layout_session_message_sound.xml

@@ -0,0 +1,67 @@
+<?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="wrap_content"
+    android:layout_height="wrap_content"
+    tools:background="@drawable/im_message_bubble_self_bg"
+    tools:paddingBottom="@dimen/im_message_bubble_self_padding_bottom"
+    tools:paddingEnd="@dimen/im_message_bubble_self_padding_end"
+    tools:paddingStart="@dimen/im_message_bubble_self_padding_start"
+    tools:paddingTop="@dimen/im_message_bubble_self_padding_top">
+
+    <androidx.appcompat.widget.AppCompatImageView
+        android:id="@+id/btn_play"
+        android:layout_width="29dp"
+        android:layout_height="29dp"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:srcCompat="@drawable/im_session_sound_play_ic" />
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/cl_message_status"
+        android:layout_width="170dp"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="8dp"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toEndOf="@id/btn_play"
+        app:layout_constraintTop_toBottomOf="@id/btn_play">
+
+        <androidx.appcompat.widget.AppCompatTextView
+            android:id="@+id/tv_duration"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:includeFontPadding="false"
+            android:textColor="@color/color_FF86909C"
+            android:textSize="11sp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:text="9:10" />
+
+        <androidx.appcompat.widget.AppCompatTextView
+            android:id="@+id/tv_time"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginEnd="4dp"
+            android:includeFontPadding="false"
+            android:textColor="@color/color_FF86909C"
+            android:textSize="11sp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toStartOf="@id/iv_read_status"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:text="9:10" />
+
+        <androidx.appcompat.widget.AppCompatImageView
+            android:id="@+id/iv_read_status"
+            android:layout_width="15dp"
+            android:layout_height="14dp"
+            android:visibility="gone"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:srcCompat="@drawable/im_session_message_read_ic"
+            tools:visibility="visible" />
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 68 - 0
module/im/src/main/res/layout/layout_session_message_text.xml

@@ -0,0 +1,68 @@
+<?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="wrap_content"
+    android:layout_height="wrap_content"
+    tools:background="@drawable/im_message_bubble_self_bg"
+    tools:paddingBottom="@dimen/im_message_bubble_self_padding_bottom"
+    tools:paddingEnd="@dimen/im_message_bubble_self_padding_end"
+    tools:paddingStart="@dimen/im_message_bubble_self_padding_start"
+    tools:paddingTop="@dimen/im_message_bubble_self_padding_top">
+
+    <com.google.android.flexbox.FlexboxLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:alignContent="center"
+        app:alignItems="baseline"
+        app:flexDirection="row"
+        app:flexWrap="wrap"
+        app:justifyContent="flex_end"
+        app:layout_constrainedWidth="true"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintHorizontal_bias="0"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintWidth_max="@dimen/im_message_bubble_max_width">
+
+        <androidx.appcompat.widget.AppCompatTextView
+            android:id="@+id/tv_text"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:includeFontPadding="false"
+            android:textColor="@color/color_FF1D2129"
+            android:textSize="16sp"
+            tools:text="Mobile Game Game Mobile Mobile Game Game Mobile Mobile Game Game Mobile" />
+
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:id="@+id/cl_message_status"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content">
+
+            <androidx.appcompat.widget.AppCompatTextView
+                android:id="@+id/tv_time"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:includeFontPadding="false"
+                android:textColor="@color/color_FF86909C"
+                android:textSize="11sp"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="parent"
+                tools:text="9:10" />
+
+            <androidx.appcompat.widget.AppCompatImageView
+                android:id="@+id/iv_read_status"
+                android:layout_width="15dp"
+                android:layout_height="14dp"
+                android:layout_marginStart="4dp"
+                android:visibility="gone"
+                app:layout_constraintBottom_toBottomOf="@id/tv_time"
+                app:layout_constraintStart_toEndOf="@id/tv_time"
+                app:layout_constraintTop_toTopOf="@id/tv_time"
+                app:srcCompat="@drawable/im_session_message_read_ic"
+                tools:visibility="visible" />
+        </androidx.constraintlayout.widget.ConstraintLayout>
+
+    </com.google.android.flexbox.FlexboxLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 68 - 0
module/im/src/main/res/layout/layout_session_message_tips.xml

@@ -0,0 +1,68 @@
+<?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="wrap_content"
+    android:layout_height="wrap_content"
+    tools:background="@drawable/im_message_bubble_self_bg"
+    tools:paddingBottom="@dimen/im_message_bubble_self_padding_bottom"
+    tools:paddingEnd="@dimen/im_message_bubble_self_padding_end"
+    tools:paddingStart="@dimen/im_message_bubble_self_padding_start"
+    tools:paddingTop="@dimen/im_message_bubble_self_padding_top">
+
+    <com.google.android.flexbox.FlexboxLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:alignContent="center"
+        app:alignItems="baseline"
+        app:flexDirection="row"
+        app:flexWrap="wrap"
+        app:justifyContent="flex_end"
+        app:layout_constrainedWidth="true"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintHorizontal_bias="0"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintWidth_max="@dimen/im_message_bubble_max_width">
+
+        <androidx.appcompat.widget.AppCompatTextView
+            android:id="@+id/tv_text"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:includeFontPadding="false"
+            android:textColor="@color/color_FF1D2129"
+            android:textSize="16sp"
+            tools:text="Mobile Game Game Mobile Mobile Game Game Mobile Mobile Game Game Mobile" />
+
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:id="@+id/cl_message_status"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content">
+
+            <androidx.appcompat.widget.AppCompatTextView
+                android:id="@+id/tv_time"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:includeFontPadding="false"
+                android:textColor="@color/color_FF86909C"
+                android:textSize="11sp"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="parent"
+                tools:text="9:10" />
+
+            <androidx.appcompat.widget.AppCompatImageView
+                android:id="@+id/iv_read_status"
+                android:layout_width="15dp"
+                android:layout_height="14dp"
+                android:layout_marginStart="4dp"
+                android:visibility="gone"
+                app:layout_constraintBottom_toBottomOf="@id/tv_time"
+                app:layout_constraintStart_toEndOf="@id/tv_time"
+                app:layout_constraintTop_toTopOf="@id/tv_time"
+                app:srcCompat="@drawable/im_session_message_read_ic"
+                tools:visibility="visible" />
+        </androidx.constraintlayout.widget.ConstraintLayout>
+
+    </com.google.android.flexbox.FlexboxLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 55 - 0
module/im/src/main/res/layout/layout_session_message_unsupport.xml

@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<com.google.android.flexbox.FlexboxLayout 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="wrap_content"
+    android:layout_height="wrap_content"
+    android:maxWidth="@dimen/im_message_bubble_max_width"
+    app:alignContent="center"
+    app:alignItems="baseline"
+    app:flexDirection="row"
+    app:flexWrap="wrap"
+    app:justifyContent="flex_end">
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/tv_text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="8dp"
+        android:includeFontPadding="false"
+        android:textColor="@color/color_FF1D2129"
+        android:textSize="16sp"
+        app:layout_flexGrow="1"
+        tools:text="Mobile Game Game Mobile Mobile Game Game Mobile " />
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/cl_message_status"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="8dp">
+
+        <androidx.appcompat.widget.AppCompatTextView
+            android:id="@+id/tv_time"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:includeFontPadding="false"
+            android:textColor="@color/color_FF86909C"
+            android:textSize="11sp"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:text="9:10" />
+
+        <androidx.appcompat.widget.AppCompatImageView
+            android:id="@+id/iv_read_status"
+            android:layout_width="15dp"
+            android:layout_height="14dp"
+            android:layout_marginStart="4dp"
+            android:visibility="gone"
+            app:layout_constraintBottom_toBottomOf="@id/tv_time"
+            app:layout_constraintStart_toEndOf="@id/tv_time"
+            app:layout_constraintTop_toTopOf="@id/tv_time"
+            app:srcCompat="@drawable/im_session_message_read_ic"
+            tools:visibility="visible" />
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+</com.google.android.flexbox.FlexboxLayout>

+ 101 - 0
module/im/src/main/res/layout/layout_session_top_bar.xml

@@ -0,0 +1,101 @@
+<?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" />
+
+    <com.adealink.weparty.commonui.imageview.AvatarView
+        android:id="@+id/iv_avatar"
+        android:layout_width="34dp"
+        android:layout_height="34dp"
+        android:layout_marginStart="12dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintStart_toEndOf="@id/iv_back"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/tv_user_name"
+        android:layout_width="wrap_content"
+        android:layout_height="18dp"
+        android:layout_marginStart="12dp"
+        android:fontFamily="@font/poppins_semibold"
+        android:gravity="start|center_vertical"
+        android:includeFontPadding="false"
+        android:text="@string/app_name"
+        android:textColor="@color/color_FF1D2129"
+        android:textSize="16sp"
+        app:layout_constraintStart_toEndOf="@id/iv_avatar"
+        app:layout_constraintTop_toTopOf="@id/iv_avatar"
+        tools:text="Zaraaaa🦋rl" />
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/tv_user_online"
+        android:layout_width="wrap_content"
+        android:layout_height="18dp"
+        android:gravity="start|center_vertical"
+        android:includeFontPadding="false"
+        android:text="@string/app_name"
+        android:textColor="@color/color_FF86909C"
+        android:textSize="11sp"
+        app:layout_constraintBottom_toBottomOf="@id/iv_avatar"
+        app:layout_constraintStart_toStartOf="@id/tv_user_name"
+        app:layout_constraintTop_toBottomOf="@id/tv_user_name"
+        tools:text="text tip" />
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/cl_right"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="12dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="parent">
+
+        <androidx.appcompat.widget.AppCompatImageView
+            android:id="@+id/btn_follow"
+            android:layout_width="24dp"
+            android:layout_height="24dp"
+            android:layout_marginEnd="10dp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toStartOf="@id/btn_call"
+            app:layout_constraintTop_toTopOf="parent"
+            app:srcCompat="@drawable/im_session_follow_ic" />
+
+        <androidx.appcompat.widget.AppCompatImageView
+            android:id="@+id/btn_call"
+            android:layout_width="24dp"
+            android:layout_height="24dp"
+            android:layout_marginEnd="10dp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toStartOf="@id/btn_more"
+            app:layout_constraintTop_toTopOf="parent"
+            app:srcCompat="@drawable/im_session_call_ic" />
+
+        <androidx.appcompat.widget.AppCompatImageView
+            android:id="@+id/btn_more"
+            android:layout_width="24dp"
+            android:layout_height="24dp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:srcCompat="@drawable/im_session_more_ic" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 14 - 0
module/im/src/main/res/values/dimens.xml

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <dimen name="im_message_bubble_max_width">280dp</dimen>
+
+    <dimen name="im_message_bubble_self_padding_start">8dp</dimen>
+    <dimen name="im_message_bubble_self_padding_end">18dp</dimen>
+    <dimen name="im_message_bubble_self_padding_top">8dp</dimen>
+    <dimen name="im_message_bubble_self_padding_bottom">8dp</dimen>
+
+    <dimen name="im_message_bubble_other_padding_start">18dp</dimen>
+    <dimen name="im_message_bubble_other_padding_end">8dp</dimen>
+    <dimen name="im_message_bubble_other_padding_top">8dp</dimen>
+    <dimen name="im_message_bubble_other_padding_bottom">8dp</dimen>
+</resources>

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

@@ -4,4 +4,7 @@
     <string name="im_open_notification_title">Open notification</string>
     <string name="im_open_notification_title_desc">Receive important information</string>
     <string name="im_turn_on_notification">Turn on</string>
+    <string name="im_time_just_now">刚刚</string>
+    <string name="im_session_input_message">Please enter text</string>
+    <string name="im_no_suport_message">Unsupported messages</string>
 </resources>

+ 4 - 0
module/im/src/main/res/values/styles.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+</resources>