Bladeren bron

feat: 优化IM操作,举报

DoggyZhang 3 maanden geleden
bovenliggende
commit
9dbd653bf2
48 gewijzigde bestanden met toevoegingen van 1251 en 56 verwijderingen
  1. 1 1
      app/src/main/java/com/adealink/weparty/module/im/Router.kt
  2. 8 0
      app/src/main/java/com/adealink/weparty/module/setting/Router.kt
  3. 11 0
      app/src/main/res/drawable/common_switch_thumb.xml
  4. 6 0
      app/src/main/res/drawable/common_switch_track.xml
  5. 5 0
      app/src/main/res/drawable/common_switch_track_sel.xml
  6. 6 0
      app/src/main/res/drawable/common_switch_track_selected.xml
  7. 9 0
      app/src/main/res/values/styles.xml
  8. 6 2
      frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/TextUtil.java
  9. 2 1
      frame/tuikit/TUIChat/tuichat/src/main/res/layout/tuichat_chat_activity_layout.xml
  10. 1 1
      module/im/src/main/java/com/adealink/weparty/im/IMServiceImpl.kt
  11. 42 0
      module/im/src/main/java/com/adealink/weparty/im/data/IMData.kt
  12. 24 0
      module/im/src/main/java/com/adealink/weparty/im/datasource/remote/IMHttpService.kt
  13. 0 8
      module/im/src/main/java/com/adealink/weparty/im/manager/IIMLoginListener.kt
  14. 0 4
      module/im/src/main/java/com/adealink/weparty/im/manager/login/GenerateUserSig.kt
  15. 8 0
      module/im/src/main/java/com/adealink/weparty/im/manager/login/ILoginListener.kt
  16. 2 2
      module/im/src/main/java/com/adealink/weparty/im/manager/login/ILoginManager.kt
  17. 6 7
      module/im/src/main/java/com/adealink/weparty/im/manager/login/LoginManager.kt
  18. 0 0
      module/im/src/main/java/com/adealink/weparty/im/manager/login/LoginWrapper.kt
  19. 7 0
      module/im/src/main/java/com/adealink/weparty/im/manager/session/ISessionListener.kt
  20. 29 0
      module/im/src/main/java/com/adealink/weparty/im/manager/session/ISessionManager.kt
  21. 169 0
      module/im/src/main/java/com/adealink/weparty/im/manager/session/SessionManager.kt
  22. 14 3
      module/im/src/main/java/com/adealink/weparty/im/session/SessionActivity.kt
  23. 10 3
      module/im/src/main/java/com/adealink/weparty/im/session/SessionFragment.kt
  24. 6 5
      module/im/src/main/java/com/adealink/weparty/im/session/adapter/viewbinder/BaseMessageViewBinder.kt
  25. 2 1
      module/im/src/main/java/com/adealink/weparty/im/session/adapter/viewbinder/ImageMessageViewBinder.kt
  26. 2 3
      module/im/src/main/java/com/adealink/weparty/im/session/adapter/viewbinder/SoundMessageViewBinder.kt
  27. 12 0
      module/im/src/main/java/com/adealink/weparty/im/session/adapter/viewbinder/TextMessageViewBinder.kt
  28. 6 0
      module/im/src/main/java/com/adealink/weparty/im/session/comp/SessionBottomAudioComp.kt
  29. 21 1
      module/im/src/main/java/com/adealink/weparty/im/session/comp/SessionBottomInputComp.kt
  30. 85 1
      module/im/src/main/java/com/adealink/weparty/im/session/comp/SessionTopComp.kt
  31. 5 3
      module/im/src/main/java/com/adealink/weparty/im/session/comp/input/SessionInputMachine.kt
  32. 3 1
      module/im/src/main/java/com/adealink/weparty/im/session/comp/viewmodel/SessionInputViewModel.kt
  33. 53 0
      module/im/src/main/java/com/adealink/weparty/im/session/dialog/SessionSettingDialog.kt
  34. 56 3
      module/im/src/main/java/com/adealink/weparty/im/session/viewmodel/SessionViewModel.kt
  35. BIN
      module/im/src/main/res/drawable-xhdpi/im_session_setting_black_ic.png
  36. BIN
      module/im/src/main/res/drawable-xhdpi/im_session_setting_mark_ic.png
  37. BIN
      module/im/src/main/res/drawable-xhdpi/im_session_setting_mute_ic.png
  38. BIN
      module/im/src/main/res/drawable-xhdpi/im_session_setting_report_ic.png
  39. 195 0
      module/im/src/main/res/layout/dialog_session_setting.xml
  40. 1 1
      module/im/src/main/res/layout/layout_session_bottom_input_bar.xml
  41. 7 3
      module/im/src/main/res/layout/layout_session_top_bar.xml
  42. 4 0
      module/im/src/main/res/values/strings.xml
  43. 5 0
      module/setting/src/main/AndroidManifest.xml
  44. 190 0
      module/setting/src/main/java/com/adealink/weparty/setting/report/ReportActivity.kt
  45. 56 0
      module/setting/src/main/java/com/adealink/weparty/setting/report/viewmodel/ReportViewModel.kt
  46. 1 2
      module/setting/src/main/res/layout/activity_help_center.xml
  47. 171 0
      module/setting/src/main/res/layout/activity_report.xml
  48. 4 0
      module/setting/src/main/res/values/strings.xml

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

@@ -27,7 +27,7 @@ interface IM {
             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_ID = "extra_chat_id" //即uid
             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"

+ 8 - 0
app/src/main/java/com/adealink/weparty/module/setting/Router.kt

@@ -36,4 +36,12 @@ interface Setting {
         }
     }
 
+    interface Report {
+        companion object {
+            const val PATH = "${Common.PATH}/report"
+
+            const val EXTRA_UID = "extra_uid"
+        }
+    }
+
 }

+ 11 - 0
app/src/main/res/drawable/common_switch_thumb.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="oval">
+    <solid android:color="@color/white" />
+    <size
+        android:width="16dp"
+        android:height="16dp" />
+    <stroke
+        android:width="3dp"
+        android:color="@color/transparent" />
+</shape>

+ 6 - 0
app/src/main/res/drawable/common_switch_track.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/color_FFC9CDD4" />
+    <corners android:radius="15dp" />
+</shape>

+ 5 - 0
app/src/main/res/drawable/common_switch_track_sel.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:drawable="@drawable/common_switch_track_selected" android:state_checked="true" />
+    <item android:drawable="@drawable/common_switch_track" />
+</selector>

+ 6 - 0
app/src/main/res/drawable/common_switch_track_selected.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/color_FF1789FF" />
+    <corners android:radius="15dp" />
+</shape>

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

@@ -158,4 +158,13 @@
         <item name="android:requiresFadingEdge">horizontal</item>
         <item name="android:fadingEdge">horizontal</item>
     </style>
+
+    <style name="CommonSwitch">
+        <item name="android:layout_width">wrap_content</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:switchMinWidth">32dp</item>
+        <item name="android:switchPadding">0dp</item>
+        <item name="android:thumb">@drawable/common_switch_thumb</item>
+        <item name="track">@drawable/common_switch_track_sel</item>
+    </style>
 </resources>

+ 6 - 2
frame/tuikit/TIMCommon/timcommon/src/main/java/com/tencent/qcloud/tuikit/timcommon/util/TextUtil.java

@@ -1,5 +1,6 @@
 package com.tencent.qcloud.tuikit.timcommon.util;
 
+import android.annotation.SuppressLint;
 import android.graphics.Path;
 import android.graphics.RectF;
 import android.graphics.Region;
@@ -28,6 +29,7 @@ public class TextUtil {
     public static final Pattern PHONE_NUMBER_PATTERN =
             Pattern.compile("(\\+?(\\d{1,4}[-\\s]?)?)?(\\(?\\d+\\)?[-\\s]?)?[\\d\\s-]{5,14}");
 
+    @SuppressLint("ClickableViewAccessibility")
     public static void linkifyUrls(TextView textView) {
         Linkify.addLinks(textView, Linkify.WEB_URLS | Linkify.EMAIL_ADDRESSES);
         Linkify.addLinks(textView, PHONE_NUMBER_PATTERN, "tel:");
@@ -64,8 +66,10 @@ public class TextUtil {
         textView.setMovementMethod(new LinkMovementMethod() {
             @Override
             public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
-                gestureDetector.onTouchEvent(event);
-                return false;
+                if (gestureDetector.onTouchEvent(event)) {
+                    return true;
+                }
+                return super.onTouchEvent(widget, buffer, event);
             }
         });
     }

+ 2 - 1
frame/tuikit/TUIChat/tuichat/src/main/res/layout/tuichat_chat_activity_layout.xml

@@ -2,7 +2,8 @@
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:orientation="vertical">
+    android:orientation="vertical"
+    android:fitsSystemWindows="true">
 
     <RelativeLayout
         android:id="@+id/empty_view"

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

@@ -5,7 +5,7 @@ import android.app.Application
 import com.adealink.frame.router.Router
 import com.adealink.frame.spi.RegisterService
 import com.adealink.weparty.im.constant.OFFICIAL_IMAGE_TEXT_BUSINESS_ID
-import com.adealink.weparty.im.manager.imLoginManager
+import com.adealink.weparty.im.manager.login.imLoginManager
 import com.adealink.weparty.im.service.TIMAppService
 import com.adealink.weparty.im.session.mesasge.CustomMessageViewHolder
 import com.adealink.weparty.im.session.mesasge.OfficialImageTextMessageBean

+ 42 - 0
module/im/src/main/java/com/adealink/weparty/im/data/IMData.kt

@@ -0,0 +1,42 @@
+package com.adealink.weparty.im.data
+
+import com.adealink.frame.network.data.PageReq
+import com.google.gson.annotations.GsonNullable
+import com.google.gson.annotations.SerializedName
+
+data class IMBlackReq(
+    @SerializedName("userNo") val uid: String,
+    @SerializedName("black") val black: Boolean
+)
+
+
+data class IMIsBlackReq(
+    @SerializedName("userNo") val uid: String,
+)
+
+data class IMIsBlackRes(
+    @SerializedName("black") val black: Boolean,
+)
+
+
+data class IMBlackListReq(
+    @SerializedName("page") val page: PageReq
+)
+
+data class IMBlackListRes(
+    @SerializedName("list") val list: List<BlackData>,
+    @GsonNullable
+    @SerializedName("next") val next: String?,
+)
+
+data class BlackData(
+    @SerializedName("userNo") val uid: String,
+    @GsonNullable
+    @SerializedName("nickname") var nickName: String? = null,
+    @GsonNullable
+    @SerializedName("avatar") var avatar: String? = null,
+    @GsonNullable
+    @SerializedName("gender") var gender: Int? = null,
+    @GsonNullable
+    @SerializedName("age") var age: Int? = null
+)

+ 24 - 0
module/im/src/main/java/com/adealink/weparty/im/datasource/remote/IMHttpService.kt

@@ -0,0 +1,24 @@
+package com.adealink.weparty.im.datasource.remote
+
+import com.adealink.frame.base.Rlt
+import com.adealink.frame.network.data.Res
+import com.adealink.weparty.im.data.IMBlackListReq
+import com.adealink.weparty.im.data.IMBlackListRes
+import com.adealink.weparty.im.data.IMBlackReq
+import com.adealink.weparty.im.data.IMIsBlackReq
+import com.adealink.weparty.im.data.IMIsBlackRes
+import retrofit2.http.Body
+import retrofit2.http.POST
+
+interface IMHttpService {
+
+    @POST("im/pullInBlacklist")
+    suspend fun imBlack(@Body req: IMBlackReq): Rlt<Res<Any>>
+
+
+    @POST("im/pullInBlacklist")
+    suspend fun isBlack(@Body req: IMIsBlackReq): Rlt<Res<IMIsBlackRes>>
+
+    @POST("im/pullInBlacklist")
+    suspend fun imBlackList(@Body req: IMBlackListReq): Rlt<Res<IMBlackListRes>>
+}

+ 0 - 8
module/im/src/main/java/com/adealink/weparty/im/manager/IIMLoginListener.kt

@@ -1,8 +0,0 @@
-package com.adealink.weparty.call.manager
-
-import com.adealink.frame.frame.IListener
-
-interface ICallLoginListener : IListener {
-
-
-}

+ 0 - 4
module/im/src/main/java/com/adealink/weparty/im/manager/GenerateUserSig.kt → module/im/src/main/java/com/adealink/weparty/im/manager/login/GenerateUserSig.kt

@@ -11,10 +11,6 @@ import java.util.Arrays
 import java.util.zip.Deflater
 import javax.crypto.Mac
 import javax.crypto.spec.SecretKeySpec
-import kotlin.code
-import kotlin.collections.indices
-import kotlin.text.toByteArray
-import kotlin.text.trimIndent
 
 /*
 * Description: Generates UserSig for testing. UserSig is a security signature designed

+ 8 - 0
module/im/src/main/java/com/adealink/weparty/im/manager/login/ILoginListener.kt

@@ -0,0 +1,8 @@
+package com.adealink.weparty.im.manager.login
+
+import com.adealink.frame.frame.IListener
+
+interface ILoginListener : IListener {
+
+
+}

+ 2 - 2
module/im/src/main/java/com/adealink/weparty/im/manager/IIMLoginManager.kt → module/im/src/main/java/com/adealink/weparty/im/manager/login/ILoginManager.kt

@@ -1,9 +1,9 @@
-package com.adealink.weparty.call.manager
+package com.adealink.weparty.im.manager.login
 
 import android.app.Application
 import com.adealink.frame.frame.IBaseFrame
 
-interface ICallLoginManager : IBaseFrame<ICallLoginListener> {
+interface ILoginManager : IBaseFrame<ILoginListener> {
 
     fun init(application: Application)
 

+ 6 - 7
module/im/src/main/java/com/adealink/weparty/im/manager/IMLoginManager.kt → module/im/src/main/java/com/adealink/weparty/im/manager/login/LoginManager.kt

@@ -1,13 +1,12 @@
-package com.adealink.weparty.im.manager
+package com.adealink.weparty.im.manager.login
 
 import android.app.Application
 import com.adealink.frame.frame.BaseFrame
 import com.adealink.frame.log.Log
 import com.adealink.frame.util.AppUtil
-import com.adealink.weparty.call.manager.ICallLoginListener
-import com.adealink.weparty.call.manager.ICallLoginManager
-import com.adealink.weparty.im.constant.OFFICIAL_UID
 import com.adealink.weparty.im.constant.TAG_IM_LOGIN
+import com.adealink.weparty.im.manager.GenerateUserSig
+import com.adealink.weparty.im.manager.LoginWrapper
 import com.adealink.weparty.im.service.TIMAppService
 import com.adealink.weparty.im.util.TUIUtils
 import com.adealink.weparty.module.account.AccountModule
@@ -18,10 +17,10 @@ import com.tencent.qcloud.tuicore.interfaces.TUICallback
 import com.tencent.qcloud.tuicore.interfaces.TUILoginConfig
 import com.tencent.qcloud.tuicore.interfaces.TUILoginListener
 
-val imLoginManager: ICallLoginManager by lazy { IMLoginManager() }
+val imLoginManager: ILoginManager by lazy { LoginManager() }
 
-class IMLoginManager : BaseFrame<ICallLoginListener>(),
-    ICallLoginManager,
+class LoginManager : BaseFrame<com.adealink.weparty.im.manager.login.ILoginListener>(),
+    ILoginManager,
     ILoginListener {
 
     override fun init(application: Application) {

+ 0 - 0
module/im/src/main/java/com/adealink/weparty/im/manager/LoginWrapper.kt → module/im/src/main/java/com/adealink/weparty/im/manager/login/LoginWrapper.kt


+ 7 - 0
module/im/src/main/java/com/adealink/weparty/im/manager/session/ISessionListener.kt

@@ -0,0 +1,7 @@
+package com.adealink.weparty.im.manager.session
+
+import com.adealink.frame.frame.IListener
+
+interface ISessionListener : IListener {
+
+}

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

@@ -0,0 +1,29 @@
+package com.adealink.weparty.im.manager.session
+
+import com.adealink.frame.base.Rlt
+import com.adealink.frame.frame.IBaseFrame
+
+interface ISessionManager : IBaseFrame<ISessionListener> {
+
+    suspend fun isSessionBlack(
+        uid: String,
+    ): Rlt<Boolean>
+
+    suspend fun setSessionBlack(
+        uids: String,
+        black: Boolean
+    ): Rlt<Map<String, Boolean>>
+
+
+    suspend fun isSessionMute(
+        uids: List<String>,
+        chatInfoType: Int
+    ): Rlt<Map<String, Boolean>>
+
+    suspend fun setSessionMute(
+        uids: List<String>,
+        chatInfoType: Int,
+        mute: Boolean
+    ): Rlt<Any>
+
+}

+ 169 - 0
module/im/src/main/java/com/adealink/weparty/im/manager/session/SessionManager.kt

@@ -0,0 +1,169 @@
+package com.adealink.weparty.im.manager.session
+
+import com.adealink.frame.base.CommonParamError
+import com.adealink.frame.base.IError
+import com.adealink.frame.base.Rlt
+import com.adealink.frame.base.fastLazy
+import com.adealink.frame.frame.BaseFrame
+import com.adealink.frame.storage.cache.TimeoutLruCache
+import com.adealink.weparty.App
+import com.adealink.weparty.im.data.IMIsBlackReq
+import com.adealink.weparty.im.datasource.remote.IMHttpService
+import com.tencent.imsdk.v2.V2TIMCallback
+import com.tencent.imsdk.v2.V2TIMManager
+import com.tencent.imsdk.v2.V2TIMMessage
+import com.tencent.imsdk.v2.V2TIMReceiveMessageOptInfo
+import com.tencent.imsdk.v2.V2TIMValueCallback
+import com.tencent.qcloud.tuikit.tuichat.bean.ChatInfo
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlin.coroutines.resume
+
+val sessionManager: ISessionManager by lazy { SessionManager() }
+
+class SessionManager : BaseFrame<ISessionListener>(),
+    ISessionManager {
+
+
+    //会话免打扰(静音)
+    private val sessionMuteCache by lazy {
+        TimeoutLruCache<String, Boolean>(
+            1000,
+            10 * 60 * 1000
+        ) //最多缓存1000个,缓存时长10分钟
+    }
+
+    //拉黑名单
+    private val sessionBlackCache by lazy {
+        TimeoutLruCache<String, Boolean>(
+            1000,
+            10 * 60 * 1000
+        ) //最多缓存1000个,缓存时长10分钟
+    }
+
+
+    private val imHttpService by fastLazy {
+        App.instance.networkService.getHttpService(IMHttpService::class.java)
+    }
+
+    override suspend fun isSessionBlack(
+        uid: String
+    ): Rlt<Boolean> {
+        val rlt = imHttpService.isBlack(IMIsBlackReq(uid))
+        return when (rlt) {
+            is Rlt.Failed -> {
+                rlt
+            }
+
+            is Rlt.Success -> {
+                Rlt.Success(rlt.data.data?.black ?: false)
+            }
+        }
+    }
+
+    override suspend fun setSessionBlack(
+        uids: String,
+        black: Boolean
+    ): Rlt<Map<String, Boolean>> {
+        return suspendCancellableCoroutine { continuation ->
+
+        }
+    }
+
+    override suspend fun isSessionMute(
+        uids: List<String>,
+        chatInfoType: Int
+    ): Rlt<Map<String, Boolean>> {
+        return suspendCancellableCoroutine { continuation ->
+            when (chatInfoType) {
+                ChatInfo.TYPE_C2C -> {
+                    V2TIMManager.getMessageManager().getC2CReceiveMessageOpt(uids, object :
+                        V2TIMValueCallback<List<V2TIMReceiveMessageOptInfo>> {
+                        override fun onSuccess(options: List<V2TIMReceiveMessageOptInfo>?) {
+                            if (continuation.isActive) {
+                                val muteMap = mutableMapOf<String, Boolean>()
+                                options?.forEach { option ->
+                                    muteMap[option.userID] =
+                                        option.c2CReceiveMessageOpt == V2TIMMessage.V2TIM_RECEIVE_NOT_NOTIFY_MESSAGE
+                                    sessionMuteCache.put(
+                                        option.userID,
+                                        option.c2CReceiveMessageOpt == V2TIMMessage.V2TIM_RECEIVE_NOT_NOTIFY_MESSAGE
+                                    )
+                                }
+                                continuation.resume(Rlt.Success(muteMap))
+                            }
+                        }
+
+                        override fun onError(code: Int, desc: String?) {
+                            if (continuation.isActive) {
+                                continuation.resume(
+                                    Rlt.Failed(
+                                        IError(
+                                            serverCode = code,
+                                            msg = desc ?: ""
+                                        )
+                                    )
+                                )
+                            }
+                        }
+                    })
+                }
+
+                ChatInfo.TYPE_GROUP -> {
+                    //Not support
+                    continuation.resume(Rlt.Failed(CommonParamError("Unsupported: group chat")))
+                }
+            }
+        }
+    }
+
+    override suspend fun setSessionMute(
+        uids: List<String>,
+        chatInfoType: Int,
+        mute: Boolean
+    ): Rlt<Any> {
+        return suspendCancellableCoroutine { continuation ->
+            when (chatInfoType) {
+                ChatInfo.TYPE_C2C -> {
+                    V2TIMManager.getMessageManager().setC2CReceiveMessageOpt(
+                        uids,
+                        if (mute) {
+                            V2TIMMessage.V2TIM_RECEIVE_NOT_NOTIFY_MESSAGE
+                        } else {
+                            V2TIMMessage.V2TIM_RECEIVE_MESSAGE
+                        },
+                        object : V2TIMCallback {
+                            override fun onSuccess() {
+                                if (continuation.isActive) {
+                                    uids.forEach { uid ->
+                                        sessionMuteCache.put(uid, mute)
+                                    }
+                                    continuation.resume(Rlt.Success(Any()))
+                                }
+                            }
+
+                            override fun onError(code: Int, desc: String?) {
+                                if (continuation.isActive) {
+                                    continuation.resume(
+                                        Rlt.Failed(
+                                            IError(
+                                                serverCode = code,
+                                                msg = desc ?: ""
+                                            )
+                                        )
+                                    )
+                                }
+                            }
+
+                        }
+                    )
+                }
+
+                ChatInfo.TYPE_GROUP -> {
+                    continuation.resume(Rlt.Failed(CommonParamError("Unsupported: group chat")))
+                }
+            }
+        }
+    }
+
+
+}

+ 14 - 3
module/im/src/main/java/com/adealink/weparty/im/session/SessionActivity.kt

@@ -1,6 +1,7 @@
 package com.adealink.weparty.im.session
 
 import android.view.inputmethod.InputMethodManager
+import androidx.activity.viewModels
 import com.adealink.frame.base.fastLazy
 import com.adealink.frame.mvvm.view.viewBinding
 import com.adealink.frame.router.Router
@@ -11,6 +12,8 @@ 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.im.session.viewmodel.SessionViewModel
+import com.adealink.weparty.im.viewmodel.IMViewModelFactory
 import com.adealink.weparty.module.im.IM
 import com.tencent.imsdk.v2.V2TIMConversation
 import com.tencent.qcloud.tuikit.tuichat.bean.C2CChatInfo
@@ -40,13 +43,16 @@ class SessionActivity : BaseActivity() {
     @BindExtra(IM.Session.EXTRA_CHAT_DRAFT_TIME)
     var chatDraftTime: Long? = null
 
-    private val binding by viewBinding(ActivitySessionBinding::inflate)
+    private var chatInfo: ChatInfo? = null
 
+    private val binding by viewBinding(ActivitySessionBinding::inflate)
+    private val sessionViewModel by viewModels<SessionViewModel> { IMViewModelFactory() }
     private val sessionFragment: SessionFragment by fastLazy { SessionFragment() }
 
     override fun onBeforeCreate() {
         super.onBeforeCreate()
         Router.bind(this)
+        chatInfo = getChatInfo()
     }
 
     override fun initViews() {
@@ -63,19 +69,24 @@ class SessionActivity : BaseActivity() {
 
     override fun initComponents() {
         super.initComponents()
-        SessionTopComp(this, binding.topBar).attach()
+        SessionTopComp(this, binding.topBar, chatInfo).attach()
     }
 
     private fun inflateSessionFragment() {
         if (sessionFragment.isAdded) {
             return
         }
-        sessionFragment.setChatInfo(getChatInfo())
+        sessionFragment.setChatInfo(chatInfo)
         supportFragmentManager.beginTransaction()
             .replace(binding.flContent.id, sessionFragment, IM.Session.PATH)
             .commitAllowingStateLoss()
     }
 
+    override fun loadData() {
+        super.loadData()
+        sessionViewModel.loadChatInfo(chatInfo)
+    }
+
     private fun getChatInfo(): ChatInfo? {
         if (chatID.isNullOrEmpty()) {
             return null

+ 10 - 3
module/im/src/main/java/com/adealink/weparty/im/session/SessionFragment.kt

@@ -1,8 +1,10 @@
 package com.adealink.weparty.im.session
 
+import android.annotation.SuppressLint
 import android.view.MotionEvent
 import android.view.View
 import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.doOnPreDraw
 import androidx.core.view.updatePadding
 import androidx.fragment.app.activityViewModels
 import androidx.recyclerview.widget.LinearLayoutManager
@@ -20,6 +22,7 @@ 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.im.session.comp.input.InputAction
 import com.adealink.weparty.im.session.comp.viewmodel.SessionInputViewModel
 import com.adealink.weparty.module.im.data.TAG_IM_SESSION
 import com.tencent.qcloud.tuicore.TUIConstants
@@ -55,6 +58,7 @@ class SessionFragment : BaseFragment(R.layout.fragment_session) {
         this.chatInfo = chatInfo
     }
 
+    @SuppressLint("ClickableViewAccessibility")
     override fun initViews() {
         super.initViews()
         when (chatInfo?.type) {
@@ -148,6 +152,7 @@ class SessionFragment : BaseFragment(R.layout.fragment_session) {
         // 关键:只在需要的时候设置
         activity?.window?.fitSystemWindows(false)
         // 内容布局处理键盘
+        val messageList = binding.rvMessage
         binding.root.onWindowInsets { view, insets ->
             val ime = insets.getInsets(WindowInsetsCompat.Type.ime())
             val nav = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
@@ -155,9 +160,9 @@ class SessionFragment : BaseFragment(R.layout.fragment_session) {
             // 键盘显示时使用键盘高度,否则使用导航栏高度
             val bottomInset = if (ime.bottom > 0) ime.bottom else nav.bottom
             view.updatePadding(bottom = bottomInset)
-//            binding.svContent.doOnPreDraw {
-//                binding.svContent.smoothScrollTo(0, bottomInset)
-//            }
+            messageList.doOnPreDraw {
+                messageList.scrollToEnd()
+            }
             insets
         }
     }
@@ -270,12 +275,14 @@ class SessionFragment : BaseFragment(R.layout.fragment_session) {
         override fun onMessageLongClick(view: View?, messageBean: TUIMessageBean?) {
             view ?: return
             messageBean ?: return
+            inputViewModel.execute(InputAction.EMPTY_CLICKED)
             onMessageLongClicked(view, messageBean)
         }
 
         override fun onMessageClick(view: View?, messageBean: TUIMessageBean?) {
             view ?: return
             messageBean ?: return
+            inputViewModel.execute(InputAction.EMPTY_CLICKED)
         }
 
         override fun onUserIconClick(view: View?, messageBean: TUIMessageBean?) {

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

@@ -27,6 +27,8 @@ abstract class BaseMessageViewBinder<T : TUIMessageBean, V : ViewBinding, MH : M
     override fun onBindViewHolder(holder: MH, item: T) {
         initView(holder, item)
         setTimeTitle(holder, item)
+
+        holder.initView(holder.messageBinding, item, onItemClickListener)
         if (item.isSelf) {
             applySelfStyle(holder, item)
             holder.onBindSelfMessage(holder.messageBinding, item)
@@ -41,22 +43,21 @@ abstract class BaseMessageViewBinder<T : TUIMessageBean, V : ViewBinding, MH : M
         holder: MH, msg: T
     ) {
         holder.binding.root.onClick {
-            onItemClickListener?.onMessageLongClick(holder.binding.root, msg)
+            onItemClickListener?.onMessageClick(holder.binding.root, msg)
             true
         }
         if (msg.status == TUIMessageBean.MSG_STATUS_SEND_FAIL) {
             //消息发送失败触发长按
-            holder.binding.root.onClick {
+            holder.messageBinding.root.onClick {
                 onItemClickListener?.onMessageLongClick(holder.binding.root, msg)
                 true
             }
         } else {
-            holder.binding.root.onClick {
+            holder.messageBinding.root.onClick {
                 onItemClickListener?.onMessageClick(holder.binding.root, msg)
                 true
             }
         }
-        holder.initView(holder.messageBinding, msg)
     }
 
     private fun applySelfStyle(holder: MH, msg: T) {
@@ -125,7 +126,7 @@ abstract class MessageViewHolder<T : TUIMessageBean, V : ViewBinding>(
     val messageBinding: V
 ) : BindingViewHolder<LayoutSessionMessageBaseBinding>(rootBinding) {
 
-    open fun initView(binding: V, msg: T) {}
+    open fun initView(binding: V, msg: T, onItemClickListener: OnItemClickListener?) {}
 
     abstract fun onBindSelfMessage(binding: V, msg: T)
 

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

@@ -26,7 +26,8 @@ class ImageMessageViewHolder(
 
     override fun initView(
         binding: LayoutSessionMessageImageBinding,
-        msg: ImageMessageBean
+        msg: ImageMessageBean,
+        onItemClickListener: OnItemClickListener?
     ) {
         binding.ivImg.onClick {
             it.context.getActivity()?.let { act ->

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

@@ -51,7 +51,8 @@ class SoundMessageViewHolder(
 
     override fun initView(
         binding: LayoutSessionMessageSoundBinding,
-        msg: SoundMessageBean
+        msg: SoundMessageBean,
+        onItemClickListener: OnItemClickListener?
     ) {
         val durationStr = formatSecondsTo00(max(msg.getDuration(), 1)) //音频时间最小展示1s
         resetTimerStatus(binding, durationStr)
@@ -66,7 +67,6 @@ class SoundMessageViewHolder(
         binding: LayoutSessionMessageSoundBinding,
         msg: SoundMessageBean
     ) {
-        initView(binding, msg)
         setMessageTime(binding, msg)
         //播放进度颜色
         binding.vPlayView.setColor(
@@ -80,7 +80,6 @@ class SoundMessageViewHolder(
         binding: LayoutSessionMessageSoundBinding,
         msg: SoundMessageBean
     ) {
-        initView(binding, msg)
         setMessageTime(binding, msg)
         //播放进度颜色
         binding.vPlayView.setColor(

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

@@ -3,6 +3,7 @@ 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.frame.util.onClick
 import com.adealink.weparty.im.R
 import com.adealink.weparty.im.databinding.LayoutSessionMessageBaseBinding
 import com.adealink.weparty.im.databinding.LayoutSessionMessageTextBinding
@@ -19,6 +20,17 @@ class TextMessageViewHolder(
     rootBinding: LayoutSessionMessageBaseBinding,
     binding: LayoutSessionMessageTextBinding
 ) : MessageViewHolder<TextMessageBean, LayoutSessionMessageTextBinding>(rootBinding, binding) {
+
+    override fun initView(
+        binding: LayoutSessionMessageTextBinding,
+        msg: TextMessageBean,
+        onItemClickListener: OnItemClickListener?
+    ) {
+        binding.tvText.onClick {
+            onItemClickListener?.onMessageClick(binding.root, msg)
+        }
+    }
+
     override fun onBindSelfMessage(
         binding: LayoutSessionMessageTextBinding,
         msg: TextMessageBean

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

@@ -40,6 +40,7 @@ class SessionBottomAudioComp(
 ) : ViewComponent(lifecycleOwner) {
 
     companion object {
+        private const val TAG = "SessionBottomAudioComp"
         private const val MIN_VOICE_DB = 2f
     }
 
@@ -73,6 +74,7 @@ class SessionBottomAudioComp(
          * transition to [InputState.STATE_AUDIO_INPUT]
          */
         inputViewModel.registerTransaction(
+            TAG,
             currentState = InputState.STATE_SOFT_INPUT,
             action = InputAction.CLICK_AUDIO,
             nextState = InputState.STATE_AUDIO_INPUT
@@ -80,6 +82,7 @@ class SessionBottomAudioComp(
             startAudioRecord()
         }
         inputViewModel.registerTransaction(
+            TAG,
             currentState = InputState.STATE_EMOJI_INPUT,
             action = InputAction.CLICK_AUDIO,
             nextState = InputState.STATE_AUDIO_INPUT
@@ -87,6 +90,7 @@ class SessionBottomAudioComp(
             startAudioRecord()
         }
         inputViewModel.registerTransaction(
+            TAG,
             currentState = InputState.STATE_NONE,
             action = InputAction.CLICK_AUDIO,
             nextState = InputState.STATE_AUDIO_INPUT
@@ -96,6 +100,7 @@ class SessionBottomAudioComp(
 
 
         inputViewModel.registerTransaction(
+            TAG,
             currentState = InputState.STATE_AUDIO_INPUT,
             action = InputAction.CLICK_AUDIO,
             nextState = InputState.STATE_NONE
@@ -103,6 +108,7 @@ class SessionBottomAudioComp(
             stopAudioRecord()
         }
         inputViewModel.registerTransaction(
+            TAG,
             currentState = InputState.STATE_AUDIO_INPUT,
             action = InputAction.CANCEL_AUDIO,
             nextState = InputState.STATE_NONE

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

@@ -48,6 +48,10 @@ class SessionBottomInputComp(
     val presenter: ChatPresenter? = null
 ) : ViewComponent(lifecycleOwner) {
 
+    companion object{
+        private const val TAG = "SessionBottomInputComp"
+    }
+
     private lateinit var takePhotoComp: TakePhotoComp
     private lateinit var takeFromAlbumComp: TakeFromAlbumComp
 
@@ -194,6 +198,7 @@ class SessionBottomInputComp(
          *  transition to {@link InputState.STATE_EMOJI_INPUT}
          */
         sessionInputViewModel.registerTransaction(
+            TAG,
             currentState = InputState.STATE_SOFT_INPUT,
             action = InputAction.CLICK_EMOJI_BUTTON,
             nextState = InputState.STATE_EMOJI_INPUT
@@ -203,6 +208,7 @@ class SessionBottomInputComp(
             inputBar.etInputMessage.requestFocus()
         }
         sessionInputViewModel.registerTransaction(
+            TAG,
             currentState = InputState.STATE_NONE,
             action = InputAction.CLICK_EMOJI_BUTTON,
             nextState = InputState.STATE_EMOJI_INPUT
@@ -211,6 +217,7 @@ class SessionBottomInputComp(
         }
 
         sessionInputViewModel.registerTransaction(
+            TAG,
             currentState = InputState.STATE_EMOJI_INPUT,
             action = InputAction.EMPTY_CLICKED,
             nextState = InputState.STATE_NONE
@@ -219,6 +226,16 @@ class SessionBottomInputComp(
         }
 
         sessionInputViewModel.registerTransaction(
+            TAG,
+            currentState = InputState.STATE_NONE,
+            action = InputAction.CLICK_INPUT,
+            nextState = InputState.STATE_SOFT_INPUT
+        ).event.observe(viewLifecycleOwner) {
+            showSoftInput()
+        }
+
+        sessionInputViewModel.registerTransaction(
+            TAG,
             currentState = InputState.STATE_EMOJI_INPUT,
             action = InputAction.CLICK_INPUT,
             nextState = InputState.STATE_SOFT_INPUT
@@ -228,6 +245,7 @@ class SessionBottomInputComp(
         }
 
         sessionInputViewModel.registerTransaction(
+            TAG,
             currentState = InputState.STATE_EMOJI_INPUT,
             action = InputAction.CLICK_EMOJI_BUTTON,
             nextState = InputState.STATE_SOFT_INPUT
@@ -237,6 +255,7 @@ class SessionBottomInputComp(
         }
 
         sessionInputViewModel.registerTransaction(
+            TAG,
             currentState = InputState.STATE_EMOJI_INPUT,
             action = InputAction.EMPTY_CLICKED,
             nextState = InputState.STATE_NONE
@@ -245,6 +264,7 @@ class SessionBottomInputComp(
         }
 
         sessionInputViewModel.registerTransaction(
+            TAG,
             currentState = InputState.STATE_SOFT_INPUT,
             action = InputAction.EMPTY_CLICKED,
             nextState = InputState.STATE_NONE
@@ -253,6 +273,7 @@ class SessionBottomInputComp(
         }
 
         sessionInputViewModel.registerTransaction(
+            TAG,
             currentState = InputState.STATE_AUDIO_INPUT,
             action = InputAction.EMPTY_CLICKED,
             nextState = InputState.STATE_NONE
@@ -260,7 +281,6 @@ class SessionBottomInputComp(
             resetInput()
         }
 
-        sessionInputViewModel.inputStateLD
         sessionInputViewModel.inputStateLD.observe(viewLifecycleOwner) { state ->
             when (state.currentState) {
                 InputState.STATE_SOFT_INPUT,

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

@@ -1,29 +1,113 @@
 package com.adealink.weparty.im.session.comp
 
 import androidx.lifecycle.LifecycleOwner
+import com.adealink.frame.base.fastLazy
 import com.adealink.frame.mvvm.view.ViewComponent
+import com.adealink.frame.router.Router
 import com.adealink.frame.util.onClick
+import com.adealink.weparty.commonui.ext.gone
+import com.adealink.weparty.commonui.ext.show
+import com.adealink.weparty.commonui.toast.util.showFailedToast
 import com.adealink.weparty.im.databinding.LayoutSessionTopBarBinding
+import com.adealink.weparty.im.session.dialog.SessionSettingDialog
+import com.adealink.weparty.module.profile.Profile
+import com.adealink.weparty.module.profile.ProfileModule
+import com.adealink.weparty.module.profile.data.UserInfo
+import com.adealink.weparty.module.profile.data.isFollowed
+import com.tencent.qcloud.tuikit.tuichat.bean.ChatInfo
 
 class SessionTopComp(
     lifecycleOwner: LifecycleOwner,
-    val topBar: LayoutSessionTopBarBinding
+    val topBar: LayoutSessionTopBarBinding,
+    val chatInfo: ChatInfo?
 ) : ViewComponent(lifecycleOwner) {
 
+    private val profileViewModel by fastLazy { ProfileModule.getProfileViewModel(viewModelStoreOwner) }
+    private val followViewModel by fastLazy { ProfileModule.getFollowViewModel(viewModelStoreOwner) }
+
     override fun onCreate() {
         super.onCreate()
         initView()
         observeViewModel()
+        loadData()
     }
 
     private fun initView() {
         topBar.ivBack.onClick {
             activity?.finish()
         }
+        topBar.ivAvatar.onClick {
+            goProfile()
+        }
+        topBar.tvUserName.onClick {
+            goProfile()
+        }
+        topBar.tvUserOnline.onClick {
+            goProfile()
+        }
+        topBar.btnFollow.onClick {
+            clickFollow()
+        }
+        topBar.btnSetting.onClick {
+            clickSetting()
+        }
     }
 
     private fun observeViewModel() {
+        profileViewModel?.userInfoLD?.observe(viewLifecycleOwner) {
+            updateUserInfo(it)
+        }
+
+        followViewModel?.isFollowLD?.observe(viewLifecycleOwner) {
+            val uid = chatInfo?.id ?: return@observe
+            updateFollow(it[uid]?.isFollowed() ?: false)
+        }
+    }
+
+    private fun loadData() {
+        val chatInfo = chatInfo ?: return
+        updateUserInfo(profileViewModel?.getUserInfoBy(chatInfo.id))
+    }
+
+    override fun onResume() {
+        super.onResume()
+        chatInfo?.id?.let { uid ->
+            profileViewModel?.pullUserInfoBy(uid, false)
+            followViewModel?.isFollow(uid)
+        }
+    }
+
+    private fun updateUserInfo(userInfo: UserInfo?) {
+        topBar.ivAvatar.setImageUrl(userInfo?.avatar)
+        topBar.tvUserName.text = userInfo?.nickName
+    }
+
+    private fun updateFollow(isFollowed: Boolean) {
+        if (isFollowed) {
+            topBar.btnFollow.gone()
+        } else {
+            topBar.btnFollow.show()
+        }
+    }
 
+    private fun goProfile() {
+        val uid = chatInfo?.id ?: return
+        val act = activity ?: return
+        Router.build(act, Profile.UserProfile.PATH)
+            .putExtra(Profile.Common.EXTRA_UID, uid)
+            .start()
     }
 
+    private fun clickFollow() {
+        val uid = chatInfo?.id ?: return
+        followViewModel?.follow(uid)?.observe(viewLifecycleOwner) {
+            showFailedToast(it)
+        }
+    }
+
+    private fun clickSetting() {
+        SessionSettingDialog().show(fragmentManager, "SessionSettingDialog")
+    }
+
+
 }

+ 5 - 3
module/im/src/main/java/com/adealink/weparty/im/session/comp/input/SessionInputMachine.kt

@@ -46,6 +46,7 @@ class SessionInputMachine(
     }
 
     fun registerTransaction(
+        page:String,
         currentState: InputState,
         action: InputAction,
         nextState: InputState
@@ -54,7 +55,7 @@ class SessionInputMachine(
             it.currentState == currentState && it.action == action && it.nextState == nextState
         }
         if (transaction == null) {
-            val newTransaction = InputMachineTransaction(currentState, action, nextState)
+            val newTransaction = InputMachineTransaction(page, currentState, action, nextState)
             transactionList.add(newTransaction)
             return newTransaction
         } else {
@@ -69,7 +70,7 @@ class SessionInputMachine(
             if (transaction.currentState == currentState && transaction.action == action) {
                 Log.d(
                     TAG_IM_INPUT_FLOW,
-                    "SessionInputMachine, action:$action, $currentState => ${transaction.nextState}"
+                    "SessionInputMachine, ${transaction.page}, action:$action, $currentState => ${transaction.nextState}"
                 )
                 transaction.runEvent()
                 pendingTransactions.add(transaction)
@@ -101,7 +102,7 @@ class SessionInputMachine(
         } else {
             Log.w(
                 TAG_IM_INPUT_FLOW,
-                "SessionInputMachine, action:$action can not find nextState"
+                "SessionInputMachine, action:$action can not find nextState(currentState:$currentState)"
             )
         }
 //        for (transaction in pendingTransactions) {
@@ -122,6 +123,7 @@ class SessionInputMachine(
 }
 
 data class InputMachineTransaction(
+    val page: String,
     val currentState: InputState,
     val action: InputAction,
     val nextState: InputState,

+ 3 - 1
module/im/src/main/java/com/adealink/weparty/im/session/comp/viewmodel/SessionInputViewModel.kt

@@ -38,6 +38,7 @@ class SessionInputViewModel : BaseViewModel() {
                  * transition to [InputState.STATE_NONE]
                  */
                 InputMachineTransaction(
+                    "",
                     currentState = InputState.STATE_NONE,
                     action = InputAction.EMPTY_CLICKED,
                     nextState = InputState.STATE_NONE
@@ -60,11 +61,12 @@ class SessionInputViewModel : BaseViewModel() {
     }
 
     fun registerTransaction(
+        page: String,
         currentState: InputState,
         action: InputAction,
         nextState: InputState
     ): InputMachineTransaction {
-        return stateMachine.registerTransaction(currentState, action, nextState)
+        return stateMachine.registerTransaction(page, currentState, action, nextState)
     }
 
 

+ 53 - 0
module/im/src/main/java/com/adealink/weparty/im/session/dialog/SessionSettingDialog.kt

@@ -0,0 +1,53 @@
+package com.adealink.weparty.im.session.dialog
+
+import android.widget.CompoundButton
+import androidx.fragment.app.activityViewModels
+import com.adealink.frame.base.fastLazy
+import com.adealink.frame.mvvm.view.viewBinding
+import com.adealink.frame.util.onClick
+import com.adealink.weparty.commonui.widget.BottomDialogFragment
+import com.adealink.weparty.im.R
+import com.adealink.weparty.im.databinding.DialogSessionSettingBinding
+import com.adealink.weparty.im.session.viewmodel.SessionViewModel
+import com.adealink.weparty.im.viewmodel.IMViewModelFactory
+import com.adealink.weparty.module.profile.ProfileModule
+
+class SessionSettingDialog : BottomDialogFragment(R.layout.dialog_session_setting) {
+
+    private val binding by viewBinding(DialogSessionSettingBinding::bind)
+
+    private val sessionViewModel by activityViewModels<SessionViewModel> { IMViewModelFactory() }
+    private val profileViewModel by fastLazy { ProfileModule.getProfileViewModel(requireActivity()) }
+
+    override fun initViews() {
+        super.initViews()
+        binding.vMuteSwitch.setOnCheckedChangeListener(object: CompoundButton.OnCheckedChangeListener{
+            override fun onCheckedChanged(
+                buttonView: CompoundButton?,
+                isChecked: Boolean
+            ) {
+                sessionViewModel.setSessionMute(isChecked)
+            }
+        })
+        binding.clMark.onClick {
+
+        }
+        binding.clBlack.onClick {
+
+        }
+        binding.clReport.onClick {
+
+        }
+    }
+
+    override fun observeViewModel() {
+        super.observeViewModel()
+        sessionViewModel.isSessionMuteLD.observe(viewLifecycleOwner){
+            updateMute(it)
+        }
+    }
+
+    private fun updateMute(mute:Boolean){
+        binding.vMuteSwitch.isChecked = mute
+    }
+}

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

@@ -1,14 +1,67 @@
 package com.adealink.weparty.im.session.viewmodel
 
+import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
+import com.adealink.frame.base.Rlt
+import com.adealink.frame.mvvm.livedata.OnceMutableLiveData
 import com.adealink.frame.mvvm.viewmodel.BaseViewModel
-import com.adealink.weparty.module.profile.data.UserInfo
+import com.adealink.weparty.im.manager.session.sessionManager
+import com.tencent.qcloud.tuikit.tuichat.bean.ChatInfo
+import kotlinx.coroutines.launch
 
 class SessionViewModel : BaseViewModel() {
 
-    val senderLD = MutableLiveData<UserInfo>()
+    private var chatInfo: ChatInfo? = null
 
-    fun setSession(chatID: String) {
+    fun loadChatInfo(chatInfo: ChatInfo?) {
+        this@SessionViewModel.chatInfo = chatInfo
+        viewModelScope.launch {
+            isSessionMute()
 
+        }
     }
+
+    val isSessionMuteLD = MutableLiveData<Boolean>()
+
+    //会话免打扰
+    fun isSessionMute(): LiveData<Boolean> {
+        val liveData = OnceMutableLiveData<Boolean>()
+        viewModelScope.launch {
+            val chatInfo = chatInfo ?: return@launch
+            val uid = chatInfo.id
+            val rlt = sessionManager.isSessionMute(listOf(uid), chatInfo.type)
+            when (rlt) {
+                is Rlt.Failed -> {
+                }
+
+                is Rlt.Success -> {
+                    isSessionMuteLD.send(rlt.data[uid] ?: false)
+                    liveData.send(rlt.data[uid] ?: false)
+                }
+            }
+        }
+        return liveData
+    }
+
+    fun setSessionMute(mute: Boolean): LiveData<Rlt<Any>> {
+        val liveData = OnceMutableLiveData<Rlt<Any>>()
+        viewModelScope.launch {
+            val chatInfo = chatInfo ?: return@launch
+            val uid = chatInfo.id
+            val rlt = sessionManager.setSessionMute(listOf(uid), chatInfo.type, mute)
+            when (rlt) {
+                is Rlt.Failed -> {
+                    //Ntd.
+                }
+
+                is Rlt.Success -> {
+                    isSessionMuteLD.send(mute)
+                }
+            }
+            liveData.send(rlt)
+        }
+        return liveData
+    }
+
+
 }

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


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


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


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


+ 195 - 0
module/im/src/main/res/layout/dialog_session_setting.xml

@@ -0,0 +1,195 @@
+<?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="@drawable/common_bottom_dialog_bg"
+    android:paddingHorizontal="16dp"
+    android:paddingTop="10dp"
+    android:paddingBottom="12dp">
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/cl_mute"
+        android:layout_width="match_parent"
+        android:layout_height="52dp"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent">
+
+        <androidx.appcompat.widget.AppCompatImageView
+            android:layout_width="18dp"
+            android:layout_height="18dp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:srcCompat="@drawable/im_session_setting_mute_ic" />
+
+        <androidx.appcompat.widget.AppCompatTextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="28dp"
+            android:ellipsize="end"
+            android:includeFontPadding="false"
+            android:singleLine="true"
+            android:text="@string/im_setting_mute"
+            android:textColor="@color/color_FF1D2129"
+            android:textSize="14sp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent" />
+
+        <androidx.appcompat.widget.SwitchCompat
+            android:id="@+id/v_mute_switch"
+            style="@style/CommonSwitch"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toTopOf="parent" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+    <View
+        android:layout_width="0dp"
+        android:layout_height="1dp"
+        android:background="@color/color_FFF2F3F5"
+        app:layout_constraintBottom_toBottomOf="@id/cl_mute"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent" />
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/cl_mark"
+        android:layout_width="match_parent"
+        android:layout_height="52dp"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/cl_mute">
+
+        <androidx.appcompat.widget.AppCompatImageView
+            android:layout_width="18dp"
+            android:layout_height="18dp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:srcCompat="@drawable/im_session_setting_mark_ic" />
+
+        <androidx.appcompat.widget.AppCompatTextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="28dp"
+            android:ellipsize="end"
+            android:includeFontPadding="false"
+            android:singleLine="true"
+            android:text="@string/im_setting_mark"
+            android:textColor="@color/color_FF1D2129"
+            android:textSize="14sp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent" />
+
+        <androidx.appcompat.widget.AppCompatImageView
+            android:layout_width="16dp"
+            android:layout_height="16dp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:srcCompat="@drawable/common_go_ic" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+    <View
+        android:layout_width="0dp"
+        android:layout_height="1dp"
+        android:background="@color/color_FFF2F3F5"
+        app:layout_constraintBottom_toBottomOf="@id/cl_mark"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent" />
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/cl_black"
+        android:layout_width="match_parent"
+        android:layout_height="52dp"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/cl_mark">
+
+        <androidx.appcompat.widget.AppCompatImageView
+            android:layout_width="18dp"
+            android:layout_height="18dp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:srcCompat="@drawable/im_session_setting_black_ic" />
+
+        <androidx.appcompat.widget.AppCompatTextView
+            android:id="@+id/tv_black"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="28dp"
+            android:ellipsize="end"
+            android:includeFontPadding="false"
+            android:singleLine="true"
+            android:text="@string/im_setting_black"
+            android:textColor="@color/color_FF1D2129"
+            android:textSize="14sp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent" />
+
+        <androidx.appcompat.widget.AppCompatImageView
+            android:layout_width="16dp"
+            android:layout_height="16dp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:srcCompat="@drawable/common_go_ic" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+    <View
+        android:layout_width="0dp"
+        android:layout_height="1dp"
+        android:background="@color/color_FFF2F3F5"
+        app:layout_constraintBottom_toBottomOf="@id/cl_black"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent" />
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/cl_report"
+        android:layout_width="match_parent"
+        android:layout_height="52dp"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/cl_black">
+
+        <androidx.appcompat.widget.AppCompatImageView
+            android:layout_width="18dp"
+            android:layout_height="18dp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:srcCompat="@drawable/im_session_setting_report_ic" />
+
+        <androidx.appcompat.widget.AppCompatTextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="28dp"
+            android:ellipsize="end"
+            android:includeFontPadding="false"
+            android:singleLine="true"
+            android:text="@string/im_setting_report"
+            android:textColor="@color/color_FF1D2129"
+            android:textSize="14sp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent" />
+
+        <androidx.appcompat.widget.AppCompatImageView
+            android:layout_width="16dp"
+            android:layout_height="16dp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:srcCompat="@drawable/common_go_ic" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>

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

@@ -36,7 +36,7 @@
             android:layout_width="0dp"
             android:layout_height="wrap_content"
             android:layout_marginHorizontal="6dp"
-            android:layout_marginVertical="8dp"
+            android:paddingVertical="8dp"
             android:background="@null"
             android:gravity="start|center_vertical"
             android:hint="@string/im_session_input_message"

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

@@ -72,23 +72,27 @@
             android:layout_width="24dp"
             android:layout_height="24dp"
             android:layout_marginEnd="10dp"
+            android:visibility="gone"
             app:layout_constraintBottom_toBottomOf="parent"
             app:layout_constraintEnd_toStartOf="@id/btn_call"
             app:layout_constraintTop_toTopOf="parent"
-            app:srcCompat="@drawable/im_session_follow_ic" />
+            app:srcCompat="@drawable/im_session_follow_ic"
+            tools:visibility="visible" />
 
+        <!--通话功能延后-->
         <androidx.appcompat.widget.AppCompatImageView
             android:id="@+id/btn_call"
             android:layout_width="24dp"
             android:layout_height="24dp"
             android:layout_marginEnd="10dp"
+            android:visibility="gone"
             app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintEnd_toStartOf="@id/btn_more"
+            app:layout_constraintEnd_toStartOf="@id/btn_setting"
             app:layout_constraintTop_toTopOf="parent"
             app:srcCompat="@drawable/im_session_call_ic" />
 
         <androidx.appcompat.widget.AppCompatImageView
-            android:id="@+id/btn_more"
+            android:id="@+id/btn_setting"
             android:layout_width="24dp"
             android:layout_height="24dp"
             app:layout_constraintBottom_toBottomOf="parent"

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

@@ -12,4 +12,8 @@
     <string name="im_sound_play_tip_for_voice_not_download">Voice file not downloaded.</string>
     <string name="im_official_view_now">View now</string>
     <string name="im_official_session_title">System Message</string>
+    <string name="im_setting_mute">消息免打扰</string>
+    <string name="im_setting_mark">备注</string>
+    <string name="im_setting_black">拉黑</string>
+    <string name="im_setting_report">举报</string>
 </resources>

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

@@ -29,5 +29,10 @@
             android:screenOrientation="portrait"
             android:theme="@style/AppTheme"
             android:windowSoftInputMode="adjustResize|adjustPan" />
+        <activity
+            android:name=".report.ReportActivity"
+            android:screenOrientation="portrait"
+            android:theme="@style/AppTheme"
+            android:windowSoftInputMode="adjustResize|adjustPan" />
     </application>
 </manifest>

+ 190 - 0
module/setting/src/main/java/com/adealink/weparty/setting/report/ReportActivity.kt

@@ -0,0 +1,190 @@
+package com.adealink.weparty.setting.report
+
+import android.annotation.SuppressLint
+import android.text.Editable
+import android.text.TextWatcher
+import androidx.activity.viewModels
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.view.updateLayoutParams
+import androidx.recyclerview.widget.GridLayoutManager
+import com.adealink.frame.aab.util.getCompatDimension
+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.naviBarHeight
+import com.adealink.frame.util.onClick
+import com.adealink.frame.util.statusBarHeight
+import com.adealink.weparty.commonui.BaseActivity
+import com.adealink.weparty.commonui.ext.dp
+import com.adealink.weparty.commonui.recycleview.adapter.MultiTypeListAdapter
+import com.adealink.weparty.commonui.recycleview.itemdecoration.GridSpacingItemDecoration
+import com.adealink.weparty.commonui.toast.util.showToast
+import com.adealink.weparty.imageselect.comp.TakeFromAlbumComp
+import com.adealink.weparty.module.setting.Setting
+import com.adealink.weparty.setting.R
+import com.adealink.weparty.setting.databinding.ActivityReportBinding
+import com.adealink.weparty.setting.helpcenter.adapter.AddPictureItemViewBinder
+import com.adealink.weparty.setting.helpcenter.adapter.PictureItemViewBinder
+import com.adealink.weparty.setting.helpcenter.data.AddPictureItemData
+import com.adealink.weparty.setting.helpcenter.data.BasePictureItemData
+import com.adealink.weparty.setting.helpcenter.data.PictureData
+import com.adealink.weparty.setting.report.viewmodel.ReportViewModel
+import com.adealink.weparty.util.goImagePreviewActivity
+import com.qmuiteam.qmui.widget.util.QMUIKeyboardHelper
+import com.adealink.weparty.R as APP_R
+
+@RouterUri(path = [Setting.Report.PATH], desc = "举报页面")
+class ReportActivity : BaseActivity() {
+
+    companion object {
+        private const val MAX_INPUT_COUNT = 200
+        private const val PICTURE_SPAN = 3
+    }
+
+    @BindExtra(Setting.Report.EXTRA_UID)
+    var uid: String? = null
+
+    private val binding by viewBinding(ActivityReportBinding::inflate)
+
+    private val pictureAdapter by fastLazy { MultiTypeListAdapter<BasePictureItemData>() }
+
+    private val viewModel by viewModels<ReportViewModel>()
+
+    private lateinit var takeFromAlbumComp: TakeFromAlbumComp
+
+    override fun onBeforeCreate() {
+        super.onBeforeCreate()
+        Router.bind(this)
+    }
+
+    override fun initViews() {
+        super.initViews()
+        setContentView(binding.root)
+        val statusBarHeight = statusBarHeight()
+        val naviBarHeight = naviBarHeight()
+        binding.topBar.setPadding(0, statusBarHeight, 0, 0)
+        binding.topBar.updateLayoutParams<ConstraintLayout.LayoutParams> {
+            height = statusBarHeight + getCompatDimension(APP_R.dimen.common_top_bar_height).toInt()
+        }
+        binding.btnSubmit.updateLayoutParams<ConstraintLayout.LayoutParams> {
+            bottomMargin = naviBarHeight + 24.dp()
+        }
+        binding.clContent.setPadding(
+            0, 0, 0,
+            getCompatDimension(APP_R.dimen.common_button_height).toInt() + naviBarHeight + 48.dp()
+        )
+
+        binding.etInput.addTextChangedListener(object : TextWatcher {
+            override fun beforeTextChanged(
+                s: CharSequence?,
+                start: Int,
+                count: Int,
+                after: Int
+            ) {
+            }
+
+            override fun onTextChanged(
+                s: CharSequence?,
+                start: Int,
+                before: Int,
+                count: Int
+            ) {
+            }
+
+            @SuppressLint("SetTextI18n")
+            override fun afterTextChanged(s: Editable?) {
+                val input = s?.trim()?.toString()
+                if (input.isNullOrEmpty()) {
+                    binding.btnSubmit.isEnabled = false
+                    binding.tvInputCount.text = "0/${MAX_INPUT_COUNT}"
+                } else {
+                    binding.btnSubmit.isEnabled = true
+                    binding.tvInputCount.text = "${input.length}/${MAX_INPUT_COUNT}"
+                }
+            }
+        })
+
+
+        pictureAdapter.register(
+            AddPictureItemViewBinder {
+                addPicture()
+            }
+        )
+        pictureAdapter.register(
+            PictureItemViewBinder(
+                onItemClick = {
+                    previewPicture(it.data)
+                },
+                onDeleteClick = {
+                    deletePicture(it.data)
+                }
+            ))
+        binding.rvPicture.adapter = pictureAdapter
+        binding.rvPicture.layoutManager = GridLayoutManager(
+            this,
+            PICTURE_SPAN
+        )
+        binding.rvPicture.addItemDecoration(
+            GridSpacingItemDecoration(PICTURE_SPAN, 4.dp(), 4.dp(), false)
+        )
+
+        binding.btnSubmit.onClick {
+            submit()
+        }
+    }
+
+    override fun observeViewModel() {
+        super.observeViewModel()
+        viewModel.pictureItemListLD.observe(this) {
+            pictureAdapter.submitList(it)
+        }
+    }
+
+    override fun loadData() {
+        super.loadData()
+        pictureAdapter.submitList(
+            listOf(AddPictureItemData)
+        )
+    }
+
+    override fun initComponents() {
+        super.initComponents()
+        TakeFromAlbumComp(this, null) { path, uri ->
+            viewModel.addPicture(path, uri)
+        }.also {
+            takeFromAlbumComp = it
+        }.attach()
+    }
+
+    private fun addPicture() {
+        takeFromAlbumComp.takeFromAlbum()
+    }
+
+    private fun previewPicture(data: PictureData) {
+        if (data.uri.isNullOrEmpty()) {
+            return
+        }
+        goImagePreviewActivity(
+            this,
+            arrayListOf(data.uri)
+        )
+    }
+
+    private fun deletePicture(data: PictureData) {
+        viewModel.removePicture(data)
+    }
+
+    private fun submit() {
+        viewModel.submit().observe(this) {
+            showToast(R.string.setting_report_submit_success)
+        }
+    }
+
+    override fun onDestroy() {
+        binding.etInput.clearFocus()
+        QMUIKeyboardHelper.hideKeyboard(binding.etInput)
+        super.onDestroy()
+    }
+}

+ 56 - 0
module/setting/src/main/java/com/adealink/weparty/setting/report/viewmodel/ReportViewModel.kt

@@ -0,0 +1,56 @@
+package com.adealink.weparty.setting.report.viewmodel
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import com.adealink.frame.base.Rlt
+import com.adealink.frame.mvvm.livedata.OnceMutableLiveData
+import com.adealink.frame.mvvm.viewmodel.BaseViewModel
+import com.adealink.weparty.setting.helpcenter.data.AddPictureItemData
+import com.adealink.weparty.setting.helpcenter.data.BasePictureItemData
+import com.adealink.weparty.setting.helpcenter.data.PictureData
+import com.adealink.weparty.setting.helpcenter.data.PictureItemData
+import kotlinx.coroutines.launch
+
+class ReportViewModel : BaseViewModel() {
+
+    val pictureList = mutableListOf<PictureData>()
+
+    val pictureItemListLD = MutableLiveData<List<BasePictureItemData>>()
+
+    fun addPicture(path: String?, uri: String?) {
+        viewModelScope.launch {
+            pictureList.add(PictureData(path, uri))
+            onPictureListChanged()
+        }
+    }
+
+    fun removePicture(data: PictureData) {
+        viewModelScope.launch {
+            pictureList.remove(data)
+            onPictureListChanged()
+        }
+    }
+
+    private fun onPictureListChanged() {
+        val itemList = mutableListOf<BasePictureItemData>()
+        itemList.addAll(
+            pictureList.map {
+                PictureItemData(it)
+            }
+        )
+        //小于9张图片,可以继续添加图片
+        if (pictureList.size < 9) {
+            itemList.add(AddPictureItemData)
+        }
+        pictureItemListLD.send(itemList)
+    }
+
+    fun submit(): LiveData<Rlt<Any>> {
+        val liveData = OnceMutableLiveData<Rlt<Any>>()
+        viewModelScope.launch {
+            liveData.send(Rlt.Success(Any()))
+        }
+        return liveData
+    }
+
+}

+ 1 - 2
module/setting/src/main/res/layout/activity_help_center.xml

@@ -37,12 +37,11 @@
 
                 <androidx.appcompat.widget.AppCompatTextView
                     android:id="@+id/tv_input_title"
-                    android:layout_width="wrap_content"
+                    android:layout_width="0dp"
                     android:layout_height="wrap_content"
                     android:ellipsize="end"
                     android:gravity="start|center"
                     android:includeFontPadding="false"
-                    android:singleLine="true"
                     android:text="@string/setting_help_input_title"
                     android:textColor="@color/color_FF1D2129"
                     android:textSize="14sp"

+ 171 - 0
module/setting/src/main/res/layout/activity_report.xml

@@ -0,0 +1,171 @@
+<?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="match_parent">
+
+    <com.adealink.weparty.commonui.widget.CommonTopBar
+        android:id="@+id/top_bar"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/common_top_bar_height"
+        app:layout_constraintTop_toTopOf="parent"
+        app:top_bar_title="@string/common_report" />
+
+    <androidx.core.widget.NestedScrollView
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:background="@color/color_FFF1F2F5"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/top_bar">
+
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:id="@+id/cl_content"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content">
+
+            <androidx.constraintlayout.widget.ConstraintLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginHorizontal="16dp"
+                android:layout_marginTop="16dp"
+                android:background="@drawable/common_white_radius_16_bg"
+                android:padding="16dp"
+                app:layout_constraintTop_toTopOf="parent">
+
+                <androidx.appcompat.widget.AppCompatTextView
+                    android:id="@+id/tv_input_title"
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:ellipsize="end"
+                    android:gravity="start|center"
+                    android:includeFontPadding="false"
+                    android:text="@string/setting_report_content"
+                    android:textColor="@color/color_FF1D2129"
+                    android:textSize="14sp"
+                    android:textStyle="bold"
+                    app:layout_constraintEnd_toEndOf="parent"
+                    app:layout_constraintStart_toStartOf="parent"
+                    app:layout_constraintTop_toTopOf="parent" />
+
+                <androidx.constraintlayout.widget.ConstraintLayout
+                    android:id="@+id/v_input"
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:layout_marginTop="8dp"
+                    android:background="@drawable/setting_input_edit_bg"
+                    android:paddingHorizontal="12dp"
+                    android:paddingVertical="8dp"
+                    app:layout_constrainedHeight="true"
+                    app:layout_constraintEnd_toEndOf="parent"
+                    app:layout_constraintStart_toStartOf="parent"
+                    app:layout_constraintTop_toBottomOf="@id/tv_input_title">
+
+                    <androidx.core.widget.NestedScrollView
+                        android:id="@+id/sv_et_input"
+                        android:layout_width="0dp"
+                        android:layout_height="wrap_content"
+                        app:layout_constrainedHeight="true"
+                        app:layout_constraintBottom_toTopOf="@id/tv_input_count"
+                        app:layout_constraintEnd_toEndOf="parent"
+                        app:layout_constraintHeight_max="250dp"
+                        app:layout_constraintHeight_min="150dp"
+                        app:layout_constraintStart_toStartOf="parent"
+                        app:layout_constraintTop_toTopOf="parent"
+                        app:layout_constraintVertical_chainStyle="spread_inside">
+
+                        <androidx.appcompat.widget.AppCompatEditText
+                            android:id="@+id/et_input"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:background="@null"
+                            android:gravity="start|top"
+                            android:hint="@string/setting_help_input_desc"
+                            android:includeFontPadding="false"
+                            android:maxLength="200"
+                            android:textColor="@color/color_FF1D2129"
+                            android:textColorHint="@color/color_FFCDCFD9"
+                            tools:text="asdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfkasdf;asdjfka;lsjdfk" />
+
+                    </androidx.core.widget.NestedScrollView>
+
+                    <androidx.appcompat.widget.AppCompatTextView
+                        android:id="@+id/tv_input_count"
+                        android:layout_width="0dp"
+                        android:layout_height="wrap_content"
+                        android:gravity="center|end"
+                        android:includeFontPadding="false"
+                        android:singleLine="true"
+                        android:text="0/800"
+                        android:textColor="@color/color_FFCDCFD9"
+                        android:textSize="14sp"
+                        app:layout_constraintBottom_toBottomOf="parent"
+                        app:layout_constraintEnd_toEndOf="parent"
+                        app:layout_constraintStart_toStartOf="parent"
+                        app:layout_constraintTop_toBottomOf="@id/sv_et_input"
+                        tools:ignore="HardcodedText" />
+
+                </androidx.constraintlayout.widget.ConstraintLayout>
+
+                <androidx.appcompat.widget.AppCompatTextView
+                    android:id="@+id/tv_picture_title"
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:layout_marginTop="16dp"
+                    android:ellipsize="end"
+                    android:includeFontPadding="false"
+                    android:singleLine="true"
+                    android:text="@string/setting_report_evidence"
+                    android:textColor="@color/color_FF1D2129"
+                    android:textSize="14sp"
+                    android:textStyle="bold"
+                    app:layout_constraintEnd_toEndOf="parent"
+                    app:layout_constraintStart_toStartOf="parent"
+                    app:layout_constraintTop_toBottomOf="@id/v_input" />
+
+                <androidx.appcompat.widget.AppCompatTextView
+                    android:id="@+id/tv_picture_desc"
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:ellipsize="end"
+                    android:includeFontPadding="false"
+                    android:singleLine="true"
+                    android:text="@string/setting_report_evidence_desc"
+                    android:textColor="@color/color_FF1D2129"
+                    android:textSize="12sp"
+                    app:layout_constraintEnd_toEndOf="parent"
+                    app:layout_constraintStart_toStartOf="parent"
+                    app:layout_constraintTop_toBottomOf="@id/tv_picture_title" />
+
+                <androidx.recyclerview.widget.RecyclerView
+                    android:id="@+id/rv_picture"
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:layout_marginTop="8dp"
+                    app:layout_constraintEnd_toEndOf="parent"
+                    app:layout_constraintStart_toStartOf="parent"
+                    app:layout_constraintTop_toBottomOf="@id/tv_picture_desc" />
+
+            </androidx.constraintlayout.widget.ConstraintLayout>
+
+        </androidx.constraintlayout.widget.ConstraintLayout>
+
+    </androidx.core.widget.NestedScrollView>
+
+
+    <com.adealink.weparty.commonui.widget.CommonButton
+        android:id="@+id/btn_submit"
+        android:layout_width="0dp"
+        android:layout_height="@dimen/common_button_height"
+        android:layout_marginHorizontal="20dp"
+        android:layout_marginBottom="@dimen/common_button_margin_bottom"
+        app:text="@string/common_submit"
+        app:textColor="@color/white"
+        app:textSize="16sp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 4 - 0
module/setting/src/main/res/values/strings.xml

@@ -17,5 +17,9 @@
     <string name="setting_clear_cache_confirm_title">清理缓存</string>
     <string name="setting_clear_cache_confirm_desc">清理缓存提示词</string>
     <string name="setting_logout_confirm_title">是否退出登录?</string>
+    <string name="setting_report_content">请填写详细的举报内容</string>
+    <string name="setting_report_evidence">图片证明(最多6张)</string>
+    <string name="setting_report_evidence_desc">上传图片显示你的证据</string>
+    <string name="setting_report_submit_success">您已提交举报,待官方核实情况后会周青进行</string>
 
 </resources>