Просмотр исходного кода

feat: 1v1专项提升数据 (#31)

* feat: af_purchase改成后端上报

* feat: verifyOrder amont字段取小数点位数控制

* feat: 使用私有缓存目录作为FilePath兜底目录

* feat:1v1新增收益,notice声音和震动提示

* feat:1v1今日缘分Dialog

* feat:1v1今日缘分Dialog

* feat:1v1今日缘分Dialog,调整目录

* feat:1v1今日缘分Dialog,调整目录

* feat:1v1今日缘分Dialog,调整目录

* feat:1v1私聊消息提醒Dialog

* feat:1v1私聊消息提醒Dialog

* feat:1v1女性付费信息Push

* feat:1v1女性付费信息Push

* feat:1v1付费消息差异化

* feat:1v1付费消息差异化

* feat:1v1付费未回消息强提示

* feat:1v1快捷回消息

* feat:1v1快捷回消息

* feat:1v1新用户女落地流程优化

* feat:1v1相关动画

* feat:1v1相关动画

* feat:1v1相关动画

* feat:1v1完善UI部分

* feat:1v1 p0部分协议接入

* feat:1v1 p0部分协议接入

* feat:1v1 部分逻辑调整

* feat:1v1 接入p0协议

* feat:1v1 接入p0协议

* feat:1v1 接入p0协议

* feat:1v1 接入p0协议

* bugfix:修复编译问题

* feat:联调

* feat:联调1v1 p0需求点

* feat:1v1付费消息push

* feat:1v1付费消息push

* feat:1v1 快捷回消息

* feat:1v1 快捷回消息

* feat:1v1 新客女Dialog

* bugfix:男生未读消息弹窗提醒,点击女生头像不能跳转到用户资料页

* feat:新用户女落地流程优化

* feat:调整付费消息push样式

* bugfix:im列表,女生收到付款消息,没有展示钻石icon

* bugfix:男用户一登录,马上切走一级tab,有概率在其他一级tab展示推荐女弹窗

* bugfix:男生未读消息弹窗,展示语音通话扣费的消息

* bugfix:男生未读消息弹窗,展示语音通话扣费的消息

* bugfix:男生未读消息弹窗提醒,有时候会堆积展示

* bugfix:男生弹窗顺序,有概率出现未读消息在推荐消息前面

* bugfix:异性推荐Dialog概率不展示在首页的问题

* bugfix:处理付费push文本无法完全展示问题

* bugfix:付费消息提醒限制展示页面

* bugfix:币商不展示消息提醒Dialog

* bugfix:女币商不展示收益tab

* feat:接入p1需求协议

* feat:多语言接入

* bugfix:收益弹窗在阿语状态下,文案内容太靠边

* bugfix:修复未读消息组件偶现崩溃问题ConcurrentModificationException

* bugfix:下拉私聊消息列表,没有重新请求im/batch_get_session_info接口,导致钻石外显不及时

* 修复编译问题

* feat:适配多语言

* bugfix:官方账号显示收益tab

* bugfix:官方账户/币商/官方客服,发来的消息。男性收到后不会触发未读消息弹窗

* bugfix:收益弹窗点击头像,不会跳转个人资料页

* bugfix:男生未读消息弹窗提醒,有时候推了系统推送的消息

* feat:替换接口

* bugfix:在首充页收到付费弹窗socket后再关掉首充页,付费弹窗没有出现 男生未读消息弹窗提醒,有时候推了系统推送的消息

* bugfix:用户处于私聊页面收到新的付费消息,不显示钻石icon,只有重新请求/im/batch_get_session_info接口后才会显示钻石icon

* feat:DialogShowManager优化不允许显示弹窗的Fragment页面列表的逻辑

* feat:付费push适配多语言

* bugfix:男币商账号/官方账号/客服账号,无需弹未读消息弹窗

* feat:实时监听会话变更

* feat:实时监听会话变更

* feat:付费消息通知同个发送人的等待Dialog需要去重

* bugfix:付费消息弹窗出现在star弹窗上面

* bugfix:付费消息弹窗过滤聊天设置引导

* bugfix:app切后台,陌生男发付费消息,女性收到2条push

* bugfix:app切后台,陌生男发付费消息,女性收到2条push

* bugfix:买量男用户,注册成功后,弹完推荐4个女生的弹窗后,没有继续弹窗引导say hi 和 引导去im消息的弹窗

* bugfix:处理已知问题

* bugfix:买量女用户登录没有落地到语音房tab

* bugfix:今日缘分修改自动关闭时长还有新增打招呼toast

* bugfix:适配收益tab付费消息实时监听,以及过滤过期的付费消息不展示push'

* feat:女性收钻的震动提示可以更明显一些

* bugfix:处于与男性用户的私聊页,男性发了一条新的付费消息,等待1分钟后退出私聊页,出现男性用户的付费弹窗

* bugfix:处于与男性用户的私聊页,男性发了一条新的付费消息,等待1分钟后退出私聊页,出现男性用户的付费弹窗

* feat:UI验收问题调整

* bugfix:给男的弹推荐女的弹窗,用户点击打招呼,app闪退

* bugfix:用户处于私聊页面的收益tab下时,收到新用户的付费消息,没有显示新用户的付费消息

* bugfix:处于与男性用户的私聊页,男性发了一条新的付费消息,等待1分钟后退出私聊页,出现男性用户的付费弹窗

* feat:今日缘分Dialog调整点击按钮自动关闭逻辑

* bugfix:买量女用户登录没有落地到语音房tab

* bugfix:处于与男性用户的私聊页,男性发了一条新的付费消息,等待1分钟后退出私聊页,出现男性用户的付费弹窗

* bugfix:处于与男性用户的私聊页,男性发了一条新的付费消息,等待1分钟后退出私聊页,出现男性用户的付费弹窗

* bugfix:yoki team 无需展示阅读下一条消息

* bugfix:修复新客奖励弹窗延迟问题

* 新增关键新客奖励log

* bugfix:修复新客奖励弹窗不展示问题

* 新增关键新客奖励log

* feat:ui二轮验收

* bugfix:修复今日缘分Dialog不显示的时候无法展示新客奖励Dialog问题

---------

Co-authored-by: wutiaorong <wutiaorong@gmail.com>
Co-authored-by: qimingfeng <qimingfengwy@163.com>
LiuFJie 6 месяцев назад
Родитель
Сommit
c31bf50d3a
100 измененных файлов с 3143 добавлено и 184 удалено
  1. BIN
      app/src/main/assets/income_message_reminder_reply_btn.svga
  2. BIN
      app/src/main/assets/message_income_diamond.svga
  3. BIN
      app/src/main/assets/message_reminder_reply_btn.svga
  4. BIN
      app/src/main/assets/today_fate_btn.svga
  5. BIN
      app/src/main/assets/today_fate_content_animation_bg.svga
  6. BIN
      app/src/main/assets/today_fate_head_animation_bg.svga
  7. 13 0
      app/src/main/java/com/adealink/weparty/commonui/dialogchain/BaseDialogData.kt
  8. 4 1
      app/src/main/java/com/adealink/weparty/commonui/dialogchain/BaseDialogTask.kt
  9. 24 3
      app/src/main/java/com/adealink/weparty/commonui/dialogchain/DialogShowManager.kt
  10. 2 6
      app/src/main/java/com/adealink/weparty/commonui/dialogchain/dialogtask/NewUserRewardDialogTask.kt
  11. 1 0
      app/src/main/java/com/adealink/weparty/commonui/widget/floatview/FloatViewFactory.kt
  12. 4 0
      app/src/main/java/com/adealink/weparty/commonui/widget/floatview/IWindowManager.kt
  13. 9 0
      app/src/main/java/com/adealink/weparty/commonui/widget/floatview/ModeWindowManagerProxy.kt
  14. 1 0
      app/src/main/java/com/adealink/weparty/commonui/widget/floatview/data/IFloatData.kt
  15. 12 0
      app/src/main/java/com/adealink/weparty/commonui/widget/floatview/mode/ApplicationModeWindowManager.kt
  16. 11 0
      app/src/main/java/com/adealink/weparty/commonui/widget/floatview/mode/SystemModeWindowManager.kt
  17. 5 0
      app/src/main/java/com/adealink/weparty/commonui/widget/floatview/mode/fix/Android10FixApplicationModeWindowManager.kt
  18. 10 0
      app/src/main/java/com/adealink/weparty/module/message/IMessageService.kt
  19. 36 0
      app/src/main/java/com/adealink/weparty/module/message/MessageModule.kt
  20. 30 0
      app/src/main/java/com/adealink/weparty/module/message/Router.kt
  21. 49 0
      app/src/main/java/com/adealink/weparty/module/message/data/MessageData.kt
  22. 122 0
      app/src/main/java/com/adealink/weparty/module/message/reminder/IncomeMessageReminderDialog.kt
  23. 71 0
      app/src/main/java/com/adealink/weparty/module/message/reminder/MessageIncomeReminderDialogTask.kt
  24. 133 0
      app/src/main/java/com/adealink/weparty/module/message/reminder/MessageReminderDialog.kt
  25. 111 0
      app/src/main/java/com/adealink/weparty/module/message/reminder/MessageReminderDialogTask.kt
  26. 66 0
      app/src/main/java/com/adealink/weparty/module/message/reminder/MessageReplyRewardDialog.kt
  27. 141 0
      app/src/main/java/com/adealink/weparty/module/message/todayfate/TodayFateDialog.kt
  28. 82 0
      app/src/main/java/com/adealink/weparty/module/message/todayfate/TodayFateDialogTask.kt
  29. 53 0
      app/src/main/java/com/adealink/weparty/module/message/todayfate/TodayFateUserItemViewBinder.kt
  30. 5 1
      app/src/main/java/com/adealink/weparty/module/message/viewmodel/IMessageViewModel.kt
  31. 5 0
      app/src/main/java/com/adealink/weparty/module/operation/IOperationService.kt
  32. 18 0
      app/src/main/java/com/adealink/weparty/module/operation/OperationModule.kt
  33. 25 0
      app/src/main/java/com/adealink/weparty/module/operation/recommend/Data.kt
  34. 0 9
      app/src/main/java/com/adealink/weparty/module/userlist/viewmodel/UserListViewModel.kt
  35. 7 1
      app/src/main/java/com/adealink/weparty/push/NotificationUtil.kt
  36. 2 1
      app/src/main/java/com/adealink/weparty/push/data/Constants.kt
  37. 37 1
      app/src/main/java/com/adealink/weparty/push/data/PushData.kt
  38. 29 0
      app/src/main/java/com/adealink/weparty/ui/MainStartUpFragment.kt
  39. 23 0
      app/src/main/java/com/adealink/weparty/ui/home/BaseHomeFragment.kt
  40. 8 0
      app/src/main/java/com/adealink/weparty/ui/home/util/HomeLocalService.kt
  41. BIN
      app/src/main/res/drawable-xhdpi/ic_unread_conversation.webp
  42. BIN
      app/src/main/res/drawable-xhdpi/message_dialog_close_ic.webp
  43. BIN
      app/src/main/res/drawable-xhdpi/message_dialog_title_bg.webp
  44. 0 0
      app/src/main/res/drawable-xhdpi/message_price_diamond_icon.webp
  45. BIN
      app/src/main/res/drawable-xhdpi/message_reminder_desc_bg.9.png
  46. BIN
      app/src/main/res/drawable-xhdpi/message_reminder_msg_content_bg.webp
  47. BIN
      app/src/main/res/drawable-xhdpi/message_reply_reward_head_bg.webp
  48. BIN
      app/src/main/res/drawable-xhdpi/message_reply_reward_ic.webp
  49. BIN
      app/src/main/res/drawable-xhdpi/message_reply_reward_red_packet_ic.webp
  50. BIN
      app/src/main/res/drawable-xhdpi/message_today_fate_content_bg.9.png
  51. BIN
      app/src/main/res/drawable-xhdpi/message_today_fate_head_bg.webp
  52. 6 0
      app/src/main/res/drawable/bg_unread_conversation.xml
  53. 9 0
      app/src/main/res/drawable/message_income_reminder_content_bg.xml
  54. 9 0
      app/src/main/res/drawable/message_reminder_dialog_bg.xml
  55. 9 0
      app/src/main/res/drawable/message_reply_reward_content_bg.xml
  56. 6 0
      app/src/main/res/drawable/message_reply_reward_view_bg.xml
  57. 201 0
      app/src/main/res/layout/dialog_income_message_reminder.xml
  58. 201 0
      app/src/main/res/layout/dialog_message_reminder.xml
  59. 126 0
      app/src/main/res/layout/dialog_message_reply_reward.xml
  60. 108 0
      app/src/main/res/layout/dialog_today_fate.xml
  61. 62 0
      app/src/main/res/layout/item_today_fate_user.xml
  62. 57 0
      app/src/main/res/layout/layout_unread_conversation.xml
  63. 8 0
      app/src/main/res/values-ar/strings.xml
  64. 8 0
      app/src/main/res/values-zh/strings.xml
  65. 6 0
      app/src/main/res/values/colors.xml
  66. 8 0
      app/src/main/res/values/strings.xml
  67. 8 1
      module/account/src/main/java/com/adealink/weparty/account/register/RegisterProfileActivity.kt
  68. 5 0
      module/medal/src/main/java/com/adealink/weparty/medal/dialog/ChatAchievementDialog.kt
  69. 23 1
      module/message/src/main/java/com/adealink/weparty/message/MessageFragment.kt
  70. 22 0
      module/message/src/main/java/com/adealink/weparty/message/MessageServiceImpl.kt
  71. 10 0
      module/message/src/main/java/com/adealink/weparty/message/callback/ISessionInfoChangeCallback.kt
  72. 7 0
      module/message/src/main/java/com/adealink/weparty/message/conversation/ConversationActivity.kt
  73. 7 0
      module/message/src/main/java/com/adealink/weparty/message/conversation/ConversationFragment.kt
  74. 210 0
      module/message/src/main/java/com/adealink/weparty/message/conversation/comp/UnreadConversationComp.kt
  75. 2 22
      module/message/src/main/java/com/adealink/weparty/message/conversation/data/MessageExpansion.kt
  76. 1 1
      module/message/src/main/java/com/adealink/weparty/message/conversation/provider/BaseMessageItemProvider.kt
  77. 26 0
      module/message/src/main/java/com/adealink/weparty/message/conversation/view/UnreadConversationView.kt
  78. 3 0
      module/message/src/main/java/com/adealink/weparty/message/conversationlist/model/BaseUiConversation.kt
  79. 8 0
      module/message/src/main/java/com/adealink/weparty/message/conversationlist/model/SingleConversation.kt
  80. 5 0
      module/message/src/main/java/com/adealink/weparty/message/conversationlist/provider/BaseConversationProvider.kt
  81. 68 54
      module/message/src/main/java/com/adealink/weparty/message/conversationlist/viewmodel/ConversationListViewModel.kt
  82. 2 1
      module/message/src/main/java/com/adealink/weparty/message/data/MessageData.kt
  83. 8 0
      module/message/src/main/java/com/adealink/weparty/message/datasource/local/MessageLocalService.kt
  84. 24 0
      module/message/src/main/java/com/adealink/weparty/message/datasource/remote/MessageHttpService.kt
  85. 20 0
      module/message/src/main/java/com/adealink/weparty/message/floatview/NotificationIncomeMessageFloatData.kt
  86. 91 0
      module/message/src/main/java/com/adealink/weparty/message/floatview/NotificationIncomeMessageFloatView.kt
  87. 1 1
      module/message/src/main/java/com/adealink/weparty/message/floatview/NotificationMessageFloatView.kt
  88. 14 0
      module/message/src/main/java/com/adealink/weparty/message/manager/IMessageManager.kt
  89. 320 77
      module/message/src/main/java/com/adealink/weparty/message/manager/MessageManager.kt
  90. 60 0
      module/message/src/main/java/com/adealink/weparty/message/util/MessageNoticeUtils.kt
  91. 28 0
      module/message/src/main/java/com/adealink/weparty/message/viewmodel/MessageViewModel.kt
  92. 6 0
      module/message/src/main/res/drawable-xhdpi/message_notification_income_message_btn_bg.xml
  93. BIN
      module/message/src/main/res/drawable-xhdpi/message_notification_income_message_sign.webp
  94. 6 0
      module/message/src/main/res/drawable/im_conversation_income_diamond_bg.xml
  95. 9 0
      module/message/src/main/res/drawable/message_income_float_view_bg.xml
  96. 12 1
      module/message/src/main/res/layout/fragment_conversation.xml
  97. 26 2
      module/message/src/main/res/layout/im_conversationlist_item.xml
  98. 130 0
      module/message/src/main/res/layout/layout_notification_income_message_view.xml
  99. 4 0
      module/message/src/main/res/values-ar/strings.xml
  100. 4 0
      module/message/src/main/res/values-zh/strings.xml

BIN
app/src/main/assets/income_message_reminder_reply_btn.svga


BIN
app/src/main/assets/message_income_diamond.svga


BIN
app/src/main/assets/message_reminder_reply_btn.svga


BIN
app/src/main/assets/today_fate_btn.svga


BIN
app/src/main/assets/today_fate_content_animation_bg.svga


BIN
app/src/main/assets/today_fate_head_animation_bg.svga


+ 13 - 0
app/src/main/java/com/adealink/weparty/commonui/dialogchain/BaseDialogData.kt

@@ -10,6 +10,8 @@ import com.adealink.weparty.module.task.CommonTaskCompleteRewardNotify
 import com.adealink.weparty.module.task.invite.InviteNewRewardNotify
 import com.adealink.weparty.module.userlist.data.UserInfoItemData
 import com.adealink.weparty.module.family.data.FamilyInfo
+import com.adealink.weparty.module.profile.data.UserInfo
+import io.rong.imlib.model.Message
 import kotlinx.parcelize.Parcelize
 
 
@@ -133,6 +135,17 @@ sealed class BaseDialogData(
     class GreetSettingGuideDialogData(val extraToUid: Long) : Parcelable,
         BaseDialogData("GreetSettingGuideDialog")
 
+    @Parcelize
+    class TodayFateDialogData(val recommendId: String, val userInfos: List<UserInfo>) : Parcelable,
+        BaseDialogData("TodayFateDialog")
+
+    @Parcelize
+    class MessageReminderDialogData(val message: Message) : Parcelable, BaseDialogData("MessageReminderDialog")
+
+    @Parcelize
+    class MessageIncomeReminderDialogData(val content: String, val sender: UserInfo) : Parcelable,
+        BaseDialogData("MessageIncomeReminderDialog")
+
     @Parcelize
     class DailyMsgLimitReminderDialogData(val curCount: Long, val totalCount: Long) : Parcelable,
         BaseDialogData("DailyMsgLimitReminderDialog")

+ 4 - 1
app/src/main/java/com/adealink/weparty/commonui/dialogchain/BaseDialogTask.kt

@@ -13,7 +13,7 @@ abstract class BaseDialogTask<DATA : BaseDialogData>() {
     abstract val tag: String // 用于去重等
 
     abstract suspend fun canShow(): Rlt<DATA>
-    abstract fun showDialog(dialogData: DATA, fragmentActivity: FragmentActivity): Int
+    abstract fun showDialog(dialogData: DATA, fragmentActivity: FragmentActivity): Int?
 
     //某个弹窗任务只在指定的一组 Activity 页面中才允许展示。(默认弹窗是当前可见activity就会弹)
     open val onlyShowInActivities: List<String> = listOf()
@@ -21,6 +21,9 @@ abstract class BaseDialogTask<DATA : BaseDialogData>() {
     //不允许显示弹窗的Activity页面列表(优先级高于onlyShowInActivities)
     open val excludeActivities: List<String> = listOf()
 
+    //不允许显示弹窗的Fragment页面列表(优先级高于onlyShowInActivities)
+    open val excludeFragments: List<String> = listOf()
+
     //生成的弹窗数据
     var dialogData: BaseDialogData? = null
 }

+ 24 - 3
app/src/main/java/com/adealink/weparty/commonui/dialogchain/DialogShowManager.kt

@@ -37,6 +37,8 @@ interface IDialogShowManager {
     fun pause()
 
     fun resume()
+
+    fun removeWaitToShowDialog(predicate: (BaseDialogTask<BaseDialogData>) -> Boolean)
 }
 
 object DialogShowManager : IDialogShowManager {
@@ -47,7 +49,7 @@ object DialogShowManager : IDialogShowManager {
     private val waitToShowDialogQueue = mutableListOf<BaseDialogTask<BaseDialogData>>()
 
     //key,页面hashCode,value,正在展示的弹窗
-    private val showingDialogMap = mutableMapOf<Int, Int>()
+    private val showingDialogMap = mutableMapOf<Int, Int?>()
 
 
     init {
@@ -149,6 +151,15 @@ object DialogShowManager : IDialogShowManager {
                 }
             }
 
+            if (task.excludeFragments.isNotEmpty()) {
+                val fragments =
+                    (activity as? FragmentActivity)?.supportFragmentManager?.fragments.orEmpty()
+                val needExclude = task.excludeFragments.any { excludeFragment ->
+                    fragments.any { it::class.java.name == excludeFragment && it.isResumed && it.userVisibleHint && it.isVisible }
+                }
+                if (needExclude) continue
+            }
+
             // 校验当前是否是允许展示的 Activity 页面
             if (task.onlyShowInActivities.isNotEmpty()) {
                 if (currentActivityName !in task.onlyShowInActivities) {
@@ -157,9 +168,15 @@ object DialogShowManager : IDialogShowManager {
             }
 
             // 当前页面是否已经展示弹窗
-            if (showingDialogMap[activityKey] != null) continue
+            if (showingDialogMap[activityKey] != null) {
+                Log.i(TAG, "showingDialogMap[activityKey] != null")
+                continue
+            }
 
-            if (task.dialogData == null) continue
+            if (task.dialogData == null) {
+                Log.i(TAG, "task.dialogData == null")
+                continue
+            }
 
             //找到了!展示并从队列移除
             iterator.remove()
@@ -192,6 +209,10 @@ object DialogShowManager : IDialogShowManager {
         tryShowNextDialog(AppUtil.currentActivity as? FragmentActivity ?: return)
     }
 
+    override fun removeWaitToShowDialog(predicate: (BaseDialogTask<BaseDialogData>) -> Boolean) {
+        waitToShowDialogQueue.removeAll(waitToShowDialogQueue.filter(predicate))
+    }
+
     override fun pause() {
         paused = true
         Log.i(TAG, "pause: ")

+ 2 - 6
app/src/main/java/com/adealink/weparty/commonui/dialogchain/dialogtask/NewUserRewardDialogTask.kt

@@ -64,14 +64,10 @@ class NewUserRewardDialogTask(
                         )
                     }
                 }
+        fragment?.show(fragmentActivity.supportFragmentManager)
         CoroutineScope(Dispatcher.WENEXT_THREAD_POOL).launch {
             taskHttpService.getPVReward().apply {
-                onSuccess {
-                    launch(Dispatcher.UI) {
-                        Log.e(TAG, "showNewUserRewardDialog")
-                        fragment?.show(fragmentActivity.supportFragmentManager)
-                    }
-                }
+                onSuccess { Log.i(TAG, "showNewUserRewardDialog") }
                 onFailure { Log.e(TAG, "getPVReward failed:$it") }
             }
         }

+ 1 - 0
app/src/main/java/com/adealink/weparty/commonui/widget/floatview/FloatViewFactory.kt

@@ -49,6 +49,7 @@ class FloatViewFactory : IFloatViewFactory {
             FloatWindowType.GAME_ENTRANCE -> GameEntranceFloatView(data as GameEntranceFloatData)
             FloatWindowType.GLOBAL_HEADLINE -> HeadlineModule.getGlobalHeadlineFloatView(data)
             FloatWindowType.CALL_MATCH_ENTRANCE -> CallMatchEntranceFloatView(data as CallMatchEntranceFloatData)
+            FloatWindowType.NOTIFICATION_INCOME_MESSAGE -> MessageModule.getNotificationIncomeMessageFloatView(data)
             FloatWindowType.MATCH_NOTIFY -> CallModule.getMatchFloatView(data)
             FloatWindowType.MATCH_FLOAT_WINDOW -> CallModule.getMatchFloatView(data)
         }

+ 4 - 0
app/src/main/java/com/adealink/weparty/commonui/widget/floatview/IWindowManager.kt

@@ -3,6 +3,7 @@ package com.adealink.weparty.commonui.widget.floatview
 import com.adealink.weparty.commonui.widget.floatview.data.FloatWindowType
 import com.adealink.weparty.commonui.widget.floatview.data.IFloatData
 import com.adealink.weparty.commonui.widget.floatview.view.BaseFloatView
+import java.util.concurrent.CopyOnWriteArrayList
 
 interface IWindowManager<V : BaseFloatView<out IFloatData>> {
     fun addView(view: V)
@@ -15,5 +16,8 @@ interface IWindowManager<V : BaseFloatView<out IFloatData>> {
     //移除所有正在显示或者未显示的浮窗
     fun removeAllFloatViews(type: FloatWindowType, reason: String)
 
+    //查找所有正在显示或者未显示的浮窗
+    fun findAllFloatViews(type: FloatWindowType): CopyOnWriteArrayList<BaseFloatView<out IFloatData>>
+
     fun remove(predicate: (BaseFloatView<out IFloatData>) -> Boolean, reason: String)
 }

+ 9 - 0
app/src/main/java/com/adealink/weparty/commonui/widget/floatview/ModeWindowManagerProxy.kt

@@ -14,6 +14,7 @@ import com.adealink.weparty.commonui.widget.floatview.mode.SystemModeWindowManag
 import com.adealink.weparty.commonui.widget.floatview.mode.fix.Android10FixApplicationModeWindowManager
 import com.adealink.weparty.commonui.widget.floatview.mode.fix.Android14ModeWindowManager
 import com.adealink.weparty.commonui.widget.floatview.view.BaseFloatView
+import java.util.concurrent.CopyOnWriteArrayList
 
 class ModeWindowManagerProxy : BaseWindowManager() {
 
@@ -184,6 +185,14 @@ class ModeWindowManagerProxy : BaseWindowManager() {
         }
     }
 
+    override fun findAllFloatViews(type: FloatWindowType): CopyOnWriteArrayList<BaseFloatView<out IFloatData>> {
+        val allFloatViews = CopyOnWriteArrayList<BaseFloatView<out IFloatData>>()
+        windowManagerMap.values.forEach {
+            allFloatViews.addAll(it.findAllFloatViews(type))
+        }
+        return allFloatViews
+    }
+
     override fun remove(predicate: (BaseFloatView<out IFloatData>) -> Boolean, reason: String) {
         Log.i(TAG_FLOAT_VIEW, "remove, reason: $reason")
         windowManagerMap.values.forEach {

+ 1 - 0
app/src/main/java/com/adealink/weparty/commonui/widget/floatview/data/IFloatData.kt

@@ -18,6 +18,7 @@ enum class FloatWindowType(val type: String) {
     ROCKET_HEADLINE("rocket_headline"),
     GAME_ENTRANCE("game_entrance"),
     GLOBAL_HEADLINE("global_headline"), //全服横幅,房间内房间外
+    NOTIFICATION_INCOME_MESSAGE("notification_income_message"),
     CALL_MATCH_ENTRANCE("call_match_entrance"),
     MATCH_FLOAT_WINDOW("match_float_window"),
     MATCH_NOTIFY("match_notify"),

+ 12 - 0
app/src/main/java/com/adealink/weparty/commonui/widget/floatview/mode/ApplicationModeWindowManager.kt

@@ -388,6 +388,18 @@ open class ApplicationModeWindowManager : BaseWindowManager() {
         }
     }
 
+    override fun findAllFloatViews(type: FloatWindowType): CopyOnWriteArrayList<BaseFloatView<out IFloatData>> {
+        Log.i(TAG_FLOAT_VIEW, "${SUB_TAG}, findAllFloatViews, type: $type")
+        val allFloatViews = CopyOnWriteArrayList<BaseFloatView<out IFloatData>>()
+        toAddViewQueue.filter { it.baseFloatData.windowType() == type }.forEach { view ->
+            allFloatViews.add(view)
+        }
+        viewAddMap.filter { it.baseFloatData.windowType() == type }.forEach { view ->
+            allFloatViews.add(view)
+        }
+        return allFloatViews
+    }
+
     override fun remove(predicate: (BaseFloatView<out IFloatData>) -> Boolean, reason: String) {
         Log.i(TAG_FLOAT_VIEW, "${SUB_TAG}, remove, predicate: $predicate")
         runOnUiThread {

+ 11 - 0
app/src/main/java/com/adealink/weparty/commonui/widget/floatview/mode/SystemModeWindowManager.kt

@@ -18,6 +18,7 @@ import com.adealink.weparty.commonui.widget.floatview.data.IFloatData
 import com.adealink.weparty.commonui.widget.floatview.data.TAG_FLOAT_VIEW
 import com.adealink.weparty.commonui.widget.floatview.view.BaseFloatView
 import com.adealink.weparty.commonui.widget.floatview.view.BaseWindowFloatView
+import java.util.concurrent.CopyOnWriteArrayList
 import java.util.concurrent.CopyOnWriteArraySet
 
 class SystemModeWindowManager : BaseWindowManager() {
@@ -105,6 +106,16 @@ class SystemModeWindowManager : BaseWindowManager() {
         }
     }
 
+    override fun findAllFloatViews(type: FloatWindowType): CopyOnWriteArrayList<BaseFloatView<out IFloatData>> {
+        val allFloatViews = CopyOnWriteArrayList<BaseFloatView<out IFloatData>>()
+        viewList.forEach {
+            if (it.baseFloatData.windowType() == type) {
+                allFloatViews.add(it)
+            }
+        }
+        return allFloatViews
+    }
+
     override fun remove(predicate: (BaseFloatView<out IFloatData>) -> Boolean, reason: String) {
         viewList.filter(predicate).forEach { view ->
             removeView(view, reason)

+ 5 - 0
app/src/main/java/com/adealink/weparty/commonui/widget/floatview/mode/fix/Android10FixApplicationModeWindowManager.kt

@@ -7,6 +7,7 @@ import com.adealink.weparty.commonui.widget.floatview.data.FloatWindowType
 import com.adealink.weparty.commonui.widget.floatview.data.IFloatData
 import com.adealink.weparty.commonui.widget.floatview.data.TAG_FLOAT_VIEW
 import com.adealink.weparty.commonui.widget.floatview.view.BaseFloatView
+import java.util.concurrent.CopyOnWriteArrayList
 
 /**
  * Android10透明主题有个异常生命周期回调:A Activity启动透明主题Activity B会执行以下生命周期
@@ -128,6 +129,10 @@ class Android10FixApplicationModeWindowManager(
         base.removeFloatViewByType(type, reason)
     }
 
+    override fun findAllFloatViews(type: FloatWindowType): CopyOnWriteArrayList<BaseFloatView<out IFloatData>> {
+        return base.findAllFloatViews(type)
+    }
+
     override fun remove(predicate: (BaseFloatView<out IFloatData>) -> Boolean, reason: String) {
         base.remove(predicate,reason)
     }

+ 10 - 0
app/src/main/java/com/adealink/weparty/module/message/IMessageService.kt

@@ -1,5 +1,6 @@
 package com.adealink.weparty.module.message
 
+import android.text.Spannable
 import androidx.lifecycle.ViewModelStoreOwner
 import com.adealink.frame.aab.IService
 import com.adealink.frame.base.Rlt
@@ -8,10 +9,12 @@ import com.adealink.frame.startup.IAppStartUpTask
 import com.adealink.weparty.commonui.widget.floatview.data.IFloatData
 import com.adealink.weparty.commonui.widget.floatview.view.BaseFloatView
 import com.adealink.weparty.module.message.data.CustomerInfo
+import com.adealink.weparty.module.message.data.NoReplyMessageRsp
 import com.adealink.weparty.module.message.viewmodel.IMessageViewModel
 import com.adealink.weparty.module.profile.data.UserFamilySetting
 import io.rong.imlib.IRongCoreCallback
 import io.rong.imlib.model.Conversation
+import io.rong.imlib.model.MessageContent
 
 interface IMessageService : IService<IMessageService>, IAppStartUpTask {
     fun getMessageViewModel(owner: ViewModelStoreOwner): IMessageViewModel?
@@ -27,6 +30,7 @@ interface IMessageService : IService<IMessageService>, IAppStartUpTask {
     fun getSayHiState(uid: Long): Boolean
 
     fun getNotificationMessageFloatView(data: IFloatData): BaseFloatView<out IFloatData>?
+    fun getNotificationIncomeMessageFloatView(data: IFloatData): BaseFloatView<out IFloatData>?
     fun addUnReadMessageCountChangedObserver(
         observer: UnReadMessageManager.IUnReadMessageObserver,
         conversationTypes: Array<Conversation.ConversationType>)
@@ -46,4 +50,10 @@ interface IMessageService : IService<IMessageService>, IAppStartUpTask {
     suspend fun checkIsCustomerService(targetId: Long, cache: Boolean): Boolean
 
     suspend fun getUserFamilySettingConfig(cache: Boolean): Rlt<UserFamilySetting?>
+
+    fun getMessageSummary(messageContent: MessageContent, targetName: String): Spannable
+
+    suspend fun batchSendQuickMessage(uidList: List<Long>): Rlt<Any>
+
+    suspend fun getNoReplyMessage(): Rlt<NoReplyMessageRsp?>
 }

+ 36 - 0
app/src/main/java/com/adealink/weparty/module/message/MessageModule.kt

@@ -1,6 +1,8 @@
 package com.adealink.weparty.module.message
 
 import android.app.Application
+import android.text.Spannable
+import android.text.SpannableStringBuilder
 import androidx.lifecycle.ViewModelStoreOwner
 import com.adealink.frame.aab.BaseDynamicModule
 import com.adealink.frame.aab.constant.AABModuleNotInitError
@@ -10,10 +12,12 @@ import com.adealink.weparty.R
 import com.adealink.weparty.commonui.widget.floatview.data.IFloatData
 import com.adealink.weparty.commonui.widget.floatview.view.BaseFloatView
 import com.adealink.weparty.module.message.data.CustomerInfo
+import com.adealink.weparty.module.message.data.NoReplyMessageRsp
 import com.adealink.weparty.module.message.viewmodel.IMessageViewModel
 import com.adealink.weparty.module.profile.data.UserFamilySetting
 import io.rong.imlib.IRongCoreCallback
 import io.rong.imlib.model.Conversation
+import io.rong.imlib.model.MessageContent
 
 object MessageModule : BaseDynamicModule<IMessageService>(IMessageService::class), IMessageService {
 
@@ -41,6 +45,10 @@ object MessageModule : BaseDynamicModule<IMessageService>(IMessageService::class
         return getService().getNotificationMessageFloatView(data)
     }
 
+    override fun getNotificationIncomeMessageFloatView(data: IFloatData): BaseFloatView<out IFloatData>? {
+        return getService().getNotificationIncomeMessageFloatView(data)
+    }
+
     override fun addUnReadMessageCountChangedObserver(
         observer: UnReadMessageManager.IUnReadMessageObserver,
         conversationTypes: Array<Conversation.ConversationType>
@@ -76,6 +84,18 @@ object MessageModule : BaseDynamicModule<IMessageService>(IMessageService::class
         return getService().getUserFamilySettingConfig(cache)
     }
 
+    override fun getMessageSummary(messageContent: MessageContent, targetName: String): Spannable {
+        return getService().getMessageSummary(messageContent, targetName)
+    }
+
+    override suspend fun batchSendQuickMessage(uidList: List<Long>): Rlt<Any> {
+        return getService().batchSendQuickMessage(uidList)
+    }
+
+    override suspend fun getNoReplyMessage(): Rlt<NoReplyMessageRsp?> {
+        return getService().getNoReplyMessage()
+    }
+
     override fun appOnCreateMainTask(application: Application) {
         return getService().appOnCreateMainTask(application)
     }
@@ -112,6 +132,10 @@ object MessageModule : BaseDynamicModule<IMessageService>(IMessageService::class
                 return null
             }
 
+            override fun getNotificationIncomeMessageFloatView(data: IFloatData): BaseFloatView<out IFloatData>? {
+                return null
+            }
+
             override fun addUnReadMessageCountChangedObserver(
                 observer: UnReadMessageManager.IUnReadMessageObserver,
                 conversationTypes: Array<Conversation.ConversationType>
@@ -147,6 +171,18 @@ object MessageModule : BaseDynamicModule<IMessageService>(IMessageService::class
                 return Rlt.Failed(AABModuleNotInitError())
             }
 
+            override fun getMessageSummary(messageContent: MessageContent, targetName: String): Spannable {
+                return SpannableStringBuilder()
+            }
+
+            override suspend fun batchSendQuickMessage(uidList: List<Long>): Rlt<Any> {
+                return Rlt.Failed(AABModuleNotInitError())
+            }
+
+            override suspend fun getNoReplyMessage(): Rlt<NoReplyMessageRsp?> {
+                return Rlt.Failed(AABModuleNotInitError())
+            }
+
             override fun getService(): IMessageService? {
                 return null
             }

+ 30 - 0
app/src/main/java/com/adealink/weparty/module/message/Router.kt

@@ -106,6 +106,36 @@ interface Message {
         }
     }
 
+    interface TodayFate {
+        companion object {
+            const val PATH = "/today_fate"
+            const val EXTRA_USER_INFOS = "extra_user_infos"
+        }
+    }
+
+    interface MessageReminder {
+        companion object {
+            const val PATH = "/message_reminder"
+            const val EXTRA_MESSAGE = "extra_message"
+        }
+    }
+
+    interface IncomeMessageReminder {
+        companion object {
+            const val PATH = "/income_message_reminder"
+            const val EXTRA_CONTENT = "extra_content"
+            const val EXTRA_SENDER = "extra_sender"
+        }
+    }
+
+    interface MessageReplyReward {
+        companion object {
+            const val PATH = "/message_reply_reward"
+            const val EXTRA_TITLE = "extra_title"
+            const val EXTRA_CONTENT = "extra_content"
+        }
+    }
+
     interface InviteWithdraw {
         companion object {
             const val PATH = "/invite_withdraw"

+ 49 - 0
app/src/main/java/com/adealink/weparty/module/message/data/MessageData.kt

@@ -1,9 +1,11 @@
 package com.adealink.weparty.module.message.data
 
 import android.os.Parcelable
+import com.adealink.weparty.R
 import com.adealink.weparty.module.couple.data.CoupleUserInfo
 import com.adealink.weparty.module.family.data.FamilyInfo
 import com.adealink.weparty.module.profile.data.UserInfo
+import com.adealink.weparty.module.wallet.data.Currency
 import com.google.gson.annotations.GsonNullable
 import com.google.gson.annotations.SerializedName
 import io.rong.imlib.model.Conversation
@@ -68,6 +70,10 @@ data class SendQuickMessageReq(
     @GsonNullable @SerializedName("params") val params: Map<String, String>? = null,
 )
 
+data class BatchSendQuickMessageReq(
+    @SerializedName("targetUids") val uidList: List<Long> = listOf()
+)
+
 enum class IMMessageScene(val scene: Int) {
     NORMAL(1),//普通消息
     QUICK_MESSAGE(2),//快捷消息(打招呼
@@ -135,6 +141,8 @@ data class SessionInfo(
     @GsonNullable
     @SerializedName("userIntimacyInfo")
     var userIntimacyInfo: CoupleUserInfo?,//亲密关系信息
+    @SerializedName("existsNoReplyPaidMessage")
+    var existsNoReplyPaidMessage: Boolean,//存在未回复的付费消息
 ) {
     fun toPrivateConversation(): Conversation {
         val conversation = Conversation.obtain(
@@ -243,6 +251,7 @@ enum class EnterConversationFrom {
     IntimacyLimited,
     IntimacyRelationship,
     FamilyInvite,
+    MessageReminderDialog,
     VideoCall
 }
 
@@ -256,6 +265,46 @@ data class FamilyImInfoRsp(
     @SerializedName("voiceMessageButtonStatus") val voiceMessageButtonStatus: Int = 0
 ) : Parcelable
 
+@Parcelize
+data class NoReplyMessageRsp(
+    @SerializedName("existNoReplyPaidMessage") val existNoReplyPaidMessage: Boolean,
+) : Parcelable
+
+data class UnreadPaidSessionListReq(
+    @SerializedName("pageSize") val pageSize: Int, @SerializedName("timeStamp") val timeStamp: Long
+)
+
+data class UnreadPaidSessionListRsp(
+    @SerializedName("sessionInfos") val sessionInfos: List<SessionInfo>,
+    @SerializedName("timeStamp") val timeStamp: Long,
+)
+
+data class MessagePriceInfo(
+    @SerializedName("currencyType") val currencyType: Int,
+    @SerializedName("currencyValue") val currencyValue: Long,
+    @SerializedName("targetIncomeCurrencyValue") val targetIncomeCurrencyValue: Long,
+    @SerializedName("messageExpireMilliseconds") val messageExpireMilliseconds: Long,
+) {
+    fun getSpendIconRes(): Int {
+        return when (currencyType) {
+            Currency.Coin.value.toInt() -> R.drawable.message_price_diamond_icon
+            else -> 0
+        }
+    }
+
+    fun getIncomeIconRes(): Int {
+        return when (currencyType) {
+            Currency.Diamond.value.toInt() -> R.drawable.message_price_diamond_icon
+            else -> 0
+        }
+    }
+}
+
+data class ImMessageNotifyInfo(
+    @SerializedName("showTopNotify") val showTopNotify: Int,
+    @SerializedName("fromSystem") val fromSystem: Int,
+)
+
 enum class TaskButtonStatus(val status: Int) {
     HIDDEN(0), SHOWED(1)
 }

+ 122 - 0
app/src/main/java/com/adealink/weparty/module/message/reminder/IncomeMessageReminderDialog.kt

@@ -0,0 +1,122 @@
+package com.adealink.weparty.module.message.reminder
+
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.Typeface
+import android.os.Bundle
+import android.text.TextPaint
+import android.view.Gravity
+import android.view.Window
+import android.view.WindowManager
+import androidx.core.graphics.drawable.toDrawable
+import com.adealink.frame.aab.util.getCompatColor
+import com.adealink.frame.aab.util.getCompatString
+import com.adealink.frame.dot.NumDot
+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.onClick
+import com.adealink.weparty.R
+import com.adealink.weparty.commonui.dialogchain.DialogShowManager
+import com.adealink.weparty.commonui.dialogfragment.BaseDialogFragment
+import com.adealink.weparty.commonui.ext.getActivity
+import com.adealink.weparty.databinding.DialogIncomeMessageReminderBinding
+import com.adealink.weparty.module.message.Message
+import com.adealink.weparty.module.profile.Profile
+import com.adealink.weparty.module.profile.data.UserInfo
+import com.opensource.svgaplayer.SVGADynamicEntity
+import io.rong.push.RongPushClient.ConversationType
+
+/**
+ * Created by LfJ on 2025/8/27.
+ */
+@RouterUri(path = [Message.IncomeMessageReminder.PATH], desc = "私聊付费消息提醒")
+class IncomeMessageReminderDialog : BaseDialogFragment(R.layout.dialog_income_message_reminder) {
+
+    @BindExtra(name = Message.IncomeMessageReminder.EXTRA_CONTENT, desc = "消息信息", must = true)
+    var messageContent: String? = null
+
+    @BindExtra(name = Message.IncomeMessageReminder.EXTRA_SENDER, desc = "消息发送人", must = true)
+    var messageSender: UserInfo? = null
+
+    private val binding by viewBinding(DialogIncomeMessageReminderBinding::bind)
+
+    companion object {
+        const val KEY_BTN_TEXT = "text"
+    }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        Router.bind(this)
+    }
+
+    override fun initViews() {
+        super.initViews()
+        if (messageSender == null) {
+            dismiss()
+        } else {
+            binding.ivAvatar.setImageUrl(messageSender?.url)
+            binding.dotView.show(NumDot(1))
+            binding.userCountryView.setUserInfo(messageSender)
+            binding.userSexView.setSex(messageSender?.gender, messageSender?.birthday)
+            binding.tvUserName.text = messageSender?.name
+            binding.tvMessageContent.text = messageContent
+        }
+        binding.replyBtn.setAsset(
+            "income_message_reminder_reply_btn.svga", supplier = {
+                val textPaint = TextPaint().apply {
+                    textSize = 32f
+                    isAntiAlias = true
+                    color = getCompatColor(R.color.color_FFFFFF)
+                    typeface = Typeface.DEFAULT_BOLD
+                    textAlign = Paint.Align.CENTER
+                }
+                SVGADynamicEntity().apply {
+                    setDynamicText(
+                        getCompatString(R.string.message_reply), textPaint, KEY_BTN_TEXT
+                    )
+                }
+            })
+        initListener()
+    }
+
+    private fun initListener() {
+        binding.ivAvatar.onClick {
+            val activity = binding.root.getActivity() ?: return@onClick
+            Router.build(activity, Profile.UserProfile.PATH)
+                .putExtra(Profile.Common.EXTRA_UID, messageSender?.uid).start()
+        }
+        binding.btnClose.onClick {
+            dismiss()
+        }
+        binding.replyBtn.onClick {
+            DialogShowManager.removeWaitToShowDialog { messageSender?.uid != null && (it as? MessageIncomeReminderDialogTask)?.sender?.uid == messageSender?.uid }
+            val targetIdLong = messageSender?.uid ?: return@onClick
+            activity?.let {
+                Router.build(it, Message.Conversation.PATH)
+                    .putExtra(Message.Common.EXTRA_TO_UID, targetIdLong)
+                    .putExtra(Message.Common.EXTRA_CONVERSATION_TYPE, ConversationType.PRIVATE.value)
+                    .start()
+            }
+            dismiss()
+        }
+    }
+
+    override fun onPause() {
+        super.onPause()
+        DialogShowManager.removeWaitToShowDialog { messageSender?.uid != null && (it as? MessageIncomeReminderDialogTask)?.sender?.uid == messageSender?.uid }
+    }
+
+    override fun resetWindowAttributes(window: Window) {
+        super.resetWindowAttributes(window)
+        window.setLayout(
+            WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT
+        )
+        window.setBackgroundDrawable(Color.TRANSPARENT.toDrawable())
+        val attr = window.attributes
+        attr.gravity = Gravity.CENTER
+        attr.dimAmount = 0.6f
+        window.attributes = attr
+    }
+}

+ 71 - 0
app/src/main/java/com/adealink/weparty/module/message/reminder/MessageIncomeReminderDialogTask.kt

@@ -0,0 +1,71 @@
+package com.adealink.weparty.module.message.reminder
+
+import android.os.Bundle
+import androidx.fragment.app.FragmentActivity
+import com.adealink.frame.base.Rlt
+import com.adealink.frame.router.Router
+import com.adealink.weparty.commonui.dialogchain.BaseDialogData
+import com.adealink.weparty.commonui.dialogchain.BaseDialogTask
+import com.adealink.weparty.commonui.dialogchain.Priority
+import com.adealink.weparty.commonui.dialogfragment.BaseDialogFragment
+import com.adealink.weparty.module.medal.Medal
+import com.adealink.weparty.module.message.Message
+import com.adealink.weparty.module.operation.Operation
+import com.adealink.weparty.module.profile.data.UserInfo
+import com.adealink.weparty.module.setting.Setting
+import com.adealink.weparty.module.wallet.Wallet
+import com.adealink.weparty.module.webview.Web
+
+/**
+ * Created by LfJ on 2025/6/17.
+ */
+class MessageIncomeReminderDialogTask(val content: String, val sender: UserInfo) :
+    BaseDialogTask<BaseDialogData.MessageIncomeReminderDialogData>() {
+
+    companion object {
+        private const val TAG = "MessageIncomeReminderDialogTask"
+    }
+
+    override val priority: Int
+        get() = Priority.LOWER.priority
+    override val tag: String
+        get() = "MessageIncomeReminderDialogTask"
+
+    override suspend fun canShow(): Rlt<BaseDialogData.MessageIncomeReminderDialogData> {
+        return Rlt.Success(BaseDialogData.MessageIncomeReminderDialogData(content, sender))
+    }
+
+    // 设置不允许显示的Activity页面
+    override val excludeActivities: List<String>
+        get() = listOfNotNull(
+            Router.getClazz(Wallet.Recharge.PATH)?.name,
+            Router.getClazz(Web.FullScreen.PATH)?.name,
+            Router.getClazz(Message.Conversation.PATH)?.name,
+            Router.getClazz(Message.GroupConversation.PATH)?.name
+        )
+
+    // 设置不允许显示的Fragment页面
+    override val excludeFragments: List<String>
+        get() = listOfNotNull(
+            Router.getClazz(Wallet.WalletGoogleRechargeDialog.PATH)?.name,
+            Router.getClazz(Operation.RechargePackage.PATH)?.name,
+            Router.getClazz(Medal.ChatAchievement.PATH)?.name,
+            Router.getClazz(Setting.Setting.CHAT_SETTING_GUIDE)?.name,
+        )
+
+    override fun showDialog(
+        dialogData: BaseDialogData.MessageIncomeReminderDialogData,
+        fragmentActivity: FragmentActivity
+    ): Int {
+        val fragment =
+            Router.getRouterInstance<BaseDialogFragment>(Message.IncomeMessageReminder.PATH)
+                ?.apply {
+                    arguments = Bundle().apply {
+                        putString(Message.IncomeMessageReminder.EXTRA_CONTENT, dialogData.content)
+                        putParcelable(Message.IncomeMessageReminder.EXTRA_SENDER, dialogData.sender)
+                    }
+                }
+        fragment?.show(fragmentActivity.supportFragmentManager)
+        return fragment.hashCode()
+    }
+}

+ 133 - 0
app/src/main/java/com/adealink/weparty/module/message/reminder/MessageReminderDialog.kt

@@ -0,0 +1,133 @@
+package com.adealink.weparty.module.message.reminder
+
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.Typeface
+import android.os.Bundle
+import android.text.TextPaint
+import android.view.Gravity
+import android.view.Window
+import android.view.WindowManager
+import androidx.core.graphics.drawable.toDrawable
+import androidx.lifecycle.lifecycleScope
+import com.adealink.frame.aab.util.getCompatColor
+import com.adealink.frame.aab.util.getCompatString
+import com.adealink.frame.base.Rlt
+import com.adealink.frame.base.fastLazy
+import com.adealink.frame.dot.NumDot
+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.AppUtil
+import com.adealink.weparty.App
+import com.adealink.weparty.R
+import com.adealink.weparty.commonui.dialogfragment.BaseDialogFragment
+import com.adealink.weparty.commonui.ext.getActivity
+import com.adealink.weparty.databinding.DialogMessageReminderBinding
+import com.adealink.weparty.effect.AnimExtraConfig
+import com.adealink.weparty.effect.EffectAnimType
+import com.adealink.weparty.module.message.Message
+import com.adealink.weparty.module.message.MessageModule
+import com.adealink.weparty.module.message.data.EnterConversationFrom
+import com.adealink.weparty.module.profile.Profile
+import com.adealink.weparty.module.profile.ProfileModule
+import com.adealink.weparty.util.goLocalLinkPage
+import com.opensource.svgaplayer.SVGADynamicEntity
+import kotlinx.coroutines.launch
+
+/**
+ * Created by LfJ on 2025/8/27.
+ */
+@RouterUri(path = [Message.MessageReminder.PATH], desc = "私聊消息提醒")
+class MessageReminderDialog : BaseDialogFragment(R.layout.dialog_message_reminder) {
+
+    @BindExtra(name = Message.MessageReminder.EXTRA_MESSAGE, desc = "消息信息", must = true)
+    var message: io.rong.imlib.model.Message? = null
+
+    private val binding by viewBinding(DialogMessageReminderBinding::bind)
+    private val config by fastLazy { AnimExtraConfig(loop = -1) }
+
+    companion object {
+        const val KEY_BTN_TEXT = "text"
+    }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        Router.bind(this)
+    }
+
+    override fun initViews() {
+        super.initViews()
+        message?.let {
+            val targetIdLong = it.senderUserId?.toLongOrNull()
+            if (targetIdLong == null) {
+                dismiss()
+            } else {
+                lifecycleScope.launch {
+                    val targetUserInfoRlt = ProfileModule.getUserInfoByUid(targetIdLong, true)
+                    val targetUserInfo = (targetUserInfoRlt as? Rlt.Success)?.data
+                    binding.ivAvatar.setImageUrl(targetUserInfo?.url)
+                    binding.dotView.show(NumDot(1))
+                    binding.userCountryView.setUserInfo(targetUserInfo)
+                    binding.userSexView.setSex(targetUserInfo?.gender, targetUserInfo?.birthday)
+                    binding.tvUserName.text = targetUserInfo?.name
+                    binding.tvMessageContent.text =
+                        MessageModule.getMessageSummary(it.content, targetUserInfo?.name ?: "")
+                }
+            }
+        }
+        binding.headAnimationView.setUrl(getHeadAnimationUrl(), EffectAnimType.TC, config)
+        binding.replyBtn.setAsset(
+            "message_reminder_reply_btn.svga", supplier = {
+                val textPaint = TextPaint().apply {
+                    textSize = 32f
+                    isAntiAlias = true
+                    color = getCompatColor(R.color.color_FFFFFF)
+                    typeface = Typeface.DEFAULT_BOLD
+                    textAlign = Paint.Align.CENTER
+                }
+                SVGADynamicEntity().apply {
+                    setDynamicText(
+                        getCompatString(R.string.message_reply), textPaint, KEY_BTN_TEXT
+                    )
+                }
+            })
+        initListener()
+    }
+
+    private fun initListener() {
+        binding.ivAvatar.setOnClickListener {
+            val activity = binding.root.getActivity() ?: return@setOnClickListener
+            Router.build(activity, Profile.UserProfile.PATH)
+                .putExtra(Profile.Common.EXTRA_UID, message?.targetId).start()
+        }
+        binding.btnClose.setOnClickListener {
+            dismiss()
+        }
+        binding.replyBtn.setOnClickListener {
+            val targetIdLong = message?.senderUserId?.toLongOrNull() ?: return@setOnClickListener
+            val deeplink = Message.Conversation.getPrivateConversationDeeplink(
+                targetIdLong, EnterConversationFrom.MessageReminderDialog.name
+            )
+            goLocalLinkPage(AppUtil.currentActivity, deeplink)
+            dismiss()
+        }
+    }
+
+    override fun resetWindowAttributes(window: Window) {
+        super.resetWindowAttributes(window)
+        window.setLayout(
+            WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT
+        )
+        window.setBackgroundDrawable(Color.TRANSPARENT.toDrawable())
+        val attr = window.attributes
+        attr.gravity = Gravity.CENTER
+        attr.dimAmount = 0.6f
+        window.attributes = attr
+    }
+
+    private fun getHeadAnimationUrl(): String {
+        return App.instance.ossService.getUrlByPath("/activity/message/reminder/message_reminder_head_animation_bg.tcmp4")
+    }
+}

+ 111 - 0
app/src/main/java/com/adealink/weparty/module/message/reminder/MessageReminderDialogTask.kt

@@ -0,0 +1,111 @@
+package com.adealink.weparty.module.message.reminder
+
+import android.os.Bundle
+import androidx.fragment.app.FragmentActivity
+import com.adealink.frame.base.IError
+import com.adealink.frame.base.Rlt
+import com.adealink.frame.log.Log
+import com.adealink.frame.router.Router
+import com.adealink.frame.util.AppUtil
+import com.adealink.weparty.commonui.dialogchain.BaseDialogData
+import com.adealink.weparty.commonui.dialogchain.BaseDialogTask
+import com.adealink.weparty.commonui.dialogchain.Priority
+import com.adealink.weparty.commonui.dialogfragment.BaseDialogFragment
+import com.adealink.weparty.module.message.Message
+import com.adealink.weparty.module.operation.Operation
+import com.adealink.weparty.module.profile.ProfileModule
+import com.adealink.weparty.ui.home.util.HomeLocalService
+import com.adealink.weparty.ui.home.util.HomeUIUtil
+import io.rong.imlib.model.Conversation
+
+/**
+ * Created by LfJ on 2025/6/17.
+ */
+class MessageReminderDialogTask(private val message: io.rong.imlib.model.Message) :
+    BaseDialogTask<BaseDialogData.MessageReminderDialogData>() {
+
+    companion object {
+        private const val TAG = "MessageReminderDialogTask"
+        private const val MAX_COUNT_SHOW_MESSAGE_REMINDER = 1
+        private const val SHOW_MESSAGE_REMINDER_MIN_DURATION_IN_USER_LIST_TAB = 5000
+    }
+
+    override val priority: Int
+        get() = Priority.LOWER.priority
+    override val tag: String
+        get() = "MessageReminderDialogTask"
+
+    override suspend fun canShow(): Rlt<BaseDialogData.MessageReminderDialogData> {
+        val curActivity = AppUtil.currentActivity
+        val hasShowSignInRewardDialog =
+            (curActivity as? FragmentActivity)?.supportFragmentManager?.fragments?.any {
+                it::class.java == Router.getClazz(Operation.SignInRewardDialog.PATH) && it.isAdded
+            } ?: false
+        val hasShowTodayFateDialog =
+            (curActivity as? FragmentActivity)?.supportFragmentManager?.fragments?.any {
+                it::class.java == Router.getClazz(
+                    Message.TodayFate.PATH
+                ) && it.isAdded
+            } ?: false
+        if (hasShowSignInRewardDialog) {
+            return Rlt.Failed(IError("当前页面出现签到"))
+        }
+        if (hasShowTodayFateDialog) {
+            return Rlt.Failed(IError("当前页面出现异性推荐"))
+        }
+        if (!HomeUIUtil.isInUserListTab()) {
+            return Rlt.Failed(IError("当前页面不在用户列表Tab"))
+        }
+        if (message.conversationType != Conversation.ConversationType.PRIVATE) {
+            return Rlt.Failed(IError("非私聊消息"))
+        }
+        val targetIdLong =
+            message.senderUserId.toLongOrNull() ?: return Rlt.Failed(IError("发送方uid为null"))
+        val targetUserInfoRlt = ProfileModule.getUserInfoByUid(targetIdLong, true)
+        val targetUserInfo = (targetUserInfoRlt as? Rlt.Success)?.data
+            ?: return Rlt.Failed(IError("发送方UserInfo为null"))
+        if (!targetUserInfo.isFemale()) {
+            return Rlt.Failed(IError("发送方不是女性"))
+        }
+        if (targetUserInfo.isMerchant() || ProfileModule.getMyUserInfo()?.isMerchant() == true) {
+            return Rlt.Failed(IError("发送方或接收方是币商"))
+        }
+        if (targetUserInfo.isOfficial() || ProfileModule.getMyUserInfo()?.isOfficial() == true) {
+            return Rlt.Failed(IError("发送方或接收方是官方帐号"))
+        }
+        if (ProfileModule.getMyUserInfo()?.isFemale() == true) {
+            return Rlt.Failed(IError("本账号是女性"))
+        }
+        if (HomeLocalService.setUserListTabRecentTime != -1L) {
+            val duration = System.currentTimeMillis() - HomeLocalService.setUserListTabRecentTime
+            Log.i(TAG, "showMessageReminderDialogIfNeed duration:$duration")
+            return if (duration > SHOW_MESSAGE_REMINDER_MIN_DURATION_IN_USER_LIST_TAB) {
+                if (HomeLocalService.showMessageReminderCount < MAX_COUNT_SHOW_MESSAGE_REMINDER) {
+                    Rlt.Success(BaseDialogData.MessageReminderDialogData(message))
+                } else {
+                    Rlt.Failed(IError("展示消息提醒弹窗次数已达到最大数量"))
+                }
+            } else {
+                Rlt.Failed(IError("用户停留用户列表首页未超过最小要求时长"))
+            }
+        }
+        return Rlt.Failed(IError("未知错误"))
+    }
+
+    override fun showDialog(
+        dialogData: BaseDialogData.MessageReminderDialogData, fragmentActivity: FragmentActivity
+    ): Int {
+        val fragment =
+            Router.getRouterInstance<BaseDialogFragment>(Message.MessageReminder.PATH)?.apply {
+                arguments = Bundle().apply {
+                    putParcelable(Message.MessageReminder.EXTRA_MESSAGE, dialogData.message)
+                }
+            }
+        if (HomeLocalService.showMessageReminderCount < MAX_COUNT_SHOW_MESSAGE_REMINDER) {
+            fragment?.show(fragmentActivity.supportFragmentManager)
+            val showDialogCount = HomeLocalService.showMessageReminderCount
+            HomeLocalService.showMessageReminderCount = showDialogCount + 1
+        }
+        return fragment.hashCode()
+    }
+}

+ 66 - 0
app/src/main/java/com/adealink/weparty/module/message/reminder/MessageReplyRewardDialog.kt

@@ -0,0 +1,66 @@
+package com.adealink.weparty.module.message.reminder
+
+import android.graphics.Color
+import android.os.Bundle
+import android.view.Gravity
+import android.view.Window
+import android.view.WindowManager
+import androidx.core.graphics.drawable.toDrawable
+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.weparty.R
+import com.adealink.weparty.commonui.dialogfragment.BaseDialogFragment
+import com.adealink.weparty.databinding.DialogMessageReplyRewardBinding
+import com.adealink.weparty.module.message.Message
+import com.adealink.weparty.module.task.gotoUserTaskCenter
+
+/**
+ * Created by LfJ on 2025/9/1.
+ */
+@RouterUri(path = [Message.MessageReplyReward.PATH], desc = "消息回复奖励")
+class MessageReplyRewardDialog : BaseDialogFragment(R.layout.dialog_message_reply_reward) {
+
+    private val binding by viewBinding(DialogMessageReplyRewardBinding::bind)
+
+    @BindExtra(name = Message.MessageReplyReward.EXTRA_TITLE, desc = "标题", must = true)
+    var title: String? = null
+
+    @BindExtra(name = Message.MessageReplyReward.EXTRA_CONTENT, desc = "内容", must = true)
+    var content: String? = null
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        Router.bind(this)
+    }
+
+    override fun initViews() {
+        super.initViews()
+        binding.tvTitle.text = title
+        binding.tvDesc.text = content
+        initListener()
+    }
+
+    private fun initListener() {
+        binding.btnClose.setOnClickListener {
+            dismiss()
+        }
+        binding.btnView.setOnClickListener {
+            gotoUserTaskCenter(context = requireActivity())
+            dismiss()
+        }
+    }
+
+    override fun resetWindowAttributes(window: Window) {
+        super.resetWindowAttributes(window)
+        window.setLayout(
+            WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT
+        )
+        window.setBackgroundDrawable(Color.TRANSPARENT.toDrawable())
+        val attr = window.attributes
+        attr.gravity = Gravity.CENTER
+        attr.dimAmount = 0.6f
+        window.attributes = attr
+    }
+}

+ 141 - 0
app/src/main/java/com/adealink/weparty/module/message/todayfate/TodayFateDialog.kt

@@ -0,0 +1,141 @@
+package com.adealink.weparty.module.message.todayfate
+
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.Typeface
+import android.os.Bundle
+import android.text.TextPaint
+import android.view.Gravity
+import android.view.Window
+import android.view.WindowManager
+import androidx.core.graphics.drawable.toDrawable
+import androidx.lifecycle.lifecycleScope
+import androidx.recyclerview.widget.GridLayoutManager
+import com.adealink.frame.aab.util.getCompatColor
+import com.adealink.frame.aab.util.getCompatString
+import com.adealink.frame.base.Rlt
+import com.adealink.frame.base.fastLazy
+import com.adealink.frame.coroutine.dispatcher.Dispatcher
+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.weparty.R
+import com.adealink.weparty.commonui.dialogfragment.BaseDialogFragment
+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.databinding.DialogTodayFateBinding
+import com.adealink.weparty.module.message.Message
+import com.adealink.weparty.module.message.MessageModule
+import com.adealink.weparty.module.network.data.ServerCode
+import com.adealink.weparty.module.profile.data.UserInfo
+import com.adealink.weparty.module.userlist.HomeUserListFragment
+import com.adealink.weparty.module.wallet.Wallet
+import com.opensource.svgaplayer.SVGADynamicEntity
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+/**
+ * Created by LfJ on 2025/8/27.
+ */
+@RouterUri(path = [Message.TodayFate.PATH], desc = "今日缘分")
+class TodayFateDialog : BaseDialogFragment(R.layout.dialog_today_fate) {
+
+    companion object {
+        const val SPAN_COUNT = 2
+        const val DURATION_SHOWN = 60000L
+        const val KEY_BTN_TEXT = "text2"
+    }
+
+    private val binding by viewBinding(DialogTodayFateBinding::bind)
+    private val userListAdapter by fastLazy { MultiTypeListAdapter<UserInfo>() }
+
+    @BindExtra(name = Message.TodayFate.EXTRA_USER_INFOS, desc = "推荐用户列表")
+    var userInfos: ArrayList<UserInfo>? = null
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        Router.bind(this)
+    }
+
+    override fun loadData() {
+        super.loadData()
+        userInfos?.let {
+            userListAdapter.submitList(it)
+        }
+    }
+
+    override fun initViews() {
+        super.initViews()
+        binding.userListView.apply {
+            layoutManager = GridLayoutManager(context, SPAN_COUNT)
+            addItemDecoration(GridSpacingItemDecoration(SPAN_COUNT, 10.dp(), 20.dp(), false))
+            userListAdapter.register(TodayFateUserItemViewBinder())
+            adapter = userListAdapter
+        }
+        binding.sendLoveBtn.setAsset(
+            "today_fate_btn.svga", supplier = {
+                val textPaint = TextPaint().apply {
+                    textSize = 32f
+                    isAntiAlias = true
+                    color = getCompatColor(R.color.color_FFFFFF)
+                    typeface = Typeface.DEFAULT_BOLD
+                    textAlign = Paint.Align.CENTER
+                }
+                SVGADynamicEntity().apply {
+                    setDynamicText(
+                        getCompatString(R.string.message_send_love), textPaint, KEY_BTN_TEXT
+                    )
+                }
+            })
+        initListener()
+    }
+
+    private fun initListener() {
+        binding.btnClose.setOnClickListener {
+            dismiss()
+        }
+        binding.sendLoveBtn.setOnClickListener {
+            lifecycleScope.launch(Dispatcher.WENEXT_THREAD_POOL) {
+                val rlt =
+                    MessageModule.batchSendQuickMessage(userInfos?.map { it.uid } ?: return@launch)
+                showToast(rlt)
+                if (rlt is Rlt.Failed && rlt.error.serverCode == ServerCode.CURRENCY_NOT_ENOUGH.code) {
+                    Router.getRouterInstance<BaseDialogFragment>(Wallet.WalletGoogleRechargeDialog.PATH)
+                        ?.show(activity?.supportFragmentManager ?: return@launch)
+                } else if (rlt is Rlt.Success) {
+                    launch(Dispatcher.UI) {
+                        dismiss()
+                    }
+                }
+            }
+        }
+    }
+
+    override fun onResume() {
+        super.onResume()
+        lifecycleScope.launch {
+            delay(DURATION_SHOWN)
+            dismiss()
+        }
+    }
+
+    override fun dismiss() {
+        super.dismiss()
+        HomeUserListFragment.showHomeSayHiGuide.setValue(Unit)
+    }
+
+    override fun resetWindowAttributes(window: Window) {
+        super.resetWindowAttributes(window)
+        window.setLayout(
+            WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT
+        )
+        window.setBackgroundDrawable(Color.TRANSPARENT.toDrawable())
+        val attr = window.attributes
+        attr.gravity = Gravity.CENTER
+        attr.dimAmount = 0.6f
+        window.attributes = attr
+    }
+}

+ 82 - 0
app/src/main/java/com/adealink/weparty/module/message/todayfate/TodayFateDialogTask.kt

@@ -0,0 +1,82 @@
+package com.adealink.weparty.module.message.todayfate
+
+import android.os.Bundle
+import androidx.fragment.app.FragmentActivity
+import androidx.lifecycle.lifecycleScope
+import com.adealink.frame.base.IError
+import com.adealink.frame.base.Rlt
+import com.adealink.frame.coroutine.dispatcher.Dispatcher
+import com.adealink.frame.router.Router
+import com.adealink.weparty.commonui.dialogchain.BaseDialogData
+import com.adealink.weparty.commonui.dialogchain.BaseDialogTask
+import com.adealink.weparty.commonui.dialogchain.Priority
+import com.adealink.weparty.commonui.dialogfragment.BaseDialogFragment
+import com.adealink.weparty.module.message.Message
+import com.adealink.weparty.module.operation.OperationModule
+import com.adealink.weparty.module.operation.recommend.RecommendOppositeSexAckReq
+import com.adealink.weparty.module.profile.data.UserInfo
+import com.adealink.weparty.ui.home.util.HomeLocalService
+import com.adealink.weparty.ui.home.util.HomeUIUtil
+import kotlinx.coroutines.launch
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+
+/**
+ * Created by LfJ on 2025/6/17.
+ */
+class TodayFateDialogTask(val recommendId: String, val userInfos: List<UserInfo>) :
+    BaseDialogTask<BaseDialogData.TodayFateDialogData>() {
+
+    companion object {
+        private const val TAG = "TodayFateDialogTask"
+        private const val MAX_COUNT_SHOW_TODAY_FATE = 3
+    }
+
+    override val priority: Int
+        get() = Priority.LOW.priority
+    override val tag: String
+        get() = TAG
+
+    override suspend fun canShow(): Rlt<BaseDialogData.TodayFateDialogData> {
+        if (HomeLocalService.showTodayFateCount >= MAX_COUNT_SHOW_TODAY_FATE && HomeLocalService.lastShowTodayFateDate == getTodayString()) {
+            return Rlt.Failed(IError("今日展示推荐女性弹窗次数已达到最大数量"))
+        }
+        return Rlt.Success(BaseDialogData.TodayFateDialogData(recommendId, userInfos))
+    }
+
+    override fun showDialog(
+        dialogData: BaseDialogData.TodayFateDialogData, fragmentActivity: FragmentActivity
+    ): Int? {
+        var fragment: BaseDialogFragment? = null
+        if (HomeUIUtil.isInUserListTab()) {
+            fragment = Router.getRouterInstance<BaseDialogFragment>(Message.TodayFate.PATH)?.apply {
+                arguments = Bundle().apply {
+                    putParcelableArrayList(
+                        Message.TodayFate.EXTRA_USER_INFOS, ArrayList(dialogData.userInfos)
+                    )
+                }
+            }
+            fragment?.show(fragmentActivity.supportFragmentManager)
+            if (HomeLocalService.lastShowTodayFateDate == getTodayString()) {
+                val todayCount = HomeLocalService.showTodayFateCount
+                HomeLocalService.showTodayFateCount = todayCount + 1
+            } else {
+                HomeLocalService.showTodayFateCount = 1
+            }
+            HomeLocalService.lastShowTodayFateDate = getTodayString()
+            fragment?.lifecycleScope?.launch((Dispatcher.WENEXT_THREAD_POOL)) {
+                OperationModule.recommendOppositeSexPopupAck(
+                    RecommendOppositeSexAckReq(
+                        dialogData.recommendId, dialogData.userInfos.map { it.uid })
+                )
+            }
+        }
+        return fragment?.hashCode()
+    }
+
+    private fun getTodayString(): String {
+        val sdf = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
+        return sdf.format(Date())
+    }
+}

+ 53 - 0
app/src/main/java/com/adealink/weparty/module/message/todayfate/TodayFateUserItemViewBinder.kt

@@ -0,0 +1,53 @@
+package com.adealink.weparty.module.message.todayfate
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import com.adealink.frame.aab.util.getCompatColor
+import com.adealink.frame.router.Router
+import com.adealink.weparty.R
+import com.adealink.weparty.commonui.ext.dpf
+import com.adealink.weparty.commonui.ext.getActivity
+import com.adealink.weparty.commonui.ext.show
+import com.adealink.weparty.commonui.recycleview.adapter.BindingViewHolder
+import com.adealink.weparty.commonui.recycleview.adapter.multitype.ItemViewBinder
+import com.adealink.weparty.databinding.ItemTodayFateUserBinding
+import com.adealink.weparty.module.profile.Profile
+import com.adealink.weparty.module.profile.data.UserInfo
+
+/**
+ * Created by LfJ on 2025/8/27.
+ */
+class TodayFateUserItemViewBinder :
+    ItemViewBinder<UserInfo, BindingViewHolder<ItemTodayFateUserBinding>>() {
+    override fun onCreateViewHolder(
+        inflater: LayoutInflater, parent: ViewGroup
+    ): BindingViewHolder<ItemTodayFateUserBinding> {
+        val binding = ItemTodayFateUserBinding.inflate(inflater, parent, false)
+        return BindingViewHolder(binding)
+    }
+
+    override fun onBindViewHolder(
+        holder: BindingViewHolder<ItemTodayFateUserBinding>, item: UserInfo
+    ) {
+        holder.binding.apply {
+            item.let {
+                ivAvatar.apply {
+                    setImageUrl(it.url)
+                    hierarchy.roundingParams = hierarchy.roundingParams?.apply {
+                        borderColor = getCompatColor(R.color.color_FFFFFF)
+                        borderWidth = 1.dpf()
+                    }
+                }
+                ivOnline.show(true)
+                tvUserName.text = it.name
+                userCountryView.setUserInfo(it)
+            }
+            root.setOnClickListener {
+                val activity = holder.binding.root.getActivity() ?: return@setOnClickListener
+                Router.build(activity, Profile.UserProfile.PATH)
+                    .putExtra(Profile.Common.EXTRA_UID, item.uid)
+                    .start()
+            }
+        }
+    }
+}

+ 5 - 1
app/src/main/java/com/adealink/weparty/module/message/viewmodel/IMessageViewModel.kt

@@ -6,10 +6,11 @@ import com.adealink.frame.mvvm.livedata.ExtLiveData
 import com.adealink.weparty.module.couple.data.IntimacyPrivilegeType
 import com.adealink.weparty.module.message.data.ChatPageDetailRes
 import com.adealink.weparty.module.message.data.CustomerInfo
-import com.adealink.weparty.module.message.data.FamilyImInfoRsp
 import com.adealink.weparty.module.message.data.GetCustomQuickMessageRes
 import com.adealink.weparty.module.message.data.Im1v1ContinuousSendMessageDeductNotifyInfo
 import com.adealink.weparty.module.message.data.QuickMessageInfo
+import com.adealink.weparty.module.message.data.UnreadPaidSessionListReq
+import com.adealink.weparty.module.message.data.UnreadPaidSessionListRsp
 
 interface IMessageViewModel {
     val chatPageDetailResLD: ExtLiveData<Rlt<ChatPageDetailRes>>
@@ -17,6 +18,7 @@ interface IMessageViewModel {
     val editedCustomQuickMessageLD: ExtLiveData<Pair<Int, QuickMessageInfo>> // first: index, second: QuickMessageInfo
     val sayHiLD: ExtLiveData<Long>
     val im1v1ContinuousSendMessageDeductNotifyLD: ExtLiveData<Im1v1ContinuousSendMessageDeductNotifyInfo>
+    val unreadPaidSessionListRspLD: ExtLiveData<UnreadPaidSessionListRsp>
 
     fun getChatPageDetail(toUid: Long, cache: Boolean = true): LiveData<Rlt<ChatPageDetailRes>>
 
@@ -48,4 +50,6 @@ interface IMessageViewModel {
     fun getCustomerInfo(): LiveData<Rlt<CustomerInfo>>
 
     fun getDiamondAgentOrderStatus(orderId: String): LiveData<Rlt<Int>>
+
+    fun getUnreadPaidSessionList(req: UnreadPaidSessionListReq)
 }

+ 5 - 0
app/src/main/java/com/adealink/weparty/module/operation/IOperationService.kt

@@ -8,6 +8,7 @@ import com.adealink.frame.base.Rlt
 import com.adealink.weparty.module.anchor.data.AnchorMessage
 import com.adealink.weparty.module.anchor.data.AnchorMessageReplyCode
 import com.adealink.weparty.module.operation.newuser.IBuyNewUserViewModel
+import com.adealink.weparty.module.operation.recommend.RecommendOppositeSexAckReq
 import com.adealink.weparty.module.operation.signinreward.viewmodel.ISignInViewModel
 import java.util.EnumMap
 
@@ -67,4 +68,8 @@ interface IOperationService : IService<IOperationService> {
     suspend fun getNewUserLotteryIsEnd(): Boolean
 
     fun handleDelayedNewUserRewardDialogTask(viewLifecycleOwner: LifecycleOwner)
+
+    suspend fun getRecommendOppositeSexPopup()
+
+    suspend fun recommendOppositeSexPopupAck(req: RecommendOppositeSexAckReq): Rlt<Any>
 }

+ 18 - 0
app/src/main/java/com/adealink/weparty/module/operation/OperationModule.kt

@@ -6,12 +6,14 @@ import androidx.lifecycle.ViewModelStoreOwner
 import com.adealink.frame.aab.BaseDynamicModule
 import com.adealink.frame.aab.constant.AABModuleNotInitError
 import com.adealink.frame.base.CommonDataNullError
+import com.adealink.frame.base.IError
 import com.adealink.frame.base.Rlt
 import com.adealink.weparty.R
 import com.adealink.weparty.module.anchor.data.AnchorMessage
 import com.adealink.weparty.module.anchor.data.AnchorMessageReplyCode
 import com.adealink.weparty.module.operation.newuser.IBuyNewUserViewModel
 import com.adealink.weparty.module.operation.rechargepackage.viewmodel.IRechargeDailyViewModel
+import com.adealink.weparty.module.operation.recommend.RecommendOppositeSexAckReq
 import com.adealink.weparty.module.operation.signinreward.viewmodel.ISignInViewModel
 import com.adealink.weparty.module.task.invite.InviteNewActivityShowInfo
 import java.util.EnumMap
@@ -246,6 +248,14 @@ object OperationModule : BaseDynamicModule<IOperationService>(IOperationService:
             override fun handleDelayedNewUserRewardDialogTask(viewLifecycleOwner: LifecycleOwner) {
 
             }
+
+            override suspend fun getRecommendOppositeSexPopup() {
+
+            }
+
+            override suspend fun recommendOppositeSexPopupAck(req: RecommendOppositeSexAckReq): Rlt<Any> {
+                return Rlt.Failed(IError())
+            }
         }
     }
 
@@ -268,4 +278,12 @@ object OperationModule : BaseDynamicModule<IOperationService>(IOperationService:
     override fun handleDelayedNewUserRewardDialogTask(viewLifecycleOwner: LifecycleOwner) {
         return getService().handleDelayedNewUserRewardDialogTask(viewLifecycleOwner)
     }
+
+    override suspend fun getRecommendOppositeSexPopup() {
+        getService().getRecommendOppositeSexPopup()
+    }
+
+    override suspend fun recommendOppositeSexPopupAck(req: RecommendOppositeSexAckReq): Rlt<Any> {
+        return getService().recommendOppositeSexPopupAck(req)
+    }
 }

+ 25 - 0
app/src/main/java/com/adealink/weparty/module/operation/recommend/Data.kt

@@ -0,0 +1,25 @@
+package com.adealink.weparty.module.operation.recommend
+
+import android.os.Parcelable
+import com.adealink.weparty.module.profile.data.UserInfo
+import com.google.gson.annotations.SerializedName
+import kotlinx.parcelize.Parcelize
+
+/**
+ * Created by LfJ on 2025/9/4.
+ */
+/**
+ * 推荐的异性弹窗
+ */
+@Parcelize
+data class RecommendOppositeSexPopupNotify(
+    @SerializedName("recommendId") val recommendId: String,
+    @SerializedName("userInfos") val userInfos: List<UserInfo>,
+): Parcelable
+
+
+data class RecommendOppositeSexAckReq(
+    @SerializedName("recommendId") val recommendId: String,
+    @SerializedName("uids") val uidList: List<Long>
+)
+

+ 0 - 9
app/src/main/java/com/adealink/weparty/module/userlist/viewmodel/UserListViewModel.kt

@@ -207,15 +207,6 @@ class UserListViewModel(
                     allListData.addAll(newDataList)
                     dataList.send(value = allListData)
                     Log.i(TAG_HOME_USER_LIST, "loadData, dataList.size=${newDataList.size}")
-
-                    //首次进入首页,才主动弹
-                    if (tabType == HomeUserTabType.Online && newRecommendUserInfoList.isNotEmpty()
-                        && HomeLocalService.homeTabVisitCount == 1) {
-                        DialogTaskManager.submit(
-                            UserGreetingDialogTask(
-                                uidList = recommendUserInfoList.map { it.uid })
-                        )
-                    }
                 }
 
                 is Rlt.Failed -> {

+ 7 - 1
app/src/main/java/com/adealink/weparty/push/NotificationUtil.kt

@@ -80,7 +80,7 @@ object NotificationUtil {
         }
     }
 
-    fun showNotification(pushMessage: WeNextPushMessage) {
+    fun showNotification(pushMessage: WeNextPushMessage, completeCallback: (() -> Unit)? = null) {
         if (pushMessage.pushId < 0 && pushMessage.messageType < 0) {
             return
         }
@@ -96,10 +96,12 @@ object NotificationUtil {
             override fun onSuccess(bitmap: Bitmap) {
                 super.onSuccess(bitmap)
                 showInBroadcastNotification(pushMessage, bitmap)
+                completeCallback?.invoke()
             }
             override fun onFailed() {
                 super.onFailed()
                 showInBroadcastNotification(pushMessage, null)
+                completeCallback?.invoke()
             }
         })
     }
@@ -177,6 +179,10 @@ object NotificationUtil {
         }
     }
 
+    fun cancelNotification(notifyId: Int) {
+        NotificationManagerCompat.from(AppUtil.appContext).cancel(notifyId)
+    }
+
     private fun needHandleNotification(messageType: Long): Boolean {
         return messageType == PushMessageType.CALL_MATCH_RESULT.type || messageType == PushMessageType.CALL_MATCH_ASK.type
     }

+ 2 - 1
app/src/main/java/com/adealink/weparty/push/data/Constants.kt

@@ -1,4 +1,5 @@
 package com.adealink.weparty.push.data
 
 const val KEY_LARGE_ICON_URL = "large_icon_url"
-const val KEY_TARGET_ID = "target_id"
+const val KEY_TARGET_ID = "target_id"
+const val KEY_MESSAGE_ID = "message_id"

+ 37 - 1
app/src/main/java/com/adealink/weparty/push/data/PushData.kt

@@ -7,6 +7,8 @@ import com.adealink.frame.push.data.KEY_PUSH_MESSAGE
 import com.adealink.frame.push.data.KEY_PUSH_MESSAGE_TYPE
 import com.adealink.frame.push.data.KEY_PUSH_TITLE
 import com.adealink.frame.push.data.PushMessage
+import com.adealink.weparty.module.anchor.data.AnchorMessageButton
+import com.adealink.weparty.module.message.data.SessionInfo
 import com.adealink.weparty.module.profile.data.UserInfo
 import com.google.gson.annotations.GsonNullable
 import com.google.gson.annotations.SerializedName
@@ -16,6 +18,7 @@ enum class PushMessageType(val type: Long) {
     IM(40), //1v1会话提示
     SEND_GIFT(41), //送礼提示
     INTIMACY_ONLINE(44), //亲密度上线提醒
+    INCOME_IM(45), //1v1付费消息
     CALL_MATCH_ASK(58), //匹配邀请
     CALL_MATCH_RESULT(62), //匹配结果通知
 }
@@ -50,16 +53,45 @@ data class NotificationMessage(
     var btnText: String? = null
     @GsonNullable
     var targetUserInfo: UserInfo? = null
+    @GsonNullable
+    var messageId: Int? = null
 
     fun toPushMessage(): PushMessage {
         val paramMap = hashMapOf(
             KEY_TARGET_ID to (targetId ?: ""),
-            KEY_LARGE_ICON_URL to (largeIconUrl ?: "")
+            KEY_LARGE_ICON_URL to (largeIconUrl ?: ""),
+            KEY_MESSAGE_ID to (messageId?.toString() ?: "")
         )
         return PushMessage(pushId, messageType, title, message, btnText, deeplink, paramMap)
     }
 }
 
+/**
+ * 未回复的付费消息通知
+ */
+data class NoReplyPaidMessageNotify(
+    @SerializedName("title") val title: String,
+    @SerializedName("content") val content: String,
+    @GsonNullable @SerializedName("messageSender") val messageSender: UserInfo? = null
+)
+
+/**
+ * 首次私聊获得钻石通知
+ */
+data class FirstGetDiamondsFromChatNotify(
+    @SerializedName("title") val title: String,
+    @SerializedName("content") val content: String,
+    @GsonNullable @SerializedName("button") val button: AnchorMessageButton? = null
+)
+
+/**
+ * 会话信息变动通知
+ */
+data class SessionInfoChangeNotify(
+    @SerializedName("sessionInfos") val sessionInfos: List<SessionInfo>,
+    @SerializedName("listTypes") val listTypes: List<Int>,
+)
+
 class WeNextPushMessage(pushMessage: PushMessage, val channelId: String) {
     var pushId: Long = pushMessage.pushId
     var messageType: Long = pushMessage.messageType
@@ -82,4 +114,8 @@ class WeNextPushMessage(pushMessage: PushMessage, val channelId: String) {
     fun getTargetId(): String? {
         return paramMap?.get(KEY_TARGET_ID)
     }
+
+    fun getMessageId(): String? {
+        return paramMap?.get(KEY_MESSAGE_ID)
+    }
 }

+ 29 - 0
app/src/main/java/com/adealink/weparty/ui/MainStartUpFragment.kt

@@ -13,6 +13,7 @@ import com.adealink.frame.util.AppUtil
 import com.adealink.weparty.BuildConfig
 import com.adealink.weparty.commonui.BaseActivity
 import com.adealink.weparty.commonui.BaseFragment
+import com.adealink.weparty.commonui.ext.onSuccess
 import com.adealink.weparty.commonui.ext.isUiValid
 import com.adealink.weparty.config.globalConfigManager
 import com.adealink.weparty.log.manager.logManager
@@ -41,11 +42,15 @@ import com.adealink.weparty.module.task.invite.InviteRewardManager
 import com.adealink.weparty.module.wallet.WalletModule
 import com.adealink.weparty.push.PushStatEvent
 import com.adealink.weparty.report.manager.dailyReportManager
+import com.adealink.weparty.ui.home.HomeFragment
+import com.adealink.weparty.ui.home.util.HomeLocalService
+import com.adealink.weparty.ui.tab.HomeTab
 import com.adealink.weparty.webview.manager.PreloadWebViewManager
 import com.adealink.weparty.webview.manager.PreloadWebViewWorker
 import com.google.firebase.crashlytics.ktx.crashlytics
 import com.google.firebase.crashlytics.ktx.setCustomKeys
 import com.google.firebase.ktx.Firebase
+import kotlinx.coroutines.launch
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
@@ -94,6 +99,7 @@ class MainStartUpFragment : BaseFragment() {
         BackpackModule.checkGiftBackpack()
         RoomModule.init()
 //        GameModule.checkPlayingGame()
+        checkNoReplyMessageIfNeed()
 
         runIfUiValid { roomAttrViewModel?.getMyRoomInfo(true) }
         runIfUiValid { countryViewModel?.getCountryList(true) }
@@ -102,6 +108,26 @@ class MainStartUpFragment : BaseFragment() {
         Log.d(TAG, "importantLoad-end, cost:${SystemClock.elapsedRealtime() - startTs}ms")
     }
 
+    private fun checkNoReplyMessageIfNeed() {
+        if (ProfileModule.getMyUserInfo()?.isFemale() == true) {
+            lifecycleScope.launch(Dispatcher.WENEXT_THREAD_POOL) {
+                MessageModule.getNoReplyMessage().onSuccess {
+                    val homeFragment =
+                        activity?.supportFragmentManager?.findFragmentByTag(HomeFragment.TAG) as? HomeFragment
+                    Log.i(
+                        TAG,
+                        "existNoReplyPaidMessage:${it?.existNoReplyPaidMessage} homeFragment?.isAdded:${homeFragment?.isAdded}"
+                    )
+                    if (it?.existNoReplyPaidMessage == true && homeFragment?.isAdded == true && HomeLocalService.userHomeTabNumber == HomeTab.USER_LIST.number) {
+                        lifecycleScope.launch(Dispatcher.UI) {
+                            homeFragment.setDefaultTab(HomeTab.MESSAGE.tab)
+                        }
+                    }
+                }
+            }
+        }
+    }
+
     private suspend fun minorTask() {
         withContext(Dispatcher.WENEXT_THREAD_POOL) {
             minorLoad()
@@ -146,6 +172,9 @@ class MainStartUpFragment : BaseFragment() {
         OperationModule.checkSuperSupporterWhatsAppFillStatus()
         runIfUiValid { familyInfoViewModel?.getApplyJoinFamilyUnHandleNum() }
         reportPushSwitch()
+        lifecycleScope.launch {
+            OperationModule.getRecommendOppositeSexPopup()
+        }
         Log.d(TAG, "otherLoad-end, cost:${SystemClock.elapsedRealtime() - startTs}ms")
     }
 

+ 23 - 0
app/src/main/java/com/adealink/weparty/ui/home/BaseHomeFragment.kt

@@ -96,6 +96,14 @@ abstract class BaseHomeFragment : BaseFragment, ITabManager {
     private val subTabKey by lazy {
         arguments?.getString(AppModule.Main.EXTRA_MAIN_SUB_TAB)
     }
+    private val onPageChangeCallback by lazy {
+        object : ViewPager2.OnPageChangeCallback() {
+            override fun onPageSelected(position: Int) {
+                super.onPageSelected(position)
+                updateUserListTabRecentTime(position)
+            }
+        }
+    }
 
     private var homeBannerEntranceFloatViewComp: HomeBannerEntranceFloatViewComp? = null
 
@@ -137,6 +145,7 @@ abstract class BaseHomeFragment : BaseFragment, ITabManager {
                 )
             }
         }.attach()
+        vpContent.registerOnPageChangeCallback(onPageChangeCallback)
         tlTab.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
 
             override fun onTabSelected(tab: TabLayout.Tab?) {
@@ -315,6 +324,11 @@ abstract class BaseHomeFragment : BaseFragment, ITabManager {
         }
     }
 
+    override fun onDestroyView() {
+        super.onDestroyView()
+        vpContent.unregisterOnPageChangeCallback(onPageChangeCallback)
+    }
+
     internal inner class HomeProfilePageAdapter : BaseTabFragmentStateAdapter(this) {
         override fun getTabName(pos: Int): String {
             return ""
@@ -372,4 +386,13 @@ abstract class BaseHomeFragment : BaseFragment, ITabManager {
         }
     }
 
+    private fun updateUserListTabRecentTime(position: Int) {
+        val userListTabIndex = HOME_TABS.indexOfFirst { it.type.tab == HomeTab.USER_LIST.tab }
+        if (userListTabIndex == position) {
+            HomeLocalService.setUserListTabRecentTime = System.currentTimeMillis()
+        } else {
+            HomeLocalService.setUserListTabRecentTime = -1L
+            HomeLocalService.showMessageReminderCount = 0
+        }
+    }
 }

+ 8 - 0
app/src/main/java/com/adealink/weparty/ui/home/util/HomeLocalService.kt

@@ -43,4 +43,12 @@ object HomeLocalService : TypeDelegationPrefs(
             HomeTab.map(tab)?.number ?: defaultHomeTabNumber
         }
     }
+
+    var showTodayFateCount: Int by PrefUserKey("key_show_today_fate_count", 0)
+
+    var lastShowTodayFateDate: String by PrefUserKey("key_last_show_today_fate_data", "")
+
+    var setUserListTabRecentTime: Long by PrefUserKey("key_set_user_list_tab_recent_time", -1L)
+
+    var showMessageReminderCount: Int by PrefUserKey("key_show_message_reminder_count", 0)
 }

BIN
app/src/main/res/drawable-xhdpi/ic_unread_conversation.webp


BIN
app/src/main/res/drawable-xhdpi/message_dialog_close_ic.webp


BIN
app/src/main/res/drawable-xhdpi/message_dialog_title_bg.webp


+ 0 - 0
module/message/src/main/res/drawable-xhdpi/im_price_diamond_icon.webp → app/src/main/res/drawable-xhdpi/message_price_diamond_icon.webp


BIN
app/src/main/res/drawable-xhdpi/message_reminder_desc_bg.9.png


BIN
app/src/main/res/drawable-xhdpi/message_reminder_msg_content_bg.webp


BIN
app/src/main/res/drawable-xhdpi/message_reply_reward_head_bg.webp


BIN
app/src/main/res/drawable-xhdpi/message_reply_reward_ic.webp


BIN
app/src/main/res/drawable-xhdpi/message_reply_reward_red_packet_ic.webp


BIN
app/src/main/res/drawable-xhdpi/message_today_fate_content_bg.9.png


BIN
app/src/main/res/drawable-xhdpi/message_today_fate_head_bg.webp


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

+ 9 - 0
app/src/main/res/drawable/message_income_reminder_content_bg.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <gradient
+        android:angle="270"
+        android:endColor="@color/color_FFE4F3"
+        android:startColor="@color/color_FF6FD9" />
+    <corners android:radius="24dp" />
+</shape>

+ 9 - 0
app/src/main/res/drawable/message_reminder_dialog_bg.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <gradient
+        android:angle="270"
+        android:endColor="@color/color_FFE4F3"
+        android:startColor="@color/color_FF6FD9" />
+    <corners android:radius="24dp" />
+</shape>

+ 9 - 0
app/src/main/res/drawable/message_reply_reward_content_bg.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <gradient
+        android:angle="270"
+        android:endColor="@color/color_FFFDEA"
+        android:startColor="@color/color_FFD059" />
+    <corners android:radius="12dp" />
+</shape>

+ 6 - 0
app/src/main/res/drawable/message_reply_reward_view_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/color_FF8700" />
+    <corners android:radius="35dp" />
+</shape>

+ 201 - 0
app/src/main/res/layout/dialog_income_message_reminder.xml

@@ -0,0 +1,201 @@
+<?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">
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/cl_content"
+        android:layout_width="300dp"
+        android:layout_height="wrap_content"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        tools:ignore="MissingConstraints">
+
+        <androidx.appcompat.widget.AppCompatImageView
+            android:id="@+id/iv_bg"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:layout_marginTop="5dp"
+            android:background="@drawable/message_income_reminder_content_bg"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent" />
+
+        <androidx.appcompat.widget.AppCompatImageView
+            android:id="@+id/iv_title_bg"
+            android:layout_width="198dp"
+            android:layout_height="36dp"
+            android:src="@drawable/message_dialog_title_bg"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent" />
+
+        <com.adealink.weparty.commonui.widget.MediumTextView
+            android:id="@+id/tv_title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="4dp"
+            android:gravity="center"
+            android:includeFontPadding="true"
+            android:singleLine="true"
+            android:text="@string/message_income_reminder_title"
+            android:textColor="@color/color_FFFFFF"
+            android:textSize="16sp"
+            app:layout_constraintEnd_toEndOf="@+id/iv_title_bg"
+            app:layout_constraintStart_toStartOf="@+id/iv_title_bg"
+            app:layout_constraintTop_toTopOf="@+id/iv_title_bg" />
+
+        <androidx.appcompat.widget.AppCompatImageView
+            android:id="@+id/btn_close"
+            android:layout_width="24dp"
+            android:layout_height="24dp"
+            android:layout_marginTop="15dp"
+            android:layout_marginEnd="10dp"
+            android:src="@drawable/message_dialog_close_ic"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toTopOf="parent" />
+
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:id="@+id/cl_reminder_desc"
+            android:layout_width="276dp"
+            android:layout_height="60dp"
+            android:layout_marginTop="30dp"
+            android:background="@drawable/message_reminder_desc_bg"
+            android:paddingBottom="12dp"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/tv_title"
+            tools:ignore="MissingConstraints">
+
+            <androidx.appcompat.widget.AppCompatTextView
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginHorizontal="8dp"
+                android:text="@string/message_income_reminder_desc"
+                android:textColor="@color/color_FFFFFF"
+                android:textSize="12sp"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="parent" />
+
+        </androidx.constraintlayout.widget.ConstraintLayout>
+
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:id="@+id/cl_reminder_content"
+            android:layout_width="300dp"
+            android:layout_height="98dp"
+            android:background="@drawable/message_reminder_msg_content_bg"
+            android:paddingHorizontal="22dp"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/cl_reminder_desc"
+            tools:ignore="MissingConstraints">
+
+            <!-- Avatar -->
+            <com.adealink.weparty.commonui.imageview.AvatarView
+                android:id="@+id/iv_avatar"
+                android:layout_width="52dp"
+                android:layout_height="52dp"
+                android:src="@drawable/default_avatar"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="parent" />
+
+            <com.adealink.frame.dot.DotView
+                android:id="@+id/dot_view"
+                style="@style/CommonRedDot"
+                android:layout_marginEnd="-3dp"
+                app:layout_constraintEnd_toEndOf="@+id/iv_avatar"
+                app:layout_constraintTop_toTopOf="@+id/iv_avatar" />
+
+            <androidx.constraintlayout.widget.ConstraintLayout
+                android:id="@+id/cl_user_info"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="6dp"
+                app:layout_constraintBottom_toTopOf="@+id/tv_message_content"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toEndOf="@+id/iv_avatar"
+                app:layout_constraintTop_toTopOf="parent"
+                app:layout_constraintVertical_chainStyle="packed"
+                tools:ignore="MissingConstraints">
+
+                <com.adealink.weparty.module.profile.view.UserCountryView
+                    android:id="@+id/user_country_view"
+                    android:layout_width="16dp"
+                    android:layout_height="16dp"
+                    android:layout_marginEnd="6dp"
+                    app:layout_constraintBottom_toBottomOf="parent"
+                    app:layout_constraintEnd_toStartOf="@+id/user_sex_view"
+                    app:layout_constraintStart_toStartOf="parent"
+                    app:layout_constraintTop_toTopOf="parent"
+                    tools:background="@drawable/label_good_id_bg"
+                    tools:ignore="MissingConstraints" />
+
+                <com.adealink.weparty.module.level.label.UserSexView
+                    android:id="@+id/user_sex_view"
+                    android:layout_width="wrap_content"
+                    android:layout_height="18dp"
+                    android:layout_marginEnd="6dp"
+                    app:layout_constraintBottom_toBottomOf="parent"
+                    app:layout_constraintEnd_toStartOf="@+id/tv_user_name"
+                    app:layout_constraintStart_toEndOf="@+id/user_country_view"
+                    app:layout_constraintTop_toTopOf="parent" />
+
+                <!-- Username -->
+                <TextView
+                    android:id="@+id/tv_user_name"
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:ellipsize="end"
+                    android:gravity="start"
+                    android:includeFontPadding="false"
+                    android:singleLine="true"
+                    android:textAlignment="viewStart"
+                    android:textColor="@color/color_222222"
+                    android:textSize="15sp"
+                    app:layout_constraintBottom_toBottomOf="parent"
+                    app:layout_constraintEnd_toEndOf="parent"
+                    app:layout_constraintStart_toEndOf="@id/user_sex_view"
+                    app:layout_constraintTop_toTopOf="parent"
+                    tools:text="User Name" />
+
+            </androidx.constraintlayout.widget.ConstraintLayout>
+
+            <androidx.appcompat.widget.AppCompatTextView
+                android:id="@+id/tv_message_content"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="6dp"
+                android:ellipsize="end"
+                android:singleLine="true"
+                android:textColor="@color/color_777777"
+                android:textSize="12sp"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintEnd_toEndOf="@+id/cl_user_info"
+                app:layout_constraintStart_toStartOf="@+id/cl_user_info"
+                app:layout_constraintTop_toBottomOf="@+id/cl_user_info"
+                tools:text="Hello! Do you have time for dinner" />
+
+        </androidx.constraintlayout.widget.ConstraintLayout>
+
+        <com.opensource.svgaplayer.WenextSvgaView
+            android:id="@+id/reply_btn"
+            android:layout_width="269dp"
+            android:layout_height="70dp"
+            android:layout_marginBottom="24dp"
+            app:autoPlay="true"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/cl_reminder_content" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 201 - 0
app/src/main/res/layout/dialog_message_reminder.xml

@@ -0,0 +1,201 @@
+<?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">
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="300dp"
+        android:layout_height="wrap_content"
+        tools:ignore="MissingConstraints">
+
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:id="@+id/cl_content"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="101dp"
+            android:background="@drawable/message_reminder_dialog_bg"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:ignore="MissingConstraints">
+
+            <com.adealink.weparty.commonui.widget.MediumTextView
+                android:id="@+id/tv_title"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="23dp"
+                android:gravity="center"
+                android:includeFontPadding="true"
+                android:singleLine="true"
+                android:text="@string/message_reminder_title"
+                android:textColor="@color/color_FFFFFF"
+                android:textSize="18sp"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="parent" />
+
+            <androidx.appcompat.widget.AppCompatImageView
+                android:id="@+id/btn_close"
+                android:layout_width="24dp"
+                android:layout_height="24dp"
+                android:layout_marginTop="8dp"
+                android:layout_marginEnd="8dp"
+                android:src="@drawable/message_dialog_close_ic"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintTop_toTopOf="parent" />
+
+            <androidx.constraintlayout.widget.ConstraintLayout
+                android:id="@+id/cl_reminder_desc"
+                android:layout_width="276dp"
+                android:layout_height="60dp"
+                android:layout_marginTop="12dp"
+                android:background="@drawable/message_reminder_desc_bg"
+                android:paddingBottom="12dp"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/tv_title"
+                tools:ignore="MissingConstraints">
+
+                <androidx.appcompat.widget.AppCompatTextView
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_marginHorizontal="8dp"
+                    android:gravity="center"
+                    android:text="@string/message_reminder_desc"
+                    android:textColor="@color/color_FFFFFF"
+                    android:textSize="12sp"
+                    app:layout_constraintBottom_toBottomOf="parent"
+                    app:layout_constraintEnd_toEndOf="parent"
+                    app:layout_constraintStart_toStartOf="parent"
+                    app:layout_constraintTop_toTopOf="parent" />
+
+            </androidx.constraintlayout.widget.ConstraintLayout>
+
+            <androidx.constraintlayout.widget.ConstraintLayout
+                android:id="@+id/cl_reminder_content"
+                android:layout_width="300dp"
+                android:layout_height="98dp"
+                android:background="@drawable/message_reminder_msg_content_bg"
+                android:paddingHorizontal="22dp"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/cl_reminder_desc"
+                tools:ignore="MissingConstraints">
+
+                <!-- Avatar -->
+                <com.adealink.weparty.commonui.imageview.AvatarView
+                    android:id="@+id/iv_avatar"
+                    android:layout_width="52dp"
+                    android:layout_height="52dp"
+                    android:src="@drawable/default_avatar"
+                    app:layout_constraintBottom_toBottomOf="parent"
+                    app:layout_constraintStart_toStartOf="parent"
+                    app:layout_constraintTop_toTopOf="parent" />
+
+                <com.adealink.frame.dot.DotView
+                    android:id="@+id/dot_view"
+                    style="@style/CommonRedDot"
+                    android:layout_marginEnd="-3dp"
+                    app:layout_constraintEnd_toEndOf="@+id/iv_avatar"
+                    app:layout_constraintTop_toTopOf="@+id/iv_avatar" />
+
+                <androidx.constraintlayout.widget.ConstraintLayout
+                    android:id="@+id/cl_user_info"
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:layout_marginStart="6dp"
+                    app:layout_constraintBottom_toTopOf="@+id/tv_message_content"
+                    app:layout_constraintEnd_toEndOf="parent"
+                    app:layout_constraintStart_toEndOf="@+id/iv_avatar"
+                    app:layout_constraintTop_toTopOf="parent"
+                    app:layout_constraintVertical_chainStyle="packed"
+                    tools:ignore="MissingConstraints">
+
+                    <com.adealink.weparty.module.profile.view.UserCountryView
+                        android:id="@+id/user_country_view"
+                        android:layout_width="16dp"
+                        android:layout_height="16dp"
+                        android:layout_marginEnd="6dp"
+                        app:layout_constraintBottom_toBottomOf="parent"
+                        app:layout_constraintEnd_toStartOf="@+id/user_sex_view"
+                        app:layout_constraintStart_toStartOf="parent"
+                        app:layout_constraintTop_toTopOf="parent"
+                        tools:background="@drawable/label_good_id_bg"
+                        tools:ignore="MissingConstraints" />
+
+                    <com.adealink.weparty.module.level.label.UserSexView
+                        android:id="@+id/user_sex_view"
+                        android:layout_width="wrap_content"
+                        android:layout_height="18dp"
+                        android:layout_marginEnd="6dp"
+                        app:layout_constraintBottom_toBottomOf="parent"
+                        app:layout_constraintEnd_toStartOf="@+id/tv_user_name"
+                        app:layout_constraintStart_toEndOf="@+id/user_country_view"
+                        app:layout_constraintTop_toTopOf="parent" />
+
+                    <!-- Username -->
+                    <TextView
+                        android:id="@+id/tv_user_name"
+                        android:layout_width="0dp"
+                        android:layout_height="wrap_content"
+                        android:ellipsize="end"
+                        android:gravity="start"
+                        android:includeFontPadding="false"
+                        android:singleLine="true"
+                        android:textAlignment="viewStart"
+                        android:textColor="@color/color_222222"
+                        android:textSize="15sp"
+                        app:layout_constraintBottom_toBottomOf="parent"
+                        app:layout_constraintEnd_toEndOf="parent"
+                        app:layout_constraintStart_toEndOf="@id/user_sex_view"
+                        app:layout_constraintTop_toTopOf="parent"
+                        tools:text="User Name" />
+
+                </androidx.constraintlayout.widget.ConstraintLayout>
+
+                <androidx.appcompat.widget.AppCompatTextView
+                    android:id="@+id/tv_message_content"
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:layout_marginTop="6dp"
+                    android:ellipsize="end"
+                    android:singleLine="true"
+                    android:textColor="@color/color_777777"
+                    android:textSize="12sp"
+                    app:layout_constraintBottom_toBottomOf="parent"
+                    app:layout_constraintEnd_toEndOf="@+id/cl_user_info"
+                    app:layout_constraintStart_toStartOf="@+id/cl_user_info"
+                    app:layout_constraintTop_toBottomOf="@+id/cl_user_info"
+                    tools:text="Hello! Do you have time for dinner" />
+
+            </androidx.constraintlayout.widget.ConstraintLayout>
+
+            <com.opensource.svgaplayer.WenextSvgaView
+                android:id="@+id/reply_btn"
+                android:layout_width="269dp"
+                android:layout_height="70dp"
+                android:layout_marginTop="11dp"
+                android:layout_marginBottom="24dp"
+                app:autoPlay="true"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/cl_reminder_content" />
+
+        </androidx.constraintlayout.widget.ConstraintLayout>
+
+        <com.adealink.weparty.effect.WeAnimView
+            android:id="@+id/head_animation_view"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            app:layout_constraintDimensionRatio="486:195"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 126 - 0
app/src/main/res/layout/dialog_message_reply_reward.xml

@@ -0,0 +1,126 @@
+<?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">
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/cl_content"
+        android:layout_width="283dp"
+        android:layout_height="wrap_content"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        tools:ignore="MissingConstraints">
+
+        <androidx.appcompat.widget.AppCompatImageView
+            android:id="@+id/btn_close"
+            android:layout_width="24dp"
+            android:layout_height="24dp"
+            android:layout_marginTop="40dp"
+            android:src="@drawable/common_close"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toTopOf="parent" />
+
+        <androidx.appcompat.widget.AppCompatImageView
+            android:id="@+id/iv_head_bg"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:src="@drawable/message_reply_reward_head_bg"
+            app:layout_constraintDimensionRatio="556:371"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent" />
+
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:id="@+id/cl_reward_content"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="130dp"
+            android:background="@drawable/message_reply_reward_content_bg"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:ignore="MissingConstraints">
+
+            <androidx.appcompat.widget.AppCompatTextView
+                android:id="@+id/tv_title"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="74dp"
+                tools:text="title"
+                android:textColor="@color/color_FF8700"
+                android:textSize="20sp"
+                android:textStyle="bold"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="parent" />
+
+            <androidx.appcompat.widget.AppCompatTextView
+                android:id="@+id/tv_desc"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginHorizontal="16dp"
+                android:layout_marginTop="8dp"
+                tools:text="content"
+                android:textColor="@color/color_9D611C"
+                android:textSize="15sp"
+                android:gravity="center"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/tv_title" />
+
+            <androidx.constraintlayout.widget.ConstraintLayout
+                android:id="@+id/btn_view"
+                android:layout_width="160dp"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="20dp"
+                android:layout_marginBottom="40dp"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/tv_desc"
+                tools:ignore="MissingConstraints">
+
+                <androidx.appcompat.widget.AppCompatTextView
+                    android:layout_width="match_parent"
+                    android:layout_height="40dp"
+                    android:background="@drawable/message_reply_reward_view_bg"
+                    android:gravity="center"
+                    android:text="@string/common_go"
+                    android:textColor="@color/color_FFFFFF"
+                    android:textSize="16sp"
+                    android:textStyle="bold"
+                    app:layout_constraintBottom_toBottomOf="parent"
+                    app:layout_constraintEnd_toEndOf="parent"
+                    app:layout_constraintStart_toStartOf="parent" />
+
+                <androidx.appcompat.widget.AppCompatImageView
+                    android:id="@+id/iv_red_packet"
+                    android:layout_width="16dp"
+                    android:layout_height="16dp"
+                    android:layout_marginEnd="4dp"
+                    android:layout_marginBottom="30dp"
+                    android:src="@drawable/message_reply_reward_red_packet_ic"
+                    app:layout_constraintBottom_toBottomOf="parent"
+                    app:layout_constraintEnd_toEndOf="parent" />
+
+            </androidx.constraintlayout.widget.ConstraintLayout>
+
+        </androidx.constraintlayout.widget.ConstraintLayout>
+
+        <androidx.appcompat.widget.AppCompatImageView
+            android:id="@+id/iv_reward"
+            android:layout_width="94dp"
+            android:layout_height="105dp"
+            android:layout_marginTop="77dp"
+            android:src="@drawable/message_reply_reward_ic"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 108 - 0
app/src/main/res/layout/dialog_today_fate.xml

@@ -0,0 +1,108 @@
+<?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">
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="300dp"
+        android:layout_height="wrap_content"
+        tools:ignore="MissingConstraints">
+
+        <androidx.appcompat.widget.AppCompatImageView
+            android:id="@+id/iv_head_bg"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:layout_marginBottom="-6dp"
+            android:src="@drawable/message_today_fate_head_bg"
+            app:layout_constraintBottom_toTopOf="@+id/cl_content"
+            app:layout_constraintDimensionRatio="750:270"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent" />
+
+        <com.opensource.svgaplayer.WenextSvgaView
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            app:autoPlay="true"
+            app:layout_constraintBottom_toBottomOf="@+id/iv_head_bg"
+            app:layout_constraintDimensionRatio="328:120"
+            app:layout_constraintEnd_toEndOf="@+id/iv_head_bg"
+            app:layout_constraintStart_toStartOf="@+id/iv_head_bg"
+            app:source="today_fate_head_animation_bg.svga" />
+
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:id="@+id/cl_content"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="129dp"
+            android:background="@drawable/message_today_fate_content_bg"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:ignore="MissingConstraints">
+
+            <com.opensource.svgaplayer.WenextSvgaView
+                android:layout_width="0dp"
+                android:layout_height="0dp"
+                app:autoPlay="true"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="parent"
+                app:source="today_fate_content_animation_bg.svga" />
+
+            <androidx.appcompat.widget.AppCompatTextView
+                android:id="@+id/tv_title"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="4dp"
+                android:gravity="center"
+                android:includeFontPadding="true"
+                android:maxWidth="100dp"
+                android:singleLine="true"
+                android:text="@string/message_today_fate_title"
+                android:textColor="@color/color_FFFFFF"
+                android:textSize="16sp"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="parent" />
+
+            <androidx.appcompat.widget.AppCompatImageView
+                android:id="@+id/btn_close"
+                android:layout_width="24dp"
+                android:layout_height="24dp"
+                android:layout_marginTop="13dp"
+                android:layout_marginEnd="8dp"
+                android:src="@drawable/message_dialog_close_ic"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintTop_toTopOf="parent" />
+
+            <androidx.recyclerview.widget.RecyclerView
+                android:id="@+id/user_list_view"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="30dp"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/tv_title" />
+
+            <com.opensource.svgaplayer.WenextSvgaView
+                android:id="@+id/send_love_btn"
+                android:layout_width="264dp"
+                android:layout_height="72dp"
+                android:layout_marginTop="10dp"
+                android:layout_marginBottom="12dp"
+                app:autoPlay="true"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/user_list_view"
+                app:source="today_fate_btn.svga" />
+
+        </androidx.constraintlayout.widget.ConstraintLayout>
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 62 - 0
app/src/main/res/layout/item_today_fate_user.xml

@@ -0,0 +1,62 @@
+<?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="110dp"
+    android:layout_height="98dp">
+
+    <!-- Avatar -->
+    <com.adealink.weparty.commonui.imageview.AvatarView
+        android:id="@+id/iv_avatar"
+        android:layout_width="66dp"
+        android:layout_height="66dp"
+        android:src="@drawable/default_avatar"
+        app:layout_constraintBottom_toTopOf="@+id/tv_user_name"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintVertical_chainStyle="packed" />
+
+    <androidx.appcompat.widget.AppCompatImageView
+        android:id="@+id/iv_online"
+        android:layout_width="10dp"
+        android:layout_height="10dp"
+        android:layout_gravity="top|end"
+        android:layout_marginTop="2dp"
+        android:layout_marginEnd="7dp"
+        android:background="@drawable/common_online_ic"
+        app:layout_constraintEnd_toEndOf="@+id/iv_avatar"
+        app:layout_constraintTop_toTopOf="@+id/iv_avatar" />
+
+    <com.adealink.weparty.module.profile.view.UserCountryView
+        android:id="@+id/user_country_view"
+        android:layout_width="16dp"
+        android:layout_height="16dp"
+        android:layout_marginEnd="5dp"
+        app:layout_constraintBottom_toBottomOf="@+id/tv_user_name"
+        app:layout_constraintEnd_toStartOf="@+id/tv_user_name"
+        app:layout_constraintHorizontal_chainStyle="packed"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="@+id/tv_user_name"
+        tools:background="@drawable/label_good_id_bg"
+        tools:ignore="MissingConstraints" />
+
+    <!-- Username -->
+    <TextView
+        android:id="@+id/tv_user_name"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="8dp"
+        android:ellipsize="end"
+        android:includeFontPadding="false"
+        android:maxWidth="83dp"
+        android:singleLine="true"
+        android:textColor="@color/color_FFFFFF"
+        android:textSize="15sp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toEndOf="@id/user_country_view"
+        app:layout_constraintTop_toBottomOf="@+id/iv_avatar"
+        tools:text="User Name" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 57 - 0
app/src/main/res/layout/layout_unread_conversation.xml

@@ -0,0 +1,57 @@
+<?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:id="@+id/root_container"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content">
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/cl_unread"
+        android:layout_width="wrap_content"
+        android:layout_height="25dp"
+        android:background="@drawable/bg_unread_conversation"
+        android:paddingHorizontal="10dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        tools:ignore="MissingConstraints">
+
+        <androidx.appcompat.widget.AppCompatTextView
+            android:id="@+id/tv_unread"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/message_unread_conversation"
+            android:textColor="@color/color_FFFFFF"
+            android:textSize="11sp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toStartOf="@+id/iv_unread"
+            app:layout_constraintHorizontal_chainStyle="packed"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent" />
+
+        <androidx.appcompat.widget.AppCompatImageView
+            android:id="@+id/iv_unread"
+            android:layout_width="12dp"
+            android:layout_height="12dp"
+            android:background="@drawable/ic_unread_conversation"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toEndOf="@+id/tv_unread"
+            app:layout_constraintTop_toTopOf="parent" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+    <com.adealink.frame.dot.DotView
+        android:id="@+id/dot_view"
+        style="@style/CommonRedDot"
+        android:layout_width="9dp"
+        android:layout_height="9dp"
+        android:layout_marginEnd="3dp"
+        android:layout_marginBottom="-4dp"
+        android:visibility="gone"
+        app:layout_constraintBottom_toTopOf="@+id/cl_unread"
+        app:layout_constraintEnd_toEndOf="parent"
+        tools:visibility="visible" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 8 - 0
app/src/main/res/values-ar/strings.xml

@@ -720,6 +720,14 @@
     <string name="family_role_admin">إدارة الأسرة</string>
     <string name="common_random_chat">دردشة عشوائية</string>
     <string name="common_response_timeout">مهلة الاستجابة</string>
+    <string name="message_income_reminder_title">أرباح اليوم</string>
+    <string name="message_reply">الرد الآن</string>
+    <string name="message_reminder_title">رسالة كيوبيد</string>
+    <string name="message_send_love">أرسل الحب</string>
+    <string name="message_today_fate_title">اليوم القدر</string>
+    <string name="message_income_reminder_desc">لقد أرسل لك شاب جميل رسالة، قم بالرد بسرعة!</string>
+    <string name="message_reminder_desc">لقد أرسلت لك فتاة جميلة رسالة، قم بالرد بسرعة!</string>
+    <string name="message_unread_conversation">التالي غير مقروء</string>
     <string name="common_can_not_send">غير قادر على الإرسال</string>
     <string name="common_reward_records">سجلات المكافآت</string>
     <string name="commonui_danmaku">دانماكو</string>

+ 8 - 0
app/src/main/res/values-zh/strings.xml

@@ -719,6 +719,14 @@
     <string name="family_role_admin">家庭管理员</string>
     <string name="common_random_chat">随机聊天</string>
     <string name="common_response_timeout">响应超时</string>
+    <string name="message_income_reminder_title">今日收益</string>
+    <string name="message_reply">立即回复</string>
+    <string name="message_reminder_title">丘比特的信息</string>
+    <string name="message_send_love">传递爱</string>
+    <string name="message_today_fate_title">今日命运</string>
+    <string name="message_income_reminder_desc">有美少年给你留言了,快点回复吧!</string>
+    <string name="message_reminder_desc">有美女给你发信息,快点回复吧!</string>
+    <string name="message_unread_conversation">下一篇未读</string>
     <string name="common_can_not_send">无法发送</string>
     <string name="common_reward_records">奖励记录</string>
     <string name="commonui_danmaku">弹幕</string>

+ 6 - 0
app/src/main/res/values/colors.xml

@@ -1174,6 +1174,12 @@
     <color name="color_31C7A3">#31C7A3</color>
     <color name="color_C75325">#C75325</color>
     <color name="color_FFAA01">#FFAA01</color>
+    <color name="color_FF6FD9">#FF6FD9</color>
+    <color name="color_FFE4F3">#FFE4F3</color>
+    <color name="color_FFD059">#FFD059</color>
+    <color name="color_FFFDEA">#FFFDEA</color>
+    <color name="color_9D611C">#9D611C</color>
+    <color name="color_FFF2E3">#FFF2E3</color>
     <color name="color_FFC251FF">#FFC251FF</color>
     <color name="color_FFF051FF">#FFF051FF</color>
     <color name="color_0AFF5784">#0AFF5784</color>

+ 8 - 0
app/src/main/res/values/strings.xml

@@ -839,6 +839,14 @@
     <string name="profile_gender_certification">Gender Verification</string>
     <string name="family_role_owner">Family Owner</string>
     <string name="family_role_admin">Family Admin</string>
+    <string name="message_send_love">Send love</string>
+    <string name="message_today_fate_title">Today Fate</string>
+    <string name="message_reminder_title">Cupid Message</string>
+    <string name="message_reminder_desc">A beautiful girl has sent you a message, reply quickly!</string>
+    <string name="message_income_reminder_desc">A beautiful boy has sent you a message, reply quickly!</string>
+    <string name="message_reply">Reply Now</string>
+    <string name="message_income_reminder_title">Today earnings</string>
+    <string name="message_unread_conversation">Next Unread</string>
     <string name="common_can_not_send">Cannot send</string>
     <string name="common_reward_records">Reward records</string>
     <string name="call_permissoin_float_window">This feature requires enabling floating window and background startup permissions</string>

+ 8 - 1
module/account/src/main/java/com/adealink/weparty/account/register/RegisterProfileActivity.kt

@@ -22,6 +22,9 @@ import com.adealink.weparty.module.account.Account
 import com.adealink.weparty.module.account.AccountModule
 import com.adealink.weparty.module.follow.FollowModule
 import com.adealink.weparty.module.follow.viewmodel.IFollowViewModel
+import com.adealink.weparty.module.profile.ProfileModule
+import com.adealink.weparty.ui.home.util.HomeLocalService
+import com.adealink.weparty.ui.tab.HomeTab
 
 @RouterUri(path = [Account.Register.PATH], desc = "注册页面")
 class RegisterProfileActivity : BaseActivity() {
@@ -91,9 +94,13 @@ class RegisterProfileActivity : BaseActivity() {
     }
 
     private fun goHome() {
+        val extras = intent.extras ?: Bundle()
+        if (ProfileModule.getMyUserInfo()?.isFemale() == true && HomeLocalService.userHomeTabNumber == HomeTab.USER_LIST.number) {
+            extras.putString(AppModule.Main.EXTRA_MAIN_TAB, HomeTab.MESSAGE.tab)
+        }
         Router.build(this, AppModule.Main.PATH)
             .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
-            .putExtras(intent.extras ?: Bundle())
+            .putExtras(extras)
             .start()
     }
 

+ 5 - 0
module/medal/src/main/java/com/adealink/weparty/medal/dialog/ChatAchievementDialog.kt

@@ -13,6 +13,7 @@ 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.weparty.commonui.dialogchain.DialogShowManager
 import com.adealink.weparty.commonui.dialogfragment.BaseDialogFragment
 import com.adealink.weparty.commonui.ext.dp
 import com.adealink.weparty.medal.R
@@ -88,4 +89,8 @@ class ChatAchievementDialog : BaseDialogFragment(R.layout.dialog_chat_achievemen
         binding.tvTips.text = spannable
     }
 
+    override fun onDestroyView() {
+        super.onDestroyView()
+        DialogShowManager.resume()
+    }
 }

+ 23 - 1
module/message/src/main/java/com/adealink/weparty/message/MessageFragment.kt

@@ -32,6 +32,7 @@ import com.adealink.weparty.module.family.Family
 import com.adealink.weparty.module.family.data.FamilyInfo
 import com.adealink.weparty.module.family.data.FamilyTabOpenType
 import com.adealink.weparty.module.message.Message
+import com.adealink.weparty.module.message.MessageModule
 import com.adealink.weparty.module.message.data.EnterConversationFrom
 import com.adealink.weparty.module.message.data.OFFICIAL_TARGET_ID
 import com.adealink.weparty.module.message.dot.familyMessageDot
@@ -56,8 +57,10 @@ class MessageFragment : BaseFragment(R.layout.fragment_message), IScrollManager
     private val messageViewModel by activityViewModels<MessageViewModel>()
 
     private val conversationListTypeLists by fastLazy {
-        listOf(
+        val myUserInfo = ProfileModule.getMyUserInfo()
+        listOfNotNull(
             ConversationListType.All,
+            if (myUserInfo?.isFemale() == true && !myUserInfo.isMerchant() && !myUserInfo.isOfficial()) ConversationListType.Income else null,
             ConversationListType.Online,
             ConversationListType.UnRead,
             ConversationListType.FrequentChat,
@@ -181,6 +184,7 @@ class MessageFragment : BaseFragment(R.layout.fragment_message), IScrollManager
         override fun getTabName(pos: Int): String {
             return when (conversationListTypeLists[pos]) {
                 ConversationListType.All -> getCompatString(R.string.message_tab_all)
+                ConversationListType.Income -> getCompatString(R.string.message_tab_income)
                 ConversationListType.Online -> getCompatString(R.string.message_tab_online)
                 ConversationListType.UnRead -> getCompatString(R.string.message_tab_unread)
                 ConversationListType.FrequentChat -> getCompatString(R.string.message_tab_frequent_chat)
@@ -227,6 +231,24 @@ class MessageFragment : BaseFragment(R.layout.fragment_message), IScrollManager
         checkUserFamilySetting()
 
         checkMyFamilyInfo()
+
+        checkNoReplyMessageIfNeed()
+    }
+
+    private fun checkNoReplyMessageIfNeed() {
+        if (ProfileModule.getMyUserInfo()?.isFemale() == true) {
+            lifecycleScope.launch(Dispatcher.WENEXT_THREAD_POOL) {
+                MessageModule.getNoReplyMessage().onSuccess {
+                    val incomeTabIndex =
+                        conversationListTypeLists.indexOf(ConversationListType.Income)
+                    if (it?.existNoReplyPaidMessage == true && incomeTabIndex >= 0 && incomeTabIndex < pageAdapter.itemCount) {
+                        launch(Dispatcher.UI) {
+                            binding.viewPager.setCurrentItem(incomeTabIndex, false)
+                        }
+                    }
+                }
+            }
+        }
     }
 
     private fun checkUserFamilySetting() {

+ 22 - 0
module/message/src/main/java/com/adealink/weparty/message/MessageServiceImpl.kt

@@ -1,6 +1,7 @@
 package com.adealink.weparty.message
 
 import android.app.Application
+import android.text.Spannable
 import androidx.lifecycle.ViewModelProvider
 import androidx.lifecycle.ViewModelStoreOwner
 import com.adealink.frame.base.Rlt
@@ -8,7 +9,10 @@ import com.adealink.frame.imkit.manager.UnReadMessageManager
 import com.adealink.frame.spi.RegisterService
 import com.adealink.weparty.App
 import com.adealink.weparty.commonui.widget.floatview.data.IFloatData
+import com.adealink.weparty.commonui.widget.floatview.view.BaseFloatView
 import com.adealink.weparty.message.config.IMConfigCenter
+import com.adealink.weparty.message.floatview.NotificationIncomeMessageFloatData
+import com.adealink.weparty.message.floatview.NotificationIncomeMessageFloatView
 import com.adealink.weparty.message.floatview.NotificationMessageFloatData
 import com.adealink.weparty.message.floatview.NotificationMessageFloatView
 import com.adealink.weparty.message.manager.imConnectManager
@@ -17,10 +21,12 @@ import com.adealink.weparty.message.viewmodel.MessageViewModel
 import com.adealink.weparty.message.viewmodel.MessageViewModelFactory
 import com.adealink.weparty.module.message.IMessageService
 import com.adealink.weparty.module.message.data.CustomerInfo
+import com.adealink.weparty.module.message.data.NoReplyMessageRsp
 import com.adealink.weparty.module.message.viewmodel.IMessageViewModel
 import com.adealink.weparty.module.profile.data.UserFamilySetting
 import io.rong.imlib.IRongCoreCallback
 import io.rong.imlib.model.Conversation
+import io.rong.imlib.model.MessageContent
 
 @RegisterService(IMessageService::class)
 class MessageServiceImpl : IMessageService {
@@ -60,6 +66,10 @@ class MessageServiceImpl : IMessageService {
         return NotificationMessageFloatView(data as NotificationMessageFloatData)
     }
 
+    override fun getNotificationIncomeMessageFloatView(data: IFloatData): BaseFloatView<out IFloatData>? {
+        return NotificationIncomeMessageFloatView(data as NotificationIncomeMessageFloatData)
+    }
+
     override fun addUnReadMessageCountChangedObserver(
         observer: UnReadMessageManager.IUnReadMessageObserver,
         conversationTypes: Array<Conversation.ConversationType>
@@ -95,6 +105,18 @@ class MessageServiceImpl : IMessageService {
         return messageManager.getUserFamilySettingConfig(cache)
     }
 
+    override fun getMessageSummary(messageContent: MessageContent, targetName: String): Spannable {
+        return messageManager.getMessageSummary(messageContent, targetName)
+    }
+
+    override suspend fun batchSendQuickMessage(uidList: List<Long>): Rlt<Any> {
+        return messageManager.batchSendQuickMessage(uidList)
+    }
+
+    override suspend fun getNoReplyMessage(): Rlt<NoReplyMessageRsp?> {
+        return messageManager.getNoReplyMessage()
+    }
+
     override fun getService(): IMessageService {
         return this
     }

+ 10 - 0
module/message/src/main/java/com/adealink/weparty/message/callback/ISessionInfoChangeCallback.kt

@@ -0,0 +1,10 @@
+package com.adealink.weparty.message.callback
+
+import com.adealink.weparty.module.message.data.SessionInfo
+
+/**
+ * Created by LfJ on 2025/9/15.
+ */
+interface ISessionInfoChangeCallback {
+    fun onSessionInfoChanged(list: List<SessionInfo>)
+}

+ 7 - 0
module/message/src/main/java/com/adealink/weparty/message/conversation/ConversationActivity.kt

@@ -11,6 +11,7 @@ import com.adealink.frame.router.annotation.RouterUri
 import com.adealink.frame.statistics.CommonEventValue
 import com.adealink.weparty.commonui.BaseActivity
 import com.adealink.weparty.commonui.BaseFragment
+import com.adealink.weparty.commonui.dialogchain.DialogShowManager
 import com.adealink.weparty.message.MessageHomeFragment
 import com.adealink.weparty.message.R
 import com.adealink.weparty.message.databinding.ActivityConversationBinding
@@ -19,6 +20,7 @@ import com.adealink.weparty.message.util.IMRouteUtils
 import com.adealink.weparty.module.anchor.data.FromScene
 import com.adealink.weparty.module.message.Message
 import com.adealink.weparty.module.message.conversation.extension.InputMode
+import com.adealink.weparty.module.message.reminder.MessageIncomeReminderDialogTask
 import com.adealink.weparty.module.profile.ProfileModule
 import com.adealink.weparty.stat.constant.Page
 import com.adealink.weparty.stat.reportEnterPage
@@ -89,6 +91,11 @@ class ConversationActivity: BaseActivity() {
         fragment?.onNewIntent(intent)
     }
 
+    override fun onPause() {
+        super.onPause()
+        DialogShowManager.removeWaitToShowDialog { (it as? MessageIncomeReminderDialogTask)?.sender?.uid == toUid }
+    }
+
     override fun onStop() {
         super.onStop()
         if (isFinishing) {

+ 7 - 0
module/message/src/main/java/com/adealink/weparty/message/conversation/ConversationFragment.kt

@@ -49,6 +49,7 @@ import com.adealink.weparty.message.conversation.comp.CoupleMessageComp
 import com.adealink.weparty.message.conversation.comp.GreetingBoardComp
 import com.adealink.weparty.message.conversation.comp.IConversationCompCallback
 import com.adealink.weparty.message.conversation.comp.MerchantMessageComp
+import com.adealink.weparty.message.conversation.comp.UnreadConversationComp
 import com.adealink.weparty.message.conversation.extension.ExtensionViewModel
 import com.adealink.weparty.message.conversation.extension.longclick.MessageItemLongClickAction
 import com.adealink.weparty.message.conversation.extension.longclick.MessageItemLongClickBean
@@ -139,6 +140,7 @@ class ConversationFragment : BaseFragment(R.layout.fragment_conversation),
     private var onScrollStopRefreshList: Boolean = false
     private var topInfoComp: ConversationTargetInfoComp? = null
     private var coupleMessageComp: CoupleMessageComp? = null
+    private var unreadConversationComp: UnreadConversationComp? = null
     private var loadDataWhenConnect: Boolean = false
 
     private var pageObserver = Observer<PageEvent> { event ->
@@ -478,6 +480,11 @@ class ConversationFragment : BaseFragment(R.layout.fragment_conversation),
 
         compList.add(ChatBackgroundComp(this, binding.nivChatBackground, binding.pvChatBackground, this).attach())
         compList.add(MerchantMessageComp(this, this).attach())
+        this.unreadConversationComp =
+            UnreadConversationComp(this, this, binding.unreadConversation).apply {
+                attach()
+                compList.add(this)
+            }
     }
 
     override fun observeViewModel() {

+ 210 - 0
module/message/src/main/java/com/adealink/weparty/message/conversation/comp/UnreadConversationComp.kt

@@ -0,0 +1,210 @@
+package com.adealink.weparty.message.conversation.comp
+
+import android.content.Intent
+import android.util.Log
+import androidx.lifecycle.LifecycleOwner
+import com.adealink.frame.dot.NormalDot
+import com.adealink.frame.mvvm.view.ViewComponent
+import com.adealink.frame.mvvm.viewmodel.activityViewModels
+import com.adealink.frame.mvvm.viewmodel.viewModels
+import com.adealink.frame.router.Router
+import com.adealink.frame.util.AppUtil
+import com.adealink.weparty.commonui.ext.gone
+import com.adealink.weparty.commonui.ext.show
+import com.adealink.weparty.message.conversation.view.UnreadConversationView
+import com.adealink.weparty.message.conversation.view.UnreadConversationView.Companion.TAG
+import com.adealink.weparty.message.conversationlist.model.BaseUiConversation
+import com.adealink.weparty.message.conversationlist.model.SingleConversation
+import com.adealink.weparty.message.conversationlist.viewmodel.WeConversationListViewModel
+import com.adealink.weparty.message.data.ConversationListType
+import com.adealink.weparty.message.datasource.local.MessageLocalService
+import com.adealink.weparty.message.viewmodel.ConversationListViewModelFactory
+import com.adealink.weparty.message.viewmodel.MessageViewModel
+import com.adealink.weparty.module.message.Message
+import com.adealink.weparty.module.message.data.OFFICIAL_TARGET_ID
+import com.adealink.weparty.module.message.data.UnreadPaidSessionListReq
+import com.adealink.weparty.module.message.data.isSystemTarget
+import com.adealink.weparty.module.profile.ProfileModule
+import io.rong.push.RongPushClient.ConversationType
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+import java.util.concurrent.CopyOnWriteArrayList
+
+/**
+ * Created by LfJ on 2025/9/8.
+ */
+class UnreadConversationComp(
+    lifecycleOwner: LifecycleOwner,
+    private val callback: IConversationCompCallback,
+    private val unreadView: UnreadConversationView,
+) : ViewComponent(lifecycleOwner) {
+
+    companion object {
+        const val EXPAND_CLICK_MAX_COUNT = 3
+        const val SESSION_LIST_PAGE_SIZE = 20
+    }
+
+    private var mPaidConversations: CopyOnWriteArrayList<BaseUiConversation> =
+        CopyOnWriteArrayList()
+    private var mNonPaidConversations: CopyOnWriteArrayList<BaseUiConversation> =
+        CopyOnWriteArrayList()
+    private var mTotalConversations: CopyOnWriteArrayList<BaseUiConversation> =
+        CopyOnWriteArrayList()
+    private var mEnableLoadMore: Boolean = false
+
+    private val messageViewModel by activityViewModels<MessageViewModel>()
+    private val conversationListViewModel by viewModels<WeConversationListViewModel> {
+        ConversationListViewModelFactory(
+            ConversationListType.UnRead.type
+        )
+    }
+
+    override fun onCreate() {
+        super.onCreate()
+        initListener()
+        observeViewModels()
+        getPaidConversationList()
+        getNonPaidConversationList()
+        updateUI()
+    }
+
+    override fun onNewIntent(intent: Intent?) {
+        getNonPaidConversationList()
+    }
+
+    override fun onResume() {
+        super.onResume()
+        conversationListViewModel.onResume()
+    }
+
+    override fun onPause() {
+        super.onPause()
+        conversationListViewModel.onPause()
+    }
+
+    private fun updateTotalConversations() {
+        mTotalConversations.apply {
+            clear()
+            addAll(mPaidConversations)
+            addAll(mNonPaidConversations)
+        }
+        if (mTotalConversations.isEmpty()) {
+            unreadView.gone()
+        } else {
+            unreadView.show(callback.getTargetId() != OFFICIAL_TARGET_ID)
+            unreadView.getDotView().show(NormalDot(true))
+        }
+    }
+
+    private fun initListener() {
+        unreadView.setOnClickListener {
+            val targetId = if (mTotalConversations.isEmpty()) {
+                Log.i(TAG, "onClick mTotalConversations isNullOrEmpty")
+                return@setOnClickListener
+            } else {
+                mTotalConversations[0].conversation.targetId.toLongOrNull()
+            }
+            val activity = AppUtil.currentActivity
+            if (activity == null || targetId == null || targetId < 0) {
+                Log.i(TAG, "onClick targetId:$targetId")
+            } else {
+                Router.build(activity, Message.Conversation.PATH)
+                    .putExtra(Message.Common.EXTRA_TO_UID, targetId).putExtra(
+                        Message.Common.EXTRA_CONVERSATION_TYPE, ConversationType.PRIVATE.value
+                    ).start()
+                MessageLocalService.expandUnreadConversationCountToday += 1
+                updateUI()
+                MessageLocalService.lastExpandUnreadConversationDate = getTodayString()
+                if (mPaidConversations.isNotEmpty()) {
+                    mPaidConversations.removeFirstOrNull()
+                    updateTotalConversations()
+                }
+            }
+        }
+    }
+
+    private fun observeViewModels() {
+        // 会话列表数据监听
+        conversationListViewModel.conversationListLiveData.observe(viewLifecycleOwner) { nonPaidConversations ->
+            mNonPaidConversations.clear()
+            mNonPaidConversations.addAll(filterConversations(nonPaidConversations))
+            updateTotalConversations()
+            if (mNonPaidConversations.isEmpty() && mEnableLoadMore) {
+                loadMoreNonPaidConversations()
+            }
+        }
+        conversationListViewModel.loadMoreEnableEventLiveData.observe(viewLifecycleOwner) { refreshEvent ->
+            mEnableLoadMore = refreshEvent.enable
+        }
+        messageViewModel.unreadPaidSessionListRspLD.observe(viewLifecycleOwner) { rsp ->
+            val paidConversations = rsp.sessionInfos.map { bizSession ->
+                val fakeConversation = bizSession.toPrivateConversation()
+                SingleConversation(fakeConversation, listType = ConversationListType.UnRead.type)
+            }
+            val loadMoreEnable = rsp.sessionInfos.size >= SESSION_LIST_PAGE_SIZE
+            mPaidConversations.addAll(filterConversations(paidConversations))
+            updateTotalConversations()
+            if (loadMoreEnable) {
+                getPaidConversationList(true)
+            } else {
+                getNonPaidConversationList()
+            }
+        }
+    }
+
+    private fun filterConversations(data: List<BaseUiConversation>): List<BaseUiConversation> {
+        return ArrayList(data).filter {
+            it.conversation.targetId.toLongOrNull() != callback.getTargetId() && !isSystemTarget(
+                it.conversation.targetId
+            )
+        }
+    }
+
+    private fun getPaidConversationList(loadMore: Boolean = false) {
+        if (ProfileModule.getMyUserInfo()?.isFemale() != true) {
+            return
+        }
+        val unreadPaidSessionListRsp = messageViewModel.unreadPaidSessionListRspLD.value
+        val lastTimeStamp = unreadPaidSessionListRsp?.timeStamp ?: 0L
+        if (loadMore) {
+            messageViewModel.getUnreadPaidSessionList(
+                UnreadPaidSessionListReq(
+                    SESSION_LIST_PAGE_SIZE, lastTimeStamp
+                )
+            )
+        } else {
+            messageViewModel.getUnreadPaidSessionList(
+                UnreadPaidSessionListReq(
+                    SESSION_LIST_PAGE_SIZE, 0
+                )
+            )
+        }
+    }
+
+    private fun getNonPaidConversationList() {
+        conversationListViewModel.getConversationList(
+            loadMore = false, isEventManual = false, delayTime = 500, requireResumed = false
+        )
+    }
+
+    private fun loadMoreNonPaidConversations() {
+        conversationListViewModel.getConversationList(
+            loadMore = true, isEventManual = true, delayTime = 0, requireResumed = false
+        )
+    }
+
+    private fun updateUI() {
+        val todayData = getTodayString()
+        if (MessageLocalService.expandUnreadConversationCountToday >= EXPAND_CLICK_MAX_COUNT && MessageLocalService.lastExpandUnreadConversationDate == todayData) {
+            unreadView.getTvUnread().gone()
+        } else {
+            unreadView.getTvUnread().show()
+        }
+    }
+
+    private fun getTodayString(): String {
+        val sdf = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
+        return sdf.format(Date())
+    }
+}

+ 2 - 22
module/message/src/main/java/com/adealink/weparty/message/conversation/data/MessageExpansion.kt

@@ -17,7 +17,8 @@ enum class MessageExpansionKey(val key: String) {
     AcceptStatus("is_accept"),//是否接受 "1": "已接受"
     Scene("scene"),//消息发送场景
     SessionListChangeType("session_list_change_type"),//会话列表变更信息
-    BlockMessageShowInfo("block_message_show_info");//拦截消息显示信息
+    BlockMessageShowInfo("block_message_show_info"),//拦截消息显示信息
+    MessageNotifyInfo("message_notify_info");//消息通知信息
 
     companion object {
         fun map(key: String): MessageExpansionKey? {
@@ -28,27 +29,6 @@ enum class MessageExpansionKey(val key: String) {
     }
 }
 
-data class MessagePriceInfo(
-    @SerializedName("currencyType")
-    val currencyType: Int,
-    @SerializedName("currencyValue")
-    val currencyValue: Long,
-) {
-    fun getSpendIconRes(): Int {
-        return when (currencyType) {
-            Currency.Coin.value.toInt() -> R.drawable.im_price_diamond_icon
-            else -> 0
-        }
-    }
-
-    fun getIncomeIconRes(): Int {
-        return when (currencyType) {
-            Currency.Diamond.value.toInt() -> R.drawable.im_price_diamond_icon
-            else -> 0
-        }
-    }
-}
-
 /**
  * 通知消息额外配置,可以定制内容
  */

+ 1 - 1
module/message/src/main/java/com/adealink/weparty/message/conversation/provider/BaseMessageItemProvider.kt

@@ -26,7 +26,6 @@ import com.adealink.weparty.commonui.ext.show
 import com.adealink.weparty.message.R
 import com.adealink.weparty.message.constant.TAG_IM_MSG_PROVIDER
 import com.adealink.weparty.message.conversation.data.MessageExpansionKey
-import com.adealink.weparty.message.conversation.data.MessagePriceInfo
 import com.adealink.weparty.message.conversation.data.MessageShowInfo
 import com.adealink.weparty.module.level.label.FamilyNewcomerView
 import com.adealink.weparty.module.level.label.FamilyRoleTagView
@@ -36,6 +35,7 @@ import com.adealink.weparty.module.backpack.CHAT_EXPERIENCE_CARD
 import com.adealink.weparty.module.level.label.UserSVipLevelView
 import com.adealink.weparty.module.level.label.UserWealthLevelView
 import com.adealink.weparty.module.level.label.VipRechargeLabelView
+import com.adealink.weparty.module.message.data.MessagePriceInfo
 import com.adealink.weparty.module.message.data.isOfficialTarget
 import com.adealink.weparty.module.profile.util.updateCharmLevelLabel
 import io.rong.imlib.model.Message

+ 26 - 0
module/message/src/main/java/com/adealink/weparty/message/conversation/view/UnreadConversationView.kt

@@ -0,0 +1,26 @@
+package com.adealink.weparty.message.conversation.view
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import androidx.constraintlayout.widget.ConstraintLayout
+import com.adealink.weparty.databinding.LayoutUnreadConversationBinding
+
+/**
+ * Created by LfJ on 2025/9/1.
+ */
+class UnreadConversationView @JvmOverloads constructor(
+    context: Context, attrs: AttributeSet? = null
+) : ConstraintLayout(context, attrs) {
+
+    companion object {
+        const val TAG = "UnreadConversationView"
+    }
+
+    private val binding =
+        LayoutUnreadConversationBinding.inflate(LayoutInflater.from(context), this, true)
+
+    fun getTvUnread() = binding.tvUnread
+
+    fun getDotView() = binding.dotView
+}

+ 3 - 0
module/message/src/main/java/com/adealink/weparty/message/conversationlist/model/BaseUiConversation.kt

@@ -18,6 +18,7 @@ abstract class BaseUiConversation(var conversation: Conversation, var listType:
     var targetUserInfo: IMUserInfo? = null
     var onlineInRoomStatus: OnlineInRoomStatus? = null
     var coupleUserInfo: CoupleUserInfo? = null
+    var existsNoReplyPaidMessage: Boolean = false
     var isCustomer: Boolean = false
 
     val conversationIdentifier: ConversationIdentifier?
@@ -59,6 +60,8 @@ abstract class BaseUiConversation(var conversation: Conversation, var listType:
 
     abstract fun onCoupleUserInfoUpdate(info: CoupleUserInfo?)
 
+    abstract fun onNoReplyPaidMessageUpdate(existsNoReplyPaidMessage: Boolean)
+
     abstract fun onCustomerUpdate(isCustomer: Boolean)
 
     /**

+ 8 - 0
module/message/src/main/java/com/adealink/weparty/message/conversationlist/model/SingleConversation.kt

@@ -72,6 +72,14 @@ class SingleConversation(conversation: Conversation, listType: Int) :
         change()
     }
 
+    override fun onNoReplyPaidMessageUpdate(existsNoReplyPaidMessage: Boolean) {
+        if (existsNoReplyPaidMessage == this.existsNoReplyPaidMessage) {
+            return
+        }
+        this.existsNoReplyPaidMessage = existsNoReplyPaidMessage
+        change()
+    }
+
     override fun onCustomerUpdate(isCustomer: Boolean) {
         if (isCustomer == this.isCustomer) {
             return

+ 5 - 0
module/message/src/main/java/com/adealink/weparty/message/conversationlist/provider/BaseConversationProvider.kt

@@ -17,6 +17,7 @@ import com.adealink.frame.imkit.widget.adapter.IViewProviderListener
 import com.adealink.frame.imkit.widget.adapter.ViewHolder
 import com.adealink.frame.util.DisplayUtil
 import com.adealink.weparty.commonui.ext.dp
+import com.adealink.weparty.commonui.ext.show
 import com.adealink.weparty.commonui.menu.SwipeMenuLayout
 import com.adealink.weparty.message.R
 import com.adealink.weparty.message.conversation.provider.ConversationClickType
@@ -78,6 +79,10 @@ open class BaseConversationProvider : IViewProvider<BaseUiConversation> {
         val intimacyLabelView = holder.getView<IntimacyLevelLabelView>(R.id.im_conversation_intimacy_level_label)
         intimacyLabelView?.updateLabel(uiConversation.coupleUserInfo)
 
+        //是否存在未回复的付费消息
+        val incomeDiamond = holder.getView<ViewGroup>(R.id.cl_im_conversation_income_diamond)
+        incomeDiamond?.show(uiConversation.existsNoReplyPaidMessage)
+
         //币商,客服和官方号标签
         when {
             uiConversation.targetUserInfo?.userInfo?.isMerchant() == true -> {

+ 68 - 54
module/message/src/main/java/com/adealink/weparty/message/conversationlist/viewmodel/ConversationListViewModel.kt

@@ -31,6 +31,7 @@ import com.adealink.weparty.commonui.ext.onSuccess
 import com.adealink.weparty.commonui.toast.util.showFailedToast
 import com.adealink.weparty.commonui.toast.util.showToast
 import com.adealink.weparty.message.R
+import com.adealink.weparty.message.callback.ISessionInfoChangeCallback
 import com.adealink.weparty.message.config.IMConfigCenter
 import com.adealink.weparty.message.constant.TAG_IM_CONV_LIST_VM
 import com.adealink.weparty.message.conversation.data.MessageExpansionKey
@@ -51,6 +52,7 @@ import com.adealink.weparty.module.family.FamilyModule
 import com.adealink.weparty.module.family.data.FamilyMemberApplyRecordReq
 import com.adealink.weparty.module.message.data.GetSessionListReq
 import com.adealink.weparty.module.message.data.GetSessionListRes
+import com.adealink.weparty.module.message.data.SessionInfo
 import com.adealink.weparty.module.message.data.UpdateSessionListOperate
 import com.adealink.weparty.module.message.data.UpdateSessionListReq
 import com.adealink.weparty.module.message.dot.familyApplyMsgDot
@@ -96,7 +98,7 @@ open class ConversationListViewModel(
     private var isTaskScheduled = false
     private var time = 500
     private var delayRefreshTime = 5000
-    private var sizePerPage: Int
+    private var sizePerPage = IMConfigCenter.conversationListConfig.conversationCountPerPage
     private var pageLimit: Int
 
     /** 是否优先显示置顶会话(查询结果的排序方式,是否置顶优先,true 表示置顶会话优先返回,false 结果只以会话时间排序)  */
@@ -267,11 +269,12 @@ open class ConversationListViewModel(
                 } else {
                     delayRefreshTime
                 }
+                val hasTargetConversation =
+                    uiConversationList.find { it.conversation.targetId == message.targetId } != null
                 getConversationList(
                     loadMore = false,
                     isEventManual = false,
-                    delayTime = time.toLong(),
-                    onlyLoadLocal = true
+                    delayTime = time.toLong(), onlyLoadLocal = hasTargetConversation
                 )
                 return false
             }
@@ -378,12 +381,17 @@ open class ConversationListViewModel(
             }
         }
 
+    private val sessionInfoChangeCallback = object : ISessionInfoChangeCallback {
+        override fun onSessionInfoChanged(list: List<SessionInfo>) {
+
+        }
+    }
+
     private val messageHttpService by lazy {
         App.instance.networkService.getHttpService(MessageHttpService::class.java)
     }
 
     init {
-        sizePerPage = IMConfigCenter.conversationListConfig.conversationCountPerPage
         pageLimit = 10
         dataFilter = IMConfigCenter.conversationListConfig.getDataProcessor()
         delayRefreshTime = IMConfigCenter.conversationListConfig.delayRefreshTime
@@ -408,10 +416,12 @@ open class ConversationListViewModel(
 
     fun onResume() {
         onResumed = true
+        messageManager.addSessionInfoChangeCallback(listType, sessionInfoChangeCallback)
     }
 
     fun onPause() {
         onResumed = false
+        messageManager.removeSessionInfoChangeCallback(listType)
     }
 
     /**
@@ -521,58 +531,57 @@ open class ConversationListViewModel(
                     val bizSessionResRlt = getBizSessionList(0, sizePerPage)
                     bizSessionResRlt.onSuccess { res ->
                         val bizSessionList = res.data?.sessionInfos
-                        if (bizSessionList.isNullOrEmpty()) {
-                            callback.onSuccess(
-                                emptyList()
-                            )
-                            return@onSuccess
-                        }
-                        val conversationIdentifierList = bizSessionList.map {
-                            ConversationIdentifier.obtain(
-                                ConversationType.PRIVATE,
-                                it.uid.toString(),
-                                ""
-                            )
-                        }
-                        IMService.innerService.getConversations(
-                            conversationIdentifierList,
-                            object : IRongCoreCallback.ResultCallback<List<Conversation>?>() {
-                                override fun onSuccess(t: List<Conversation>?) {
-                                    val localList = t ?: return
-                                    val mergedList = mutableListOf<Conversation>()
-                                    val localMap =
-                                        localList.associateBy { it.targetId + "_" + it.conversationType.value }
-                                    // 合并逻辑
-                                    bizSessionList.forEach { bizSession ->
-                                        val key =
-                                            bizSession.uid.toString() + "_" + ConversationType.PRIVATE.value
-                                        if (!localMap.containsKey(key)) {
-                                            // 构造一个假的 RongCloud Conversation 对象
-                                            val fakeConversation =
-                                                bizSession.toPrivateConversation()
-                                            mergedList.add(fakeConversation)
-                                        }
-                                    }
-                                    // 合并后排序
-                                    val finalList = (localList + mergedList)
-                                        .sortedWith(
-                                            compareByDescending<Conversation> { it.isTop }
-                                                .thenByDescending { it.sentTime }
-                                        )
-                                    callback.onSuccess(finalList)
-                                }
-
-                                override fun onError(e: IRongCoreEnum.CoreErrorCode) {
-                                    callback.onError(e)
-                                }
-
-                            })
+                        bizSessionToConversation(bizSessionList, callback)
                     }
                 }
             }
         }
     }
 
+    private fun bizSessionToConversation(
+        bizSessionList: List<SessionInfo>?, callback: ConversationListResultCallback
+    ) {
+        if (bizSessionList.isNullOrEmpty()) {
+            callback.onSuccess(
+                emptyList()
+            )
+            return
+        }
+        val conversationIdentifierList = bizSessionList.map {
+            ConversationIdentifier.obtain(
+                ConversationType.PRIVATE, it.uid.toString(), ""
+            )
+        }
+        IMService.innerService.getConversations(
+            conversationIdentifierList,
+            object : IRongCoreCallback.ResultCallback<List<Conversation>?>() {
+                override fun onSuccess(t: List<Conversation>?) {
+                    val localList = t ?: return
+                    val mergedList = mutableListOf<Conversation>()
+                    val localMap =
+                        localList.associateBy { it.targetId + "_" + it.conversationType.value }
+                    // 合并逻辑
+                    bizSessionList.forEach { bizSession ->
+                        val key = bizSession.uid.toString() + "_" + ConversationType.PRIVATE.value
+                        if (!localMap.containsKey(key)) {
+                            // 构造一个假的 RongCloud Conversation 对象
+                            val fakeConversation = bizSession.toPrivateConversation()
+                            mergedList.add(fakeConversation)
+                        }
+                    }
+                    // 合并后排序
+                    val finalList =
+                        (localList + mergedList).sortedWith(compareByDescending<Conversation> { it.isTop }.thenByDescending { it.sentTime })
+                    callback.onSuccess(finalList)
+                }
+
+                override fun onError(e: IRongCoreEnum.CoreErrorCode) {
+                    callback.onError(e)
+                }
+
+            })
+    }
+
     /**
      * 因为会通过dataFilter.filtered进行过滤的缘故,最新返回数量可能少于一页
      * 如果少得比较多(会话少于一屏)就会让用户造成会话消失错觉,所以做递归拉取,直到拉满或者没有了
@@ -688,20 +697,24 @@ open class ConversationListViewModel(
                 }
             }
             if (targetIds.isNotEmpty()) {
-                batchGetTargetExtraInfo(targetIds)
+                batchGetTargetExtraInfo(targetIds, !isEventManual)
             } else {
                 refreshConversationList()
             }
         }
     }
 
-    private fun batchGetTargetExtraInfo(targetIds: Set<Long>) {
+    private fun batchGetTargetExtraInfo(targetIds: Set<Long>, cache: Boolean) {
         launch {
             val uid2SessionInfoDef = async {
-                messageManager.batchGetSessionInfo(targetIds, cache = true, fromOnlineList = listType == ConversationListType.Online.type)
+                messageManager.batchGetSessionInfo(
+                    targetIds,
+                    cache = cache,
+                    fromOnlineList = listType == ConversationListType.Online.type
+                )
             }
             val customerListDef = async {
-                messageManager.getCustomerList(cache = true)
+                messageManager.getCustomerList(cache = cache)
             }
             val uid2SessionInfo = uid2SessionInfoDef.await()
             val customerIdSet = customerListDef.await()?.map { it.customerUid }?.toSet() ?: setOf()
@@ -717,6 +730,7 @@ open class ConversationListViewModel(
                 baseUiConversation.onUserInfoUpdate(IMUserInfo(userInfo))
                 baseUiConversation.onOnlineInRoomStatusUpdate(onlineInRoomStatus)
                 baseUiConversation.onCoupleUserInfoUpdate(sessionInfo.userIntimacyInfo)
+                baseUiConversation.onNoReplyPaidMessageUpdate(sessionInfo.existsNoReplyPaidMessage)
                 baseUiConversation.onCustomerUpdate(customerIdSet.contains(targetIdLong))
             }
             refreshConversationList()

+ 2 - 1
module/message/src/main/java/com/adealink/weparty/message/data/MessageData.kt

@@ -22,7 +22,8 @@ enum class ConversationListType(val type: Int) {
     UnRead(type = 2),
     FrequentChat(type = 3),
     Follow(type = 4),
-    Merchant(type = 5);
+    Merchant(type = 5),
+    Income(type = 6);
 
     companion object {
         fun map(type: Int): ConversationListType {

+ 8 - 0
module/message/src/main/java/com/adealink/weparty/message/datasource/local/MessageLocalService.kt

@@ -46,4 +46,12 @@ object MessageLocalService : TypeDelegationPrefs(
     var hasShowGreetSettingGuide: Boolean by PrefUserKey("key_has_show_greet_setting_guide", false)
 
     var sendQuickMsgCount: Int by PrefUserKey("key_send_quick_msg_count", 0)
+
+    var expandUnreadConversationCountToday: Int by PrefUserKey(
+        "key_expand_unread_conversation_count_today", 0
+    )
+
+    var lastExpandUnreadConversationDate: String by PrefUserKey(
+        "key_last_expand_unread_conversation_data", ""
+    )
 }

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

@@ -9,6 +9,7 @@ import com.adealink.weparty.module.message.data.BatchGetCustomerListRes
 import com.adealink.weparty.module.message.data.BatchGetFirstChatTsReq
 import com.adealink.weparty.module.message.data.BatchGetFirstChatTsRes
 import com.adealink.weparty.module.message.data.BatchGetSessionListReq
+import com.adealink.weparty.module.message.data.BatchSendQuickMessageReq
 import com.adealink.weparty.module.message.data.ChatPageDetailRes
 import com.adealink.weparty.module.message.data.CustomerInfo
 import com.adealink.weparty.module.message.data.FamilyImInfoRsp
@@ -16,9 +17,12 @@ import com.adealink.weparty.module.message.data.GetCustomQuickMessageRes
 import com.adealink.weparty.module.message.data.GetSessionListReq
 import com.adealink.weparty.module.message.data.GetSessionListRes
 import com.adealink.weparty.module.message.data.MerchantAgentInfo
+import com.adealink.weparty.module.message.data.NoReplyMessageRsp
 import com.adealink.weparty.module.message.data.RecallReq
 import com.adealink.weparty.module.message.data.SendQuickMessageReq
 import com.adealink.weparty.module.message.data.SendRCMessageReq
+import com.adealink.weparty.module.message.data.UnreadPaidSessionListReq
+import com.adealink.weparty.module.message.data.UnreadPaidSessionListRsp
 import com.adealink.weparty.module.message.data.UpdateQuickMessageReq
 import com.adealink.weparty.module.message.data.UpdateSessionListReq
 import retrofit2.http.Body
@@ -56,6 +60,14 @@ interface MessageHttpService {
         @Body req: SendQuickMessageReq,
     ): Rlt<Res<Any>>
 
+    /**
+     * 批量发送快捷消息
+     */
+    @POST("im/batch_send_quick_message")
+    suspend fun batchSendQuickMessage(
+        @Body req: BatchSendQuickMessageReq,
+    ): Rlt<Res<Any>>
+
     /**
      * 更新快捷消息
      */
@@ -153,6 +165,18 @@ interface MessageHttpService {
     @GET("family/im/info")
     suspend fun getFamilyImInfo(): Rlt<Res<FamilyImInfoRsp>>
 
+    /**
+     * 未回复的消息相关信息
+     */
+    @GET("im/get_no_reply_message")
+    suspend fun getNoReplyMessage(): Rlt<Res<NoReplyMessageRsp>>
+
+    /**
+     * 未读的付费消息
+     */
+    @POST("im/unread_paid_session_list")
+    suspend fun unreadPaidSessionList(@Body req: UnreadPaidSessionListReq): Rlt<Res<UnreadPaidSessionListRsp>>
+
     /**
      * 获取奖励记录列表
      */

+ 20 - 0
module/message/src/main/java/com/adealink/weparty/message/floatview/NotificationIncomeMessageFloatData.kt

@@ -0,0 +1,20 @@
+package com.adealink.weparty.message.floatview
+
+import com.adealink.weparty.commonui.widget.floatview.data.FloatWindowType
+import com.adealink.weparty.commonui.widget.floatview.data.GRAVITY_TOP
+import com.adealink.weparty.commonui.widget.floatview.data.ILayoutFloatData
+import com.adealink.weparty.push.data.NotificationMessage
+
+class NotificationIncomeMessageFloatData(val windowMode: Int, val data: NotificationMessage) :
+    ILayoutFloatData {
+
+    override fun windowType(): FloatWindowType = FloatWindowType.NOTIFICATION_INCOME_MESSAGE
+
+    override fun windowMode(): Int = windowMode
+
+    override fun gravity(): Int = GRAVITY_TOP
+
+    override fun showOnlyOne(): Boolean {
+        return true
+    }
+}

+ 91 - 0
module/message/src/main/java/com/adealink/weparty/message/floatview/NotificationIncomeMessageFloatView.kt

@@ -0,0 +1,91 @@
+package com.adealink.weparty.message.floatview
+
+import android.app.Activity
+import android.view.LayoutInflater
+import android.view.View
+import com.adealink.frame.aab.util.getCompatDimensionPixelSize
+import com.adealink.frame.aab.util.getCompatString
+import com.adealink.frame.util.AppUtil
+import com.adealink.frame.util.DisplayUtil
+import com.adealink.weparty.commonui.widget.floatview.view.BaseSlideFloatView
+import com.adealink.weparty.commonui.widget.slide.Slide
+import com.adealink.weparty.message.R
+import com.adealink.weparty.message.databinding.LayoutNotificationIncomeMessageViewBinding
+import com.adealink.weparty.message.manager.IMessageManager
+import com.adealink.weparty.message.util.MessageNoticeUtils
+import com.adealink.weparty.ui.home.util.HomeUIUtil
+import com.adealink.weparty.util.goLocalLinkPage
+
+class NotificationIncomeMessageFloatView(val notificationIncomeMessageFloatData: NotificationIncomeMessageFloatData) :
+    BaseSlideFloatView(notificationIncomeMessageFloatData) {
+
+    private lateinit var binding: LayoutNotificationIncomeMessageViewBinding
+
+    override var autoDismissDelayTs: Long = 10_000 //10秒后自动消失
+
+    private val show: Boolean
+        get() {
+            return (windowManager?.currentActivity?.get()
+                ?.let { !IMessageManager.NEW_MSG_DISMISS_ACTIVITIES.contains(it.javaClass.name) }
+                ?: false) && !HomeUIUtil.isInMessageTab()
+        }
+
+    override val layoutParams: LayoutParams
+        get() = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
+            marginStart = getCompatDimensionPixelSize(com.adealink.weparty.R.dimen.top_float_view_margin_horizontal)
+            marginEnd = getCompatDimensionPixelSize(com.adealink.weparty.R.dimen.top_float_view_margin_horizontal)
+        }
+
+    override fun slideDirection(): Slide.SlideDirection {
+        return Slide.SlideDirection.HORIZONTAL
+    }
+
+    override var autoDismiss: Boolean = true
+
+    override fun onCreate() {
+        super.onCreate()
+        binding = LayoutNotificationIncomeMessageViewBinding.inflate(LayoutInflater.from(context))
+        setContentView(binding.root)
+        visibility = if (show) View.VISIBLE else View.GONE
+        updateUI()
+    }
+
+    private fun updateUI() {
+        notificationIncomeMessageFloatData.data.let {
+            binding.avatarIv.setImageUrl(it.largeIconUrl)
+            binding.userCountryView.setUserInfo(it.targetUserInfo)
+            binding.nameTv.text = it.title
+            binding.msgIncomeTip.text = it.messageSummary
+            binding.timeTv.text = getCompatString(R.string.message_now)
+            binding.toViewBtn.text = getCompatString(R.string.message_reply_btn)
+            binding.ivSign.scaleX = if (DisplayUtil.isRtlLayout()) {
+                -1f
+            } else {
+                1f
+            }
+        }
+        setOnClickListener {
+            goLocalLinkPage(
+                AppUtil.currentActivity, notificationIncomeMessageFloatData.data.deeplink
+            )
+            removeSelf("")
+        }
+    }
+
+    override fun onActivityChange(activity: Activity) {
+        super.onActivityChange(activity)
+        if (!show) {
+            removeSelf("")
+        }
+    }
+
+    override fun onAttachedToWindow() {
+        super.onAttachedToWindow()
+        MessageNoticeUtils.triggerSoundAndVibration()
+    }
+
+    override fun onDetachedFromWindow() {
+        super.onDetachedFromWindow()
+        MessageNoticeUtils.stopVibrate()
+    }
+}

+ 1 - 1
module/message/src/main/java/com/adealink/weparty/message/floatview/NotificationMessageFloatView.kt

@@ -19,7 +19,7 @@ import com.adealink.weparty.module.profile.view.UserNameTextView
 import com.adealink.weparty.ui.home.util.HomeUIUtil
 import com.adealink.weparty.util.goLocalLinkPage
 
-class NotificationMessageFloatView(private val notificationMessageFloatData: NotificationMessageFloatData) :
+class NotificationMessageFloatView(val notificationMessageFloatData: NotificationMessageFloatData) :
     BaseSlideFloatView(notificationMessageFloatData) {
 
     private lateinit var binding: LayoutNotificationMessageViewBinding

+ 14 - 0
module/message/src/main/java/com/adealink/weparty/message/manager/IMessageManager.kt

@@ -1,17 +1,21 @@
 package com.adealink.weparty.message.manager
 
+import android.text.Spannable
 import com.adealink.frame.base.Rlt
 import com.adealink.frame.frame.IBaseFrame
 import com.adealink.frame.network.data.Res
 import com.adealink.frame.router.Router
 import com.adealink.frame.startup.IAppStartUpTask
+import com.adealink.weparty.message.callback.ISessionInfoChangeCallback
 import com.adealink.weparty.message.listener.IMessageListener
 import com.adealink.weparty.module.account.Account
 import com.adealink.weparty.module.message.data.CustomerInfo
 import com.adealink.weparty.module.message.data.FamilyImInfoRsp
+import com.adealink.weparty.module.message.data.NoReplyMessageRsp
 import com.adealink.weparty.module.message.data.SessionInfo
 import com.adealink.weparty.module.profile.data.UserFamilySetting
 import com.adealink.weparty.module.room.Room
+import io.rong.imlib.model.MessageContent
 
 interface IMessageManager : IBaseFrame<IMessageListener>, IAppStartUpTask {
     suspend fun batchQuerySayHiState(uidSet: Set<Long>): Rlt<Map<Long, Boolean>>
@@ -42,6 +46,16 @@ interface IMessageManager : IBaseFrame<IMessageListener>, IAppStartUpTask {
 
     suspend fun batchGetSessionInfo(targetIdSet: Set<Long>, cache: Boolean = true, fromOnlineList: Boolean = false): Map<Long, SessionInfo>
 
+    suspend fun batchSendQuickMessage(uidList: List<Long>): Rlt<Any>
+
+    suspend fun getNoReplyMessage(): Rlt<NoReplyMessageRsp?>
+
+    fun getMessageSummary(messageContent: MessageContent, targetName: String): Spannable
+
+    fun addSessionInfoChangeCallback(listType: Int, callback: ISessionInfoChangeCallback)
+
+    fun removeSessionInfoChangeCallback(listType: Int)
+
     companion object {
         val NEW_MSG_DISMISS_ACTIVITIES = hashSetOf<String>().apply {
             Router.getClazz(Account.Login.PATH)?.let { this.add(it.name) }

+ 320 - 77
module/message/src/main/java/com/adealink/weparty/message/manager/MessageManager.kt

@@ -2,9 +2,11 @@ package com.adealink.weparty.message.manager
 
 import android.app.Application
 import android.graphics.Bitmap
+import android.os.Bundle
 import android.text.Spannable
 import android.text.SpannableString
 import android.text.SpannableStringBuilder
+import androidx.fragment.app.FragmentActivity
 import com.adealink.frame.aab.util.getCompatDrawable
 import com.adealink.frame.aab.util.getCompatString
 import com.adealink.frame.base.Rlt
@@ -21,20 +23,26 @@ import com.adealink.frame.imkit.model.TAG_IM_UI
 import com.adealink.frame.log.Log
 import com.adealink.frame.network.ISocketNotify
 import com.adealink.frame.network.data.Res
+import com.adealink.frame.router.Router
 import com.adealink.frame.storage.cache.TimeoutLruCache
 import com.adealink.frame.util.AppUtil
 import com.adealink.frame.util.ONE_MINUTE
 import com.adealink.weparty.App
+import com.adealink.weparty.commonui.dialogchain.DialogShowManager
+import com.adealink.weparty.commonui.dialogchain.DialogTaskManager
+import com.adealink.weparty.commonui.dialogfragment.BaseDialogFragment
 import com.adealink.weparty.commonui.ext.dp
 import com.adealink.weparty.commonui.ext.onSuccess
 import com.adealink.weparty.commonui.text.span.DraweeSpan
 import com.adealink.weparty.commonui.widget.CenterImageSpan
 import com.adealink.weparty.commonui.widget.floatview.FloatViewFactory
 import com.adealink.weparty.commonui.widget.floatview.WindowManagerProxy
+import com.adealink.weparty.commonui.widget.floatview.data.FloatWindowType
 import com.adealink.weparty.commonui.widget.floatview.data.IFloatData
 import com.adealink.weparty.commonui.widget.floatview.data.MODE_APPLICATION
 import com.adealink.weparty.commonui.widget.floatview.view.BaseFloatView
 import com.adealink.weparty.message.R
+import com.adealink.weparty.message.callback.ISessionInfoChangeCallback
 import com.adealink.weparty.message.config.IMConfigCenter
 import com.adealink.weparty.message.constant.IM_NOTIFICATION_CHANNEL_ID
 import com.adealink.weparty.message.constant.OFFICIAL_NAME
@@ -53,7 +61,10 @@ import com.adealink.weparty.message.conversation.message.ProxyInviteWithdrawMess
 import com.adealink.weparty.message.conversation.message.TransferDealMessage
 import com.adealink.weparty.message.conversation.util.getCPTypeName
 import com.adealink.weparty.message.datasource.remote.MessageHttpService
+import com.adealink.weparty.message.floatview.NotificationIncomeMessageFloatData
+import com.adealink.weparty.message.floatview.NotificationIncomeMessageFloatView
 import com.adealink.weparty.message.floatview.NotificationMessageFloatData
+import com.adealink.weparty.message.floatview.NotificationMessageFloatView
 import com.adealink.weparty.message.listener.IMessageListener
 import com.adealink.weparty.message.manager.IMessageManager.Companion.NEW_MSG_DISMISS_ACTIVITIES
 import com.adealink.weparty.message.userinfo.IMUserInfoManager
@@ -67,22 +78,31 @@ import com.adealink.weparty.module.couple.data.IntimacyValInfo
 import com.adealink.weparty.module.couple.listener.ICoupleListener
 import com.adealink.weparty.module.message.data.BatchGetFirstChatTsReq
 import com.adealink.weparty.module.message.data.BatchGetSessionListReq
+import com.adealink.weparty.module.message.data.BatchSendQuickMessageReq
 import com.adealink.weparty.module.message.data.CustomerInfo
 import com.adealink.weparty.module.message.data.EnterConversationFrom
 import com.adealink.weparty.module.message.data.FamilyImInfoRsp
 import com.adealink.weparty.module.message.data.IMMessageScene
 import com.adealink.weparty.module.message.data.Im1v1ContinuousSendMessageDeductNotifyInfo
+import com.adealink.weparty.module.message.data.ImMessageNotifyInfo
+import com.adealink.weparty.module.message.data.MessagePriceInfo
+import com.adealink.weparty.module.message.data.NoReplyMessageRsp
 import com.adealink.weparty.module.message.data.OFFICIAL_TARGET_ID
 import com.adealink.weparty.module.message.data.SendQuickMessageReq
 import com.adealink.weparty.module.message.data.SessionInfo
 import com.adealink.weparty.module.message.data.isSystemTarget
+import com.adealink.weparty.module.message.reminder.MessageIncomeReminderDialogTask
+import com.adealink.weparty.module.message.reminder.MessageReminderDialogTask
 import com.adealink.weparty.module.profile.ProfileModule
 import com.adealink.weparty.module.profile.data.UserConfigType
 import com.adealink.weparty.module.profile.data.UserFamilySetting
 import com.adealink.weparty.module.profile.data.UserInfo
 import com.adealink.weparty.push.NotificationUtil
+import com.adealink.weparty.push.data.FirstGetDiamondsFromChatNotify
+import com.adealink.weparty.push.data.NoReplyPaidMessageNotify
 import com.adealink.weparty.push.data.NotificationMessage
 import com.adealink.weparty.push.data.PushMessageType
+import com.adealink.weparty.push.data.SessionInfoChangeNotify
 import com.adealink.weparty.push.data.WeNextPushMessage
 import com.adealink.weparty.ui.home.util.HomeUIUtil
 import com.facebook.common.util.UriUtil
@@ -97,6 +117,7 @@ import kotlinx.coroutines.launch
 import kotlinx.coroutines.suspendCancellableCoroutine
 import kotlinx.coroutines.withContext
 import java.util.LinkedList
+import java.util.concurrent.ConcurrentHashMap
 import kotlin.coroutines.resume
 import com.adealink.weparty.R as APP_R
 import com.adealink.weparty.module.message.Message as MESSAGE_ROUTER
@@ -105,6 +126,9 @@ val messageManager: IMessageManager by lazy { MessageManager() }
 
 class MessageManager : BaseFrame<IMessageListener>(), IMessageManager,
     UserDataProvider.UserInfoProvider, ICoupleListener {
+    private val sessionInfoChangeCallbackMap by lazy {
+        ConcurrentHashMap<Int, ISessionInfoChangeCallback>()
+    }
     private val messageHttpService by lazy {
         App.instance.networkService.getHttpService(MessageHttpService::class.java)
     }
@@ -127,6 +151,79 @@ class MessageManager : BaseFrame<IMessageListener>(), IMessageManager,
             }
         }
 
+    private val noReplyPaidMessageNotify = object : ISocketNotify<NoReplyPaidMessageNotify> {
+
+        override val uri: String = "NO_REPLY_PAID_MESSAGE_NOTIFY"
+
+        override fun needHandle(data: NoReplyPaidMessageNotify?): Boolean {
+            return data != null
+        }
+
+        override fun onNotify(data: NoReplyPaidMessageNotify) {
+            val messageSender = data.messageSender
+            if (messageSender != null) {
+                DialogTaskManager.submit(
+                    MessageIncomeReminderDialogTask(
+                        data.content, messageSender
+                    )
+                )
+            } else {
+                Log.e(TAG_IM_MSG_RECEIVE, "noReplyPaidMessageNotify messageSender null")
+            }
+        }
+    }
+
+    private val firstGetDiamondsFromChatNotify =
+        object : ISocketNotify<FirstGetDiamondsFromChatNotify> {
+
+            override val uri: String = "FIRST_GET_DIAMONDS_FROM_CHAT_NOTIFY"
+
+            override fun needHandle(data: FirstGetDiamondsFromChatNotify?): Boolean {
+                return data != null
+            }
+
+            override fun onNotify(data: FirstGetDiamondsFromChatNotify) {
+                Router.getRouterInstance<BaseDialogFragment>(com.adealink.weparty.module.message.Message.MessageReplyReward.PATH)
+                    ?.apply {
+                        arguments = Bundle().apply {
+                            putString(
+                                com.adealink.weparty.module.message.Message.MessageReplyReward.EXTRA_TITLE,
+                                data.title
+                            )
+                            putString(
+                                com.adealink.weparty.module.message.Message.MessageReplyReward.EXTRA_CONTENT,
+                                data.content
+                            )
+                        }
+                        val curActivity = AppUtil.currentActivity as? FragmentActivity ?: return
+                        show(curActivity.supportFragmentManager)
+                    }
+            }
+        }
+
+    private val sessionInfoChangeNotify = object : ISocketNotify<SessionInfoChangeNotify> {
+
+        override val uri: String = "SESSION_INFO_CHANGE_NOTIFY"
+
+        override fun needHandle(data: SessionInfoChangeNotify?): Boolean {
+            return data != null
+        }
+
+        override fun onNotify(data: SessionInfoChangeNotify) {
+            val changedSessionInfos = ArrayList<SessionInfo>()
+            data.sessionInfos.forEach {
+                val cache = sessionInfoCache[it.uid]
+                if (cache != null) {
+                    cache.existsNoReplyPaidMessage = it.existsNoReplyPaidMessage
+                }
+                changedSessionInfos.add(it)
+            }
+            data.listTypes.forEach {
+                sessionInfoChangeCallbackMap[it]?.onSessionInfoChanged(changedSessionInfos)
+            }
+        }
+    }
+
     private val im1v1ContinuousSendMessageDeductNotify =
         object : ISocketNotify<Im1v1ContinuousSendMessageDeductNotifyInfo> {
 
@@ -173,74 +270,8 @@ class MessageManager : BaseFrame<IMessageListener>(), IMessageManager,
                 //处理新消息提醒条
                 if (message == null) return false
                 if (offline) return false
-                launch {
-                    val targetIdLong =
-                        if (message.conversationType == Conversation.ConversationType.GROUP) {
-                            message.senderUserId.toLongOrNull()
-                        } else {
-                            message.targetId.toLongOrNull()
-                        } ?: return@launch
-                    if (isSystemTarget(targetIdLong)) {
-                        return@launch
-                    }
-                    if (message.senderUserId == AccountModule.uid.toString()) {
-                        return@launch
-                    }
-                    val msgTag =
-                        message.content?.javaClass?.getAnnotation(MessageTag::class.java)
-                    if (msgTag?.flag != MessageTag.ISCOUNTED) {
-                        //不计数消息不提醒
-                        return@launch
-                    }
-                    if (disableNotifyMessageContentClass.any { it.isInstance(message.content) }) {
-                        //不提醒的消息类型
-                        return@launch
-                    }
-                    val targetUserInfoRlt = ProfileModule.getUserInfoByUid(targetIdLong, true)
-                    val targetUserInfo = (targetUserInfoRlt as? Rlt.Success)?.data ?: return@launch
-                    val deeplink =
-                        if (message.conversationType == Conversation.ConversationType.GROUP) {
-                            val familyInfo = targetUserInfo.familyInfo ?: return@launch
-                            MESSAGE_ROUTER.GroupConversation.getGroupConversationDeeplink(
-                                familyInfo.imGroupId, EnterConversationFrom.Push.name
-                            )
-                        } else {
-                            MESSAGE_ROUTER.Conversation.getPrivateConversationDeeplink(
-                                targetIdLong, EnterConversationFrom.Push.name
-                            )
-                        }
-                    val title = when (msgTag.value) {
-                        CP_INVITE_MESSAGE_TYPE, APP_INFO_MESSAGE_TYPE, FAMILY_INVITE_MESSAGE_TYPE -> {
-                            null
-                        }
-
-                        else -> {
-                            if (message.conversationType == Conversation.ConversationType.GROUP) {
-                                val familyInfo = targetUserInfo.familyInfo ?: return@launch
-                                "${targetUserInfo.name}(${familyInfo.name})"
-                            } else {
-                                targetUserInfo.name
-                            }
-                        }
-                    }
-                    val messageSummary = getMessageSummary(message.content, targetUserInfo.name ?: "")
-                    val newMessageData = NotificationMessage(
-                        targetId = targetIdLong.toString(),
-                        title = title,
-                        message = messageSummary.toString(),
-                        largeIconUrl = targetUserInfo.url,
-                        pushId = System.currentTimeMillis(),
-                        messageType = PushMessageType.IM.type,
-                        deeplink = deeplink
-                    ).apply {
-                        this.messageSummary = messageSummary
-                        this.targetUserInfo = targetUserInfo
-                    }
-                    if (shouldAllowNewMessageInUIScene(newMessageData.targetId)) {
-                        notificationMessageNoticeQueue.add(newMessageData)
-                        showNextNewMessageNotice()
-                    }
-                }
+                handleReceivedMessageNotice(message)
+                showMessageReminderDialogIfNeed(message)
                 return false
             }
 
@@ -250,6 +281,117 @@ class MessageManager : BaseFrame<IMessageListener>(), IMessageManager,
             }
         }
 
+    private fun handleReceivedMessageNotice(message: Message) {
+        launch {
+            val targetIdLong =
+                if (message.conversationType == Conversation.ConversationType.GROUP) {
+                    message.senderUserId.toLongOrNull()
+                } else {
+                    message.targetId.toLongOrNull()
+                } ?: return@launch
+            if (isSystemTarget(targetIdLong)) {
+                return@launch
+            }
+            if (isFilterMessage(message)) {
+                return@launch
+            }
+            val msgTag = message.content?.javaClass?.getAnnotation(MessageTag::class.java)
+            val targetUserInfoRlt = ProfileModule.getUserInfoByUid(targetIdLong, true)
+            val targetUserInfo = (targetUserInfoRlt as? Rlt.Success)?.data ?: return@launch
+            val deeplink = if (message.conversationType == Conversation.ConversationType.GROUP) {
+                val familyInfo = targetUserInfo.familyInfo ?: return@launch
+                MESSAGE_ROUTER.GroupConversation.getGroupConversationDeeplink(
+                    familyInfo.imGroupId, EnterConversationFrom.Push.name
+                )
+            } else {
+                MESSAGE_ROUTER.Conversation.getPrivateConversationDeeplink(
+                    targetIdLong, EnterConversationFrom.Push.name
+                )
+            }
+            val title = when (msgTag?.value) {
+                CP_INVITE_MESSAGE_TYPE, APP_INFO_MESSAGE_TYPE, FAMILY_INVITE_MESSAGE_TYPE -> {
+                    null
+                }
+
+                else -> {
+                    if (message.conversationType == Conversation.ConversationType.GROUP) {
+                        val familyInfo = targetUserInfo.familyInfo ?: return@launch
+                        "${targetUserInfo.name}(${familyInfo.name})"
+                    } else {
+                        targetUserInfo.name
+                    }
+                }
+            }
+            val spend = message.expansion?.get(MessageExpansionKey.Spend.key)
+            val messagePrice = froJsonErrorNull<MessagePriceInfo>(spend)
+            val messageSummary =
+                if (messagePrice != null && messagePrice.targetIncomeCurrencyValue > 0) {
+                    SpannableStringBuilder(
+                        getCompatString(
+                            com.adealink.weparty.message.R.string.message_notificaiton_income_tip,
+                            messagePrice.targetIncomeCurrencyValue
+                        )
+                    )
+                } else {
+                    getMessageSummary(message.content, targetUserInfo.name ?: "")
+                }
+            val messageType =
+                if (messagePrice != null && messagePrice.targetIncomeCurrencyValue > 0) {
+                    PushMessageType.INCOME_IM.type
+                } else {
+                    PushMessageType.IM.type
+                }
+            val newMessageData = NotificationMessage(
+                targetId = targetIdLong.toString(),
+                title = title,
+                message = messageSummary.toString(),
+                largeIconUrl = targetUserInfo.url,
+                pushId = System.currentTimeMillis(),
+                messageType = messageType,
+                deeplink = deeplink
+            ).apply {
+                this.messageSummary = messageSummary
+                this.targetUserInfo = targetUserInfo
+                this.messageId = message.messageId
+            }
+            if (shouldAllowNewMessageInUIScene()) {
+                notificationMessageNoticeQueue.add(newMessageData)
+                showNextNewMessageNotice()
+            }
+        }
+    }
+
+    private fun showMessageReminderDialogIfNeed(message: Message) {
+        if (isSystemTarget(message.targetId)) {
+            return
+        }
+        if (isFilterMessage(message)) {
+            return
+        }
+        val info = message.expansion?.get(MessageExpansionKey.MessageNotifyInfo.key)
+        val messageNotifyInfo = froJsonErrorNull<ImMessageNotifyInfo>(info)
+        if (messageNotifyInfo?.fromSystem == 1) {
+            return
+        }
+        DialogTaskManager.submit(MessageReminderDialogTask(message))
+    }
+
+    private fun isFilterMessage(message: Message): Boolean {
+        if (message.senderUserId == AccountModule.uid.toString()) {
+            return true
+        }
+        val msgTag = message.content?.javaClass?.getAnnotation(MessageTag::class.java)
+        if (msgTag?.flag != MessageTag.ISCOUNTED) {
+            //不计数消息不提醒
+            return true
+        }
+        if (disableNotifyMessageContentClass.any { it.isInstance(message.content) }) {
+            //不提醒的消息类型
+            return true
+        }
+        return false
+    }
+
     private val messageExpansionListener = object : RongIMClient.MessageExpansionListener {
         override fun onMessageExpansionUpdate(
             expansion: MutableMap<String, String>?,
@@ -265,6 +407,12 @@ class MessageManager : BaseFrame<IMessageListener>(), IMessageManager,
                     IMService.innerService.setMessageSentStatus(message, null)
                 }
             }
+            val spend = expansion?.get(MessageExpansionKey.Spend.key)
+            val messagePrice = froJsonErrorNull<MessagePriceInfo>(spend)
+            val serverTimeNow = System.currentTimeMillis() + RongIMClient.getInstance().deltaTime
+            if (messagePrice != null && messagePrice.targetIncomeCurrencyValue > 0 && serverTimeNow - message.sentTime <= messagePrice.messageExpireMilliseconds) {
+                handleReceivedMessageNotice(message)
+            }
         }
 
         override fun onMessageExpansionRemove(
@@ -280,6 +428,9 @@ class MessageManager : BaseFrame<IMessageListener>(), IMessageManager,
     init {
         App.instance.networkService.subscribeNotify(notificationMessageNotify)
         App.instance.networkService.subscribeNotify(im1v1ContinuousSendMessageDeductNotify)
+        App.instance.networkService.subscribeNotify(noReplyPaidMessageNotify)
+        App.instance.networkService.subscribeNotify(firstGetDiamondsFromChatNotify)
+        App.instance.networkService.subscribeNotify(sessionInfoChangeNotify)
     }
 
     override fun appOnCreateMainTask(application: Application) {
@@ -435,11 +586,16 @@ class MessageManager : BaseFrame<IMessageListener>(), IMessageManager,
             return
         }
 
+        if (newMessageData.messageType == PushMessageType.INCOME_IM.type && !shouldShowNewIncomeMessageNotice(newMessageData)) {
+            Log.e(TAG_IM_NEW_MSG_NTF, "IMNewIncomeMessage shouldNotShow, messageData:$newMessageData")
+            return
+        }
+
         Log.i(TAG_IM_NEW_MSG_NTF, "showNotificationMessageNotice, messageData:$newMessageData")
         showNotificationMessageNotice(newMessageData)
     }
 
-    private fun shouldAllowNewMessageInUIScene(targetId: String?): Boolean {
+    private fun shouldAllowNewMessageInUIScene(): Boolean {
         //1. 应用在后台,都展示
         if (AppUtil.background) return true
         //2. 不在会话列表tab页
@@ -453,7 +609,7 @@ class MessageManager : BaseFrame<IMessageListener>(), IMessageManager,
 
     private fun shouldShowNewMessageNotice(data: NotificationMessage): Boolean {
         //1. UI场景判断
-        if (!shouldAllowNewMessageInUIScene(data.targetId)) return false
+        if (!shouldAllowNewMessageInUIScene()) return false
         //2. 币商不频控
         if (data.targetUserInfo?.isMerchant() == true || ProfileModule.getMyUserInfo()?.isMerchant() == true) {
             return true
@@ -464,16 +620,37 @@ class MessageManager : BaseFrame<IMessageListener>(), IMessageManager,
         return true
     }
 
+    private fun shouldShowNewIncomeMessageNotice(data: NotificationMessage): Boolean {
+        //1. UI场景判断
+        if (!shouldAllowNewMessageInUIScene()) return false
+        //2. 币商不频控
+        if (data.targetUserInfo?.isMerchant() == true || ProfileModule.getMyUserInfo()?.isMerchant() == true) {
+            return true
+        }
+        //3. 满足所有条件,允许展示
+        return true
+    }
+
     private fun showNotificationMessageNotice(data: NotificationMessage) {
         launch {
             //应用在后台,发notification
             if (AppUtil.background) {
                 NotificationUtil.createNotificationChannel(IM_NOTIFICATION_CHANNEL_ID)
+                val pushMessage = WeNextPushMessage(
+                    data.toPushMessage(), IM_NOTIFICATION_CHANNEL_ID
+                )
                 NotificationUtil.showNotification(
-                    WeNextPushMessage(
-                        data.toPushMessage(),
-                        IM_NOTIFICATION_CHANNEL_ID
-                    )
+                    pushMessage, {
+                        val messageId = pushMessage.getMessageId()
+                        if (messageId.isNullOrEmpty()) {
+                            return@showNotification
+                        }
+                        val notificationId = msgNotificationIdCache[messageId]?.toInt()
+                        if (notificationId != null) {
+                            NotificationUtil.cancelNotification(notificationId)
+                        }
+                        msgNotificationIdCache.put(messageId, pushMessage.pushId)
+                    }
                 )
                 //模拟3s后消失
                 delay(3000)
@@ -482,9 +659,9 @@ class MessageManager : BaseFrame<IMessageListener>(), IMessageManager,
             }
 
             val largeIconUrl = data.largeIconUrl ?: return@launch
-            //先下载完图片再显示
             val resizeUrl = imageService.getResizeUrl(largeIconUrl, 48.dp(), 48.dp())
-            fetchImage(resizeUrl)
+            //为避免付费消息Push闪烁,不先下载完图片再显示
+//            fetchImage(resizeUrl)
 
             withContext(Dispatcher.UI) {
                 val floatData = when (data.messageType) {
@@ -497,6 +674,13 @@ class MessageManager : BaseFrame<IMessageListener>(), IMessageManager,
                         )
                     }
 
+                    PushMessageType.INCOME_IM.type -> {
+                        NotificationIncomeMessageFloatData(
+                            MODE_APPLICATION, data.apply {
+                                this.largeIconUrl = resizeUrl
+                            })
+                    }
+
                     else -> {
                         NotificationMessageFloatData(
                             MODE_APPLICATION,
@@ -516,6 +700,29 @@ class MessageManager : BaseFrame<IMessageListener>(), IMessageManager,
                             }
                         }
                     }
+                if (floatData is NotificationIncomeMessageFloatData) {
+                    WindowManagerProxy.getWindowManager()
+                        .findAllFloatViews(FloatWindowType.NOTIFICATION_MESSAGE).mapNotNull {
+                            (it as? NotificationMessageFloatView)
+                        }.forEach {
+                            if (floatData.data.messageId == it.notificationMessageFloatData.data.messageId) {
+                                WindowManagerProxy.getWindowManager().removeView(it)
+                            }
+                        }
+                } else if (floatData is NotificationMessageFloatData) {
+                    WindowManagerProxy.getWindowManager()
+                        .findAllFloatViews(FloatWindowType.NOTIFICATION_INCOME_MESSAGE).mapNotNull {
+                            (it as? NotificationIncomeMessageFloatView)
+                        }.forEach {
+                            if (floatData.data.messageId == it.notificationIncomeMessageFloatData.data.messageId) {
+                                Log.e(
+                                    TAG_IM_UI,
+                                    "notification Message filter,messageId:${floatData.data.messageId}"
+                                )
+                                return@withContext
+                            }
+                        }
+                }
                 if (floatView != null) {
                     WindowManagerProxy.getWindowManager().addView(floatView)
                 }
@@ -541,7 +748,7 @@ class MessageManager : BaseFrame<IMessageListener>(), IMessageManager,
         }
     }
 
-    private fun getMessageSummary(messageContent: MessageContent, targetName: String): Spannable {
+    override fun getMessageSummary(messageContent: MessageContent, targetName: String): Spannable {
         if (messageContent is CPInviteMessage) {
             val giftIconUrl = messageContent.giftIcon
             val giftPrice = messageContent.giftPrice
@@ -570,6 +777,14 @@ class MessageManager : BaseFrame<IMessageListener>(), IMessageManager,
         return IMConfigCenter.conversationConfig.getMessageSummary(messageContent)
     }
 
+    override fun addSessionInfoChangeCallback(listType: Int, callback: ISessionInfoChangeCallback) {
+        sessionInfoChangeCallbackMap[listType] = callback
+    }
+
+    override fun removeSessionInfoChangeCallback(listType: Int) {
+        sessionInfoChangeCallbackMap.remove(listType)
+    }
+
     private fun handleIm1v1ContinuousSendMessageDeductNotify(data: Im1v1ContinuousSendMessageDeductNotifyInfo) {
         dispatch {
             it.onIm1v1ContinuousSendMessageDeductNotify(data)
@@ -578,6 +793,7 @@ class MessageManager : BaseFrame<IMessageListener>(), IMessageManager,
 
     override fun logout() {
         sayHiCache.clear()
+        sessionInfoCache.evictAll()
         launch {
             gettingUserInfoSet.clear()
         }
@@ -620,6 +836,8 @@ class MessageManager : BaseFrame<IMessageListener>(), IMessageManager,
         }
     }
 
+    private val msgNotificationIdCache = TimeoutLruCache<String, Long>(500, 5 * 60 * 1000)//最多500个,缓存5分钟
+
     private val sessionInfoCache = TimeoutLruCache<Long, SessionInfo>(500, 5 * 60 * 1000)//最多500个,缓存5分钟
     override suspend fun batchGetSessionInfo(
         targetIdSet: Set<Long>,
@@ -668,6 +886,31 @@ class MessageManager : BaseFrame<IMessageListener>(), IMessageManager,
         return resultMap
     }
 
+    override suspend fun batchSendQuickMessage(uidList: List<Long>): Rlt<Any> {
+        return when (val rlt =
+            messageHttpService.batchSendQuickMessage(BatchSendQuickMessageReq(uidList))) {
+            is Rlt.Success -> {
+                Rlt.Success(true)
+            }
+
+            is Rlt.Failed -> {
+                rlt
+            }
+        }
+    }
+
+    override suspend fun getNoReplyMessage(): Rlt<NoReplyMessageRsp?> {
+        return when (val rlt = messageHttpService.getNoReplyMessage()) {
+            is Rlt.Success -> {
+                Rlt.Success(rlt.data.data)
+            }
+
+            is Rlt.Failed -> {
+                rlt
+            }
+        }
+    }
+
     override fun onAddIntimacyVal(intimacyValInfo: IntimacyValInfo) {
         super.onAddIntimacyVal(intimacyValInfo)
         if (!intimacyValInfo.isSelfInvolved()) {

+ 60 - 0
module/message/src/main/java/com/adealink/weparty/message/util/MessageNoticeUtils.kt

@@ -0,0 +1,60 @@
+package com.adealink.weparty.message.util
+
+import android.content.Context
+import android.media.AudioManager
+import android.media.MediaPlayer
+import android.os.Vibrator
+import android.provider.Settings
+import com.adealink.frame.log.Log
+import com.adealink.frame.util.AppUtil
+import com.adealink.weparty.App
+
+/**
+ * Created by LfJ on 2025/8/26.
+ */
+object MessageNoticeUtils {
+
+    private const val TAG = "MessageNoticeUtils"
+
+    private val vibrator: Vibrator? by lazy {
+        AppUtil.getSystemService<Vibrator>(Context.VIBRATOR_SERVICE)
+    }
+
+    fun triggerSoundAndVibration() {
+        try {
+            val context = App.instance.baseContext
+            val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
+            val ringerMode = audioManager.ringerMode
+            // 1. 播放声音(系统通知音)
+            if (ringerMode == AudioManager.RINGER_MODE_NORMAL) {
+                val mediaPlayer =
+                    MediaPlayer.create(context, Settings.System.DEFAULT_NOTIFICATION_URI)
+                mediaPlayer?.start()
+            }
+            if (ringerMode == AudioManager.RINGER_MODE_NORMAL || ringerMode == AudioManager.RINGER_MODE_VIBRATE) {
+                vibrate()
+            }
+        } catch (e: Exception) {
+            Log.e(TAG, "triggerSoundAndVibration e:$e")
+        }
+    }
+
+    private fun vibrate() {
+        if (vibrator?.hasVibrator() != true) {
+            return
+        }
+        try {
+            val pattern = longArrayOf(0, 200, 800, 200, 800) // 震动-停止-震动-停止
+            vibrator?.vibrate(pattern, 0)
+        } catch (e: Exception) {
+            Log.e(TAG, "vibrate e:$e")
+        }
+    }
+
+    fun stopVibrate() {
+        if (vibrator?.hasVibrator() != true) {
+            return
+        }
+        vibrator?.cancel()
+    }
+}

+ 28 - 0
module/message/src/main/java/com/adealink/weparty/message/viewmodel/MessageViewModel.kt

@@ -31,6 +31,9 @@ import com.adealink.weparty.module.wallet.data.getErrorByServerError
 import com.adealink.weparty.stat.SendIMStatEvent
 import kotlinx.coroutines.launch
 import com.adealink.frame.base.Rlt
+import com.adealink.frame.log.Log
+import com.adealink.weparty.module.message.data.UnreadPaidSessionListReq
+import com.adealink.weparty.module.message.data.UnreadPaidSessionListRsp
 
 class MessageViewModel : BaseViewModel(), IMessageViewModel, IMessageListener {
     override val chatPageDetailResLD: ExtLiveData<Rlt<ChatPageDetailRes>> = ExtMutableLiveData()
@@ -41,10 +44,16 @@ class MessageViewModel : BaseViewModel(), IMessageViewModel, IMessageListener {
     override val sayHiLD: ExtLiveData<Long> = ExtMutableLiveData()
     override val im1v1ContinuousSendMessageDeductNotifyLD: ExtLiveData<Im1v1ContinuousSendMessageDeductNotifyInfo> =
         ExtMutableLiveData()
+    override val unreadPaidSessionListRspLD: ExtLiveData<UnreadPaidSessionListRsp> =
+        ExtMutableLiveData()
 
     private val messageHttpService =
         App.instance.networkService.getHttpService(MessageHttpService::class.java)
 
+    companion object {
+        const val TAG = "MessageViewModel"
+    }
+
     init {
         messageManager.addListener(this)
     }
@@ -285,6 +294,25 @@ class MessageViewModel : BaseViewModel(), IMessageViewModel, IMessageListener {
         return liveData
     }
 
+    override fun getUnreadPaidSessionList(req: UnreadPaidSessionListReq) {
+        viewModelScope.launch {
+            when (val rlt = messageHttpService.unreadPaidSessionList(req)) {
+                is Rlt.Success -> {
+                    val res = rlt.data.data
+                    if (res != null) {
+                        unreadPaidSessionListRspLD.send(res)
+                    } else {
+                        Log.e(TAG, "getUnreadPaidSessionList null")
+                    }
+                }
+
+                is Rlt.Failed -> {
+                    Log.e(TAG, "getUnreadPaidSessionList failed")
+                }
+            }
+        }
+    }
+
     override fun batchGetSendQuickMessageTs(uidSet: Set<Long>): LiveData<Rlt<Map<Long, Boolean>>> {
         val liveData = OnceMutableLiveData<Rlt<Map<Long, Boolean>>>()
         viewModelScope.launch {

+ 6 - 0
module/message/src/main/res/drawable-xhdpi/message_notification_income_message_btn_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/color_FFFFFF" />
+    <corners android:radius="28dp" />
+</shape>

BIN
module/message/src/main/res/drawable-xhdpi/message_notification_income_message_sign.webp


+ 6 - 0
module/message/src/main/res/drawable/im_conversation_income_diamond_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">
+    <corners android:radius="28dp" />
+    <solid android:color="@color/color_1A55CCFF" />
+</shape>

+ 9 - 0
module/message/src/main/res/drawable/message_income_float_view_bg.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <gradient
+        android:angle="0"
+        android:endColor="#F99BFF"
+        android:startColor="#7F61FE" />
+    <corners android:radius="12dp" />
+</shape>

+ 12 - 1
module/message/src/main/res/layout/fragment_conversation.xml

@@ -97,10 +97,21 @@
         android:visibility="gone"
         tools:visibility="visible"
         android:layout_marginBottom="12dp"
-        android:layout_marginEnd="16dp"
+        android:layout_marginStart="16dp"
         tools:text="100+"
         android:drawableStart="@drawable/im_conversation_newmsg"
         app:layout_constraintBottom_toBottomOf="@id/refresh_layout"
+        app:layout_constraintStart_toStartOf="parent" />
+
+    <com.adealink.weparty.message.conversation.view.UnreadConversationView
+        android:id="@+id/unread_conversation"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="12dp"
+        android:layout_marginBottom="54dp"
+        android:visibility="gone"
+        tools:visibility="visible"
+        app:layout_constraintBottom_toTopOf="@id/input_panel_container"
         app:layout_constraintEnd_toEndOf="parent" />
 
     <include

+ 26 - 2
module/message/src/main/res/layout/im_conversationlist_item.xml

@@ -119,9 +119,9 @@
             app:layout_constraintTop_toTopOf="@id/im_conversation_title"
             app:layout_constraintBottom_toBottomOf="@id/im_conversation_title"
             app:layout_constraintStart_toEndOf="@id/im_conversation_title"
-            app:layout_constraintEnd_toStartOf="@id/im_conversation_date">
+            app:layout_constraintEnd_toStartOf="@id/cl_im_conversation_income_diamond">
 
-            <!--客服标签-->
+        <!--客服标签-->
             <androidx.appcompat.widget.AppCompatTextView
                 android:id="@+id/im_conversation_customer_service_tv"
                 android:layout_width="wrap_content"
@@ -189,6 +189,30 @@
             app:layout_constraintTop_toBottomOf="@+id/im_conversation_title"
             tools:text="你好,朋友!" />
 
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:id="@+id/cl_im_conversation_income_diamond"
+            android:layout_width="40dp"
+            android:layout_height="20dp"
+            android:layout_marginEnd="6dp"
+            android:visibility="gone"
+            tools:visibility="visible"
+            android:background="@drawable/im_conversation_income_diamond_bg"
+            app:layout_constraintBottom_toBottomOf="@id/im_conversation_title"
+            app:layout_constraintEnd_toStartOf="@+id/im_conversation_date"
+            app:layout_constraintTop_toTopOf="@+id/im_conversation_title">
+
+            <com.opensource.svgaplayer.WenextSvgaView
+                android:layout_width="23dp"
+                android:layout_height="23dp"
+                app:autoPlay="true"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="parent"
+                app:source="message_income_diamond.svga" />
+
+        </androidx.constraintlayout.widget.ConstraintLayout>
+
         <TextView
             android:id="@+id/im_conversation_date"
             style="@style/IMTextStyle.Alignment"

+ 130 - 0
module/message/src/main/res/layout/layout_notification_income_message_view.xml

@@ -0,0 +1,130 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout 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:layoutDirection="locale">
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="match_parent"
+        android:layout_height="80dp"
+        android:layout_gravity="center_vertical"
+        android:background="@drawable/message_income_float_view_bg">
+
+        <androidx.appcompat.widget.AppCompatImageView
+            android:id="@+id/iv_sign"
+            android:layout_width="35dp"
+            android:layout_height="35dp"
+            android:background="@drawable/message_notification_income_message_sign"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent" />
+
+        <com.adealink.weparty.commonui.imageview.AvatarView
+            android:id="@+id/avatar_iv"
+            android:layout_width="52dp"
+            android:layout_height="52dp"
+            android:layout_marginStart="12dp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent" />
+
+        <androidx.appcompat.widget.AppCompatImageView
+            android:id="@+id/iv_online"
+            android:layout_width="10dp"
+            android:layout_height="10dp"
+            android:layout_gravity="top|end"
+            android:layout_marginEnd="5dp"
+            android:background="@drawable/common_online_ic"
+            app:layout_constraintEnd_toEndOf="@+id/avatar_iv"
+            app:layout_constraintTop_toTopOf="@+id/avatar_iv" />
+
+        <com.adealink.weparty.module.profile.view.UserCountryView
+            android:id="@+id/user_country_view"
+            android:layout_width="16dp"
+            android:layout_height="16dp"
+            android:layout_marginStart="12dp"
+            android:layout_marginEnd="4dp"
+            app:layout_constraintBottom_toBottomOf="@+id/name_tv"
+            app:layout_constraintStart_toEndOf="@+id/avatar_iv"
+            app:layout_constraintTop_toTopOf="@+id/name_tv"
+            tools:background="@drawable/label_good_id_bg"
+            tools:ignore="MissingConstraints" />
+
+        <androidx.appcompat.widget.AppCompatTextView
+            android:id="@+id/name_tv"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="4dp"
+            android:ellipsize="end"
+            android:gravity="start"
+            android:includeFontPadding="false"
+            android:lines="1"
+            android:textAlignment="viewStart"
+            android:textColor="@color/color_FFFFFF"
+            android:textSize="15sp"
+            android:textStyle="bold"
+            app:layout_constraintBottom_toTopOf="@+id/msg_income_tip"
+            app:layout_constraintEnd_toStartOf="@+id/right_cl"
+            app:layout_constraintStart_toEndOf="@+id/user_country_view"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintVertical_chainStyle="packed"
+            tools:text="Rella" />
+
+        <androidx.appcompat.widget.AppCompatTextView
+            android:id="@+id/msg_income_tip"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="6dp"
+            android:gravity="start"
+            android:textAlignment="viewStart"
+            android:textColor="@color/color_FFFFFF"
+            android:textSize="12sp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toStartOf="@id/right_cl"
+            app:layout_constraintStart_toStartOf="@+id/user_country_view"
+            app:layout_constraintTop_toBottomOf="@id/name_tv"
+            app:layout_goneMarginTop="0dp"
+            tools:text="Reply to the message to get 500 diamonds" />
+
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:id="@+id/right_cl"
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            android:layout_marginEnd="12dp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toTopOf="parent">
+
+            <com.adealink.weparty.commonui.widget.MediumTextView
+                android:id="@+id/to_view_btn"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:background="@drawable/message_notification_income_message_btn_bg"
+                android:paddingHorizontal="12dp"
+                android:paddingVertical="5dp"
+                android:textColor="@color/color_app_main"
+                android:textSize="12sp"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/time_tv"
+                app:layout_constraintVertical_chainStyle="packed"
+                tools:text="@string/message_reply_btn" />
+
+            <androidx.appcompat.widget.AppCompatTextView
+                android:id="@+id/time_tv"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:textColor="@color/color_FFFFFF"
+                android:textSize="11sp"
+                app:layout_constraintBottom_toTopOf="@+id/to_view_btn"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="parent"
+                tools:text="@string/message_now" />
+
+        </androidx.constraintlayout.widget.ConstraintLayout>
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+</FrameLayout>

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

@@ -153,4 +153,8 @@
     <string name="message_notification_list_title">إشعار</string>
     <string name="message_gift_message_you">أنت</string>
     <string name="message_gift_message_prefix">مرسل</string>
+    <string name="message_now">الآن</string>
+    <string name="message_reply_btn">رد</string>
+    <string name="message_notificaiton_income_tip">اكسب %s ماسة عند الرد على الرسالة</string>
+    <string name="message_tab_income">دخل</string>
 </resources>

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

@@ -152,4 +152,8 @@
     <string name="message_notification_list_title">通知</string>
     <string name="message_gift_message_you">你</string>
     <string name="message_gift_message_prefix">发送</string>
+    <string name="message_now">现在</string>
+    <string name="message_reply_btn">回复</string>
+    <string name="message_notificaiton_income_tip">回复消息最多获得%s钻石</string>
+    <string name="message_tab_income">收入</string>
 </resources>

Некоторые файлы не были показаны из-за большого количества измененных файлов