Эх сурвалжийг харах

feat: 游戏代币 (#61)

* feat: 代币,迁移相关的桥接方法

* feat: 迁移相关的sp值

* feat: 测试代码,写死true

* feat: 测试代码,写死true

* feat: CoinHistoryListActivity,改成viewpager2

* feat: 接入LuckyCoinsFragment

* feat: 新增枚举

* feat: Android代币联调

* feat: Android代币联调

* feat: Android代币多语言

* feat: 联调修改

* feat: 检查幸运币数量

* feat: 多语言文案

* feat: 用户在线状态显示

* feat: 个人资料页,用户在房显示

* feat: 在线时间多语言文案

* feat: 在线信息联调

* feat: 在线信息联调

* feat: 官号消息不可删除

* feat: 每日充值展示问题

* feat: 添加CommonActivityRewardDialog

* feat: 进入房间打开游戏面板

* feat: 弹窗修改实现逻辑

* feat: 多语言

* feat: MainStartUpFragment问题修复

* feat: 弹窗修改实现逻辑

* feat: 多语言文案

* feat: replyAnchorMessage

* feat: 奖励展示

* fix: initCamera crash
LXD312569496 9 сар өмнө
parent
commit
a458856cdd
71 өөрчлөгдсөн 1983 нэмэгдсэн , 286 устгасан
  1. BIN
      app/src/main/assets/im_user_in_room.svga
  2. 4 0
      app/src/main/java/com/adealink/weparty/cocosgame/manager/CocosWebGameManager.kt
  3. 8 0
      app/src/main/java/com/adealink/weparty/commonui/dialogchain/BaseDialogData.kt
  4. 2 0
      app/src/main/java/com/adealink/weparty/config/Data.kt
  5. 9 0
      app/src/main/java/com/adealink/weparty/module/anchor/AnchorModule.kt
  6. 3 0
      app/src/main/java/com/adealink/weparty/module/anchor/IAnchorService.kt
  7. 28 4
      app/src/main/java/com/adealink/weparty/module/anchor/data/AnchorData.kt
  8. 7 0
      app/src/main/java/com/adealink/weparty/module/operation/Router.kt
  9. 20 3
      app/src/main/java/com/adealink/weparty/module/profile/data/ProfileData2.kt
  10. 55 25
      app/src/main/java/com/adealink/weparty/module/profile/view/UserOnlineStatusView.kt
  11. 3 0
      app/src/main/java/com/adealink/weparty/module/profile/viewmodel/IProfileViewModel.kt
  12. 2 0
      app/src/main/java/com/adealink/weparty/module/room/IRoomService.kt
  13. 8 0
      app/src/main/java/com/adealink/weparty/module/room/RoomModule.kt
  14. 2 0
      app/src/main/java/com/adealink/weparty/module/room/Router.kt
  15. 274 0
      app/src/main/java/com/adealink/weparty/module/task/CommonActivityRewardDialog.kt
  16. 64 0
      app/src/main/java/com/adealink/weparty/module/task/CommonActivityRewardDialogTask.kt
  17. 1 1
      app/src/main/java/com/adealink/weparty/module/task/Data.kt
  18. 5 0
      app/src/main/java/com/adealink/weparty/module/task/UserTaskHttpService.kt
  19. 25 2
      app/src/main/java/com/adealink/weparty/module/wallet/data/WalletData.kt
  20. 10 0
      app/src/main/java/com/adealink/weparty/module/wallet/viewmodel/IWalletViewModel.kt
  21. 1 0
      app/src/main/java/com/adealink/weparty/stat/constant/Page.kt
  22. 7 8
      app/src/main/java/com/adealink/weparty/ui/MainStartUpFragment.kt
  23. 33 0
      app/src/main/java/com/adealink/weparty/ui/home/BaseHomeFragment.kt
  24. 12 1
      app/src/main/java/com/adealink/weparty/webview/datasource/local/WebLocalService.kt
  25. 42 0
      app/src/main/java/com/adealink/weparty/webview/jsbridge/method/KVStorageGetJSNativeMethod.kt
  26. 33 0
      app/src/main/java/com/adealink/weparty/webview/jsbridge/method/KVStorageSetJSNativeMethod.kt
  27. 166 0
      app/src/main/res/layout/layout_activity_reward_dialog.xml
  28. 5 0
      app/src/main/res/values-ar/strings.xml
  29. 5 0
      app/src/main/res/values-zh/strings.xml
  30. 6 1
      app/src/main/res/values/strings.xml
  31. 5 0
      module/anchor/src/main/java/com/adealink/weparty/anchor/AnchorServiceImpl.kt
  32. 29 0
      module/anchor/src/main/java/com/adealink/weparty/anchor/manager/AnchorManager.kt
  33. 2 0
      module/anchor/src/main/java/com/adealink/weparty/anchor/manager/IAnchorManager.kt
  34. 10 9
      module/certification/src/main/java/com/adealink/weparty/certification/activity/BaseLivenessActivity.kt
  35. 24 1
      module/message/src/main/java/com/adealink/weparty/message/conversation/comp/ConversationTargetInfoComp.kt
  36. 13 3
      module/message/src/main/java/com/adealink/weparty/message/conversation/extension/longclick/MessageItemLongClickActionManager.kt
  37. 25 0
      module/message/src/main/res/layout/layout_conversation_top_bar_center_new.xml
  38. 1 1
      module/operation/src/main/java/com/adealink/weparty/operation/rechargepackage/RechargeDailyFragment.kt
  39. 9 0
      module/profile/src/main/java/com/adealink/weparty/profile/datasource/ProfileHttpService.kt
  40. 3 0
      module/profile/src/main/java/com/adealink/weparty/profile/manager/IProfileManager.kt
  41. 13 0
      module/profile/src/main/java/com/adealink/weparty/profile/manager/ProfileManager.kt
  42. 2 1
      module/profile/src/main/java/com/adealink/weparty/profile/userprofile/NewUserProfileActivity.kt
  43. 24 4
      module/profile/src/main/java/com/adealink/weparty/profile/userprofile/component/ProfileUserBasisInfoComp.kt
  44. 6 1
      module/profile/src/main/java/com/adealink/weparty/profile/userprofile/fragment/PersonalFragment.kt
  45. 23 1
      module/profile/src/main/java/com/adealink/weparty/profile/viewmodel/ProfileViewModel.kt
  46. 3 3
      module/profile/src/main/res/drawable-ldrtl/profile_in_room_white_bg.xml
  47. 3 3
      module/profile/src/main/res/drawable/profile_in_room_white_bg.xml
  48. 41 2
      module/profile/src/main/res/layout/activity_user_profile_new.xml
  49. 4 0
      module/room/src/main/java/com/adealink/weparty/room/RoomServiceImpl.kt
  50. 7 0
      module/room/src/main/java/com/adealink/weparty/room/operate/RoomBottomOperateFragment.kt
  51. 207 0
      module/wallet/src/main/java/com/adealink/weparty/wallet/coin/CoinHistoryFragment.kt
  52. 43 165
      module/wallet/src/main/java/com/adealink/weparty/wallet/coin/CoinHistoryListActivity.kt
  53. 104 0
      module/wallet/src/main/java/com/adealink/weparty/wallet/coin/LuckyCoinsFragment.kt
  54. 2 0
      module/wallet/src/main/java/com/adealink/weparty/wallet/data/RechargeHistoryListData.kt
  55. 13 0
      module/wallet/src/main/java/com/adealink/weparty/wallet/datasource/remote/WalletHttpService.kt
  56. 4 0
      module/wallet/src/main/java/com/adealink/weparty/wallet/listener/IWalletListener.kt
  57. 10 0
      module/wallet/src/main/java/com/adealink/weparty/wallet/manager/IWalletManager.kt
  58. 38 0
      module/wallet/src/main/java/com/adealink/weparty/wallet/manager/WalletManager.kt
  59. 75 0
      module/wallet/src/main/java/com/adealink/weparty/wallet/util/WalletUIUtil.kt
  60. 61 0
      module/wallet/src/main/java/com/adealink/weparty/wallet/view/LuckyCoinsItemViewBinder.kt
  61. 92 1
      module/wallet/src/main/java/com/adealink/weparty/wallet/viewmodel/WalletViewModel.kt
  62. BIN
      module/wallet/src/main/res/drawable-xhdpi/wallet_lucky_coin_ic.png
  63. 16 46
      module/wallet/src/main/res/layout/activity_coin_history.xml
  64. 59 0
      module/wallet/src/main/res/layout/fragment_gold_coin_history.xml
  65. 34 0
      module/wallet/src/main/res/layout/fragment_lucky_coins_history.xml
  66. 104 0
      module/wallet/src/main/res/layout/layout_lucky_coins_item.xml
  67. 3 0
      module/wallet/src/main/res/values-ar/strings.xml
  68. 3 0
      module/wallet/src/main/res/values-zh/strings.xml
  69. 3 0
      module/wallet/src/main/res/values/strings.xml
  70. 6 0
      module/webview/src/main/java/com/adealink/weparty/webview/WeNextWebView.kt
  71. 19 0
      module/webview/src/main/java/com/adealink/weparty/webview/jsnativemethod/PerformanceHelperJSNativeMethod.kt

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


+ 4 - 0
app/src/main/java/com/adealink/weparty/cocosgame/manager/CocosWebGameManager.kt

@@ -47,6 +47,8 @@ import com.adealink.weparty.cocosgame.web.CocosGameWebView
 import com.adealink.weparty.module.gamehub.carrom.method.OnCarromShowDialogJsMethod
 import com.adealink.weparty.module.gamehub.ludo.method.OnLudoShowDialogJsMethod
 import com.adealink.weparty.module.gamehub.uno.method.OnUnoShowDialogJsMethod
+import com.adealink.weparty.webview.jsbridge.method.KVStorageSetJSNativeMethod
+import com.adealink.weparty.webview.jsnativemethod.KVStorageGetJSNativeMethod
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
 import java.lang.ref.WeakReference
@@ -170,6 +172,8 @@ class CocosWebGameManager(private val l: ICocosGameListener) :
             webView.addJSNativeMethod(OnGameRecoverFailJsMethod(l))
             webView.addJSNativeMethod(OnGetAppEnvJsMethod())
             webView.addJSNativeMethod(OnNodeVisibleChangedJsMethod(l))
+            webView.addJSNativeMethod(KVStorageGetJSNativeMethod())
+            webView.addJSNativeMethod(KVStorageSetJSNativeMethod())
             App.instance.networkService.addNotifyInterceptor(notifyInterceptor)
             registerNetworkListener(this)
         }

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

@@ -2,6 +2,7 @@ package com.adealink.weparty.commonui.dialogchain
 
 import android.os.Parcelable
 import com.adealink.weparty.module.anchor.data.AnchorMessage
+import com.adealink.weparty.module.anchor.data.CommonActivityRewardMessage
 import com.adealink.weparty.module.level.data.CommonLevelChangeNotify
 import com.adealink.weparty.module.level.data.LevelUpgradeNotify
 import com.adealink.weparty.module.operation.signinreward.data.DailySignInRewardsRes
@@ -138,5 +139,12 @@ sealed class BaseDialogData(
     @Parcelize
     class MsgSendFailedDialogData(val countDownTime: Long, val totalCount: Long) : Parcelable,
         BaseDialogData("MsgSendFailedDialog")
+
+    @Parcelize
+    data class CommonActivityDialogData(
+        val messageId: Long,
+        val messageType: Int,
+        val rewareMessage : CommonActivityRewardMessage
+    ) : Parcelable, BaseDialogData("CommonActivityDialog")
 }
 

+ 2 - 0
app/src/main/java/com/adealink/weparty/config/Data.kt

@@ -76,6 +76,8 @@ enum class GlobalConfigType(val value: Int) {
     GLOBAL_TEXAS_COWBOY_VERSION_INFO(83), //texas cowboy游戏版本信息
     GLOBAL_JACKPOT_SLOT_VERSION_INFO(84), //jackpot slot游戏版本信息
     GLOBAL_DRAGON_TIGER_FIGHT_VERSION_INFO(85), //dragon tiger fight游戏版本信息
+
+    GLOBAL_CONFIG_LUCKY_COIN_REGION(86), //代币,可见大区
     GLOBAL_APPROVING_VERSION(90),//提审版本信息
     GLOBAL_CALL_1V1(97), //1v1Call
     GLOBAL_MALE_DAILY_CHARGE_ACTIVITY_SWITCH(99), // 男性每日充值活动开关配置

+ 9 - 0
app/src/main/java/com/adealink/weparty/module/anchor/AnchorModule.kt

@@ -8,6 +8,7 @@ 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.anchor.viewmodel.IAnchorViewModel
+import okhttp3.RequestBody
 
 
 object AnchorModule : BaseDynamicModule<IAnchorService>(IAnchorService::class), IAnchorService {
@@ -37,6 +38,10 @@ object AnchorModule : BaseDynamicModule<IAnchorService>(IAnchorService::class),
         getService().handleAnchorTemplateMessage(message)
     }
 
+    override fun msgBtnHttpReq(url: String, body: RequestBody?) {
+        getService().msgBtnHttpReq(url, body)
+    }
+
     override fun emptyService(): IAnchorService {
         return object : IAnchorService {
             override fun getAnchorViewModel(owner: ViewModelStoreOwner): IAnchorViewModel? {
@@ -63,6 +68,10 @@ object AnchorModule : BaseDynamicModule<IAnchorService>(IAnchorService::class),
             override fun handleAnchorTemplateMessage(message: AnchorMessage) {
                 // nth
             }
+
+            override fun msgBtnHttpReq(url: String, body: RequestBody?) {
+
+            }
         }
     }
 

+ 3 - 0
app/src/main/java/com/adealink/weparty/module/anchor/IAnchorService.kt

@@ -7,6 +7,7 @@ import com.adealink.frame.aab.IService
 import com.adealink.weparty.module.anchor.data.AnchorMessage
 import com.adealink.weparty.module.anchor.data.AnchorMessageReplyCode
 import com.adealink.weparty.module.anchor.viewmodel.IAnchorViewModel
+import okhttp3.RequestBody
 
 
 interface IAnchorService : IService<IAnchorService> {
@@ -24,4 +25,6 @@ interface IAnchorService : IService<IAnchorService> {
      * @message:AnchorMessage
      */
     fun handleAnchorTemplateMessage(message: AnchorMessage)
+
+    fun msgBtnHttpReq(url: String, body: RequestBody?)
 }

+ 28 - 4
app/src/main/java/com/adealink/weparty/module/anchor/data/AnchorData.kt

@@ -2,6 +2,7 @@ package com.adealink.weparty.module.anchor.data
 
 import android.os.Parcelable
 import com.adealink.frame.data.json.froJsonErrorNull
+import com.adealink.weparty.module.operation.data.RewardDetailData
 import com.google.gson.annotations.GsonNullable
 import com.google.gson.annotations.Must
 import com.google.gson.annotations.SerializedName
@@ -102,6 +103,14 @@ data class AnchorMessage(
             }
             return field
         }
+
+    var commonActivityRewardMessage: CommonActivityRewardMessage? = null
+        get() {
+            if (field == null) {
+                field = froJsonErrorNull(messageBody)
+            }
+            return field
+        }
     /**
      * Body作通用模版
      */
@@ -130,7 +139,9 @@ enum class AnchorBeReportedMessageType(val type: Int) {
 enum class AnchorMessageType(val type: Int) {
     MESSAGR_DIAMOND_REWARD_NOTIFY(29),//工会长获得每月的钻石激励通知
     MESSAGE_IM_DAILY_GREETING_NOTIFY(30), //日常消息上限提醒
-    MESSAGE_USER_CHANGE_GENDER(32); // 性别变更
+    MESSAGE_USER_CHANGE_GENDER(32), // 性别变更
+    COMMON_ACTIVITY_REWARD_NOTIFY(35) // 通用活动奖励通知
+    ;
 
     companion object {
         fun map(type: Int): AnchorMessageType? {
@@ -166,6 +177,7 @@ enum class AnchorMessageTemplateType(val type: Int) {
     }
 }
 
+@Parcelize
 data class AnchorMessageButton(
     @SerializedName("buttonType")
     val buttonType: Int,//按钮类型
@@ -187,7 +199,7 @@ data class AnchorMessageButton(
     @GsonNullable
     @SerializedName("backgroundGradient")
     val backgroundGradient: String? = null,//按钮背景渐变色配置
-)
+) : Parcelable
 
 enum class AnchorMsgBtnType(val type: Int) {
     Normal(0),//普通按钮
@@ -206,7 +218,8 @@ enum class AnchorMsgBtnAction(val action: Int) {
     HttpPost(1),//http post请求
     Deeplink(2),//deeplink
     Logout(3),//退出登录
-    ExitRoom(4);//退出房间
+    ExitRoom(4),//退出房间
+    GO_ROOM_AND_OPEN_GAME(5);//进入房间并打开游戏
 
     companion object {
         fun map(action: Int): AnchorMsgBtnAction? {
@@ -270,4 +283,15 @@ data class DailyGreetingMessage(
 
 enum class DailyGreetingMessageType(val type: Int) {
     LimitCount(1), SendFailed(2)
-}
+}
+
+
+@Parcelize
+data class CommonActivityRewardMessage(
+    @SerializedName("title") val title: String? = null, //弹窗标题
+    @GsonNullable
+    @SerializedName("text") val text: String?, //弹窗文案
+    @GsonNullable
+    @SerializedName("buttons") val buttons: List<AnchorMessageButton>? = null,
+    @SerializedName("rewardPackageId") val rewardPackageId: Long? = null
+) : Parcelable

+ 7 - 0
app/src/main/java/com/adealink/weparty/module/operation/Router.kt

@@ -177,4 +177,11 @@ interface Operation {
         }
     }
 
+
+    interface CommonActivityRewardDialog {
+        companion object {
+            const val PATH = "/common_activity_reward_dialog"
+            const val EXTRA_DIALOG_DATA = "extra_dialog_data"
+        }
+    }
 }

+ 20 - 3
app/src/main/java/com/adealink/weparty/module/profile/data/ProfileData2.kt

@@ -205,8 +205,8 @@ data class UserInfo(
     fun isFemale() = gender == Gender.FEMALE.gender
 
     fun isMerchant(): Boolean {
-        if(merchantType != null) {
-            if(merchantType != NORMAL_USER) {
+        if (merchantType != null) {
+            if (merchantType != NORMAL_USER) {
                 return true
             }
         }
@@ -962,4 +962,21 @@ data class CommonLevelInfo(
     @SerializedName("nextScore") val nextScore: Long?,
     @SerializedName("lastLevel") val lastLevel: Int?,
     @SerializedName("lastScore") val lastScore: Long?,
-) : Parcelable
+) : Parcelable
+
+
+data class GetOnlineInfoReq(
+    @SerializedName("uids") val uids: List<Long>
+)
+
+data class GetOnlineInfoRes(
+    @SerializedName("uid2OnlineInfoMap") val uid2OnlineInfoMap: Map<Long, UserOnlineInfo>
+)
+
+data class UserOnlineInfo(
+    @SerializedName("onlineStatus") val onlineStatus: Int, //用户在线状态,0:离线,1:在线
+    @SerializedName("chatRoom") val chatRoom: Long, //用户所在语聊房间
+    @SerializedName("lastOnlineTs") val lastOnlineTs: Long? = null //最近一次在线时间
+)
+
+

+ 55 - 25
app/src/main/java/com/adealink/weparty/module/profile/view/UserOnlineStatusView.kt

@@ -6,24 +6,22 @@ import android.view.LayoutInflater
 import android.widget.FrameLayout
 import com.adealink.frame.aab.util.getCompatColor
 import com.adealink.frame.aab.util.getCompatString
+import com.adealink.frame.util.ONE_DAY
+import com.adealink.frame.util.ONE_HOUR
+import com.adealink.frame.util.ONE_MINUTE
 import com.adealink.weparty.R
 import com.adealink.weparty.commonui.ext.gone
 import com.adealink.weparty.commonui.ext.show
 import com.adealink.weparty.databinding.LayoutUserOnlineStatusBinding
 import com.adealink.weparty.module.profile.ProfileModule
-
-
-sealed class OnlineStatus(val uid: Long) {
-    data class OnLine(val uidData: Long) : OnlineStatus(uidData)
-    data class OffLine(val uidData: Long) : OnlineStatus(uidData)
-}
+import com.adealink.weparty.module.profile.data.UserOnlineInfo
 
 
 interface IUserOnlineStatusView {
     /**
      * 设置在线状态
      */
-    fun setOnlineStatus(status: OnlineStatus)
+    fun setOnlineStatus(userId: Long, info: UserOnlineInfo)
 }
 
 /**
@@ -35,27 +33,59 @@ class UserOnlineStatusView @JvmOverloads constructor(
 
     val binding = LayoutUserOnlineStatusBinding.inflate(LayoutInflater.from(context), this, true)
 
-    override fun setOnlineStatus(status: OnlineStatus) {
-        if (status.uid == ProfileModule.getMyUid()) {
+    companion object {
+        const val USER_STATUS_ONLINE = 1 // 在线状态
+        const val USER_STATUS_OFFLINE = 0 // 离线状态
+    }
+
+    override fun setOnlineStatus(userId: Long, info: UserOnlineInfo) {
+        if (userId == ProfileModule.getMyUid()) {
             binding.root.gone()
             return
         }
-        when (status) {
-            is OnlineStatus.OnLine -> {
-                binding.root.show()
-                binding.root.setBackgroundResource(R.drawable.profile_user_online_bg)
-                binding.icon.setImageResource(R.drawable.profile_user_online_ic)
-                binding.text.text = getCompatString(R.string.profile_user_online)
-                binding.text.setTextColor(getCompatColor(R.color.color_FF00D956))
-            }
-
-            is OnlineStatus.OffLine -> {
-                binding.root.show()
-                binding.root.setBackgroundResource(R.drawable.profile_user_offline_bg)
-                binding.icon.setImageResource(R.drawable.profile_user_offline_ic)
-                binding.text.text = getCompatString(R.string.profile_user_offline)
-                binding.text.setTextColor(getCompatColor(R.color.color_AAAAAA))
-            }
+        val status = info.onlineStatus
+
+        if (status == USER_STATUS_ONLINE) {
+            binding.root.show()
+            binding.root.setBackgroundResource(R.drawable.profile_user_online_bg)
+            binding.icon.setImageResource(R.drawable.profile_user_online_ic)
+            binding.text.text = getCompatString(R.string.profile_user_online)
+            binding.text.setTextColor(getCompatColor(R.color.color_FF00D956))
+            return
+        }
+
+        val formatText = formatLastOnlineTime(info.lastOnlineTs)
+        if (formatText.isNotEmpty()) {
+            binding.root.show()
+            binding.root.setBackgroundResource(R.drawable.profile_user_offline_bg)
+            binding.icon.setImageResource(R.drawable.profile_user_offline_ic)
+            binding.text.setTextColor(getCompatColor(R.color.color_AAAAAA))
+            // 离线时间计算
+            binding.text.text = formatText
+        } else {
+            binding.root.gone()
+        }
+    }
+
+    private fun formatLastOnlineTime(
+        lastOnlineTime: Long? = null,
+        now: Long = System.currentTimeMillis()
+    ): String {
+        if (lastOnlineTime == null) {
+            return ""
+        }
+
+        val diffMillis = now - lastOnlineTime
+        val diffMinutes = diffMillis / (ONE_MINUTE)
+        val diffHours = diffMillis / (ONE_HOUR)
+        val diffDays = diffMillis / (ONE_DAY)
+
+        return when {
+            diffDays >= 30 -> getCompatString(R.string.time_month_ago)
+            diffDays >= 1 -> getCompatString(R.string.time_days_ago, diffDays)
+            diffHours >= 1 -> getCompatString(R.string.time_hours_ago, diffHours)
+            diffMinutes >= 10 -> getCompatString(R.string.time_minutes_ago, diffMinutes)
+            else -> getCompatString(R.string.time_just_online)
         }
     }
 

+ 3 - 0
app/src/main/java/com/adealink/weparty/module/profile/viewmodel/IProfileViewModel.kt

@@ -13,6 +13,7 @@ import com.adealink.weparty.module.profile.data.RemarkAndDescRes
 import com.adealink.weparty.module.profile.data.UserConfigType
 import com.adealink.weparty.module.profile.data.UserInfo
 import com.adealink.weparty.module.profile.data.UserInfo.Companion.NECESSARY_USER_ATTR_SET
+import com.adealink.weparty.module.profile.data.UserOnlineInfo
 import com.adealink.weparty.module.profile.data.UserPrivilegeInfo
 
 interface IProfileViewModel {
@@ -86,4 +87,6 @@ interface IProfileViewModel {
     ): LiveData<Rlt<Any>>
 
     fun getUsersOnlineInRoomStatus(uids: Set<Long>): LiveData<Map<Long, OnlineInRoomStatus>>
+
+    fun getUserOnlineInfo(uids: Set<Long>): LiveData<Map<Long, UserOnlineInfo>>
 }

+ 2 - 0
app/src/main/java/com/adealink/weparty/module/room/IRoomService.kt

@@ -158,4 +158,6 @@ interface IRoomService : IService<IRoomService>, IMediaOperatorGet {
     fun isInRoomGamePage(): Boolean
 
     fun getRoomGamePlayerMicStatus(): List<UserMicStatus>
+
+    suspend fun loadRecentRoomIdList(): List<Long>
 }

+ 8 - 0
app/src/main/java/com/adealink/weparty/module/room/RoomModule.kt

@@ -292,6 +292,10 @@ object RoomModule : BaseDynamicModule<IRoomService>(IRoomService::class), IRoomS
         return getService().getMediaOperator()
     }
 
+    override suspend fun loadRecentRoomIdList(): List<Long> {
+        return getService().loadRecentRoomIdList()
+    }
+
     override fun emptyService(): IRoomService {
         return object : IRoomService {
             override fun init() {}
@@ -569,6 +573,10 @@ object RoomModule : BaseDynamicModule<IRoomService>(IRoomService::class), IRoomS
                 return emptyList()
             }
 
+            override suspend fun loadRecentRoomIdList(): List<Long> {
+                return emptyList()
+            }
+
         }
     }
 

+ 2 - 0
app/src/main/java/com/adealink/weparty/module/room/Router.kt

@@ -53,6 +53,8 @@ interface Room {
             const val EXTRA_MIC_GRAB_MAX_UN_PREPARE_SECOND =
                 "extra_mic_grab_max_un_prepare_time" //麦上用户非准备状态下的超时时间,超过该时间用户会自动踢下麦
             const val EXTRA_MIC_GRAB_GAME_INFO = "extra_mic_grab_game_info" //游戏信息
+
+            const val EXTRA_ENTER_ROOM_WITH_ACTION = "action" //进房后的操作
         }
 
     }

+ 274 - 0
app/src/main/java/com/adealink/weparty/module/task/CommonActivityRewardDialog.kt

@@ -0,0 +1,274 @@
+package com.adealink.weparty.module.task
+
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
+import android.os.Bundle
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import android.view.Window
+import android.view.WindowManager
+import androidx.core.net.toUri
+import androidx.core.view.updateLayoutParams
+import androidx.lifecycle.lifecycleScope
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.adealink.frame.base.Rlt
+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.frame.util.onClick
+import com.adealink.weparty.App
+import com.adealink.weparty.R
+import com.adealink.weparty.commonui.dialogfragment.BaseDialogFragment
+import com.adealink.weparty.commonui.ext.dp
+import com.adealink.weparty.commonui.ext.gone
+import com.adealink.weparty.commonui.ext.onSuccess
+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.commonui.recycleview.adapter.multitype.MultiTypeAdapter
+import com.adealink.weparty.commonui.recycleview.itemdecoration.HorizontalItemDecoration
+import com.adealink.weparty.databinding.ItemTaskRewardDialogBinding
+import com.adealink.weparty.databinding.LayoutActivityRewardDialogBinding
+import com.adealink.weparty.module.anchor.AnchorModule
+import com.adealink.weparty.module.anchor.data.AnchorMessageButton
+import com.adealink.weparty.module.anchor.data.AnchorMsgBtnAction
+import com.adealink.weparty.module.anchor.data.CommonActivityRewardMessage
+import com.adealink.weparty.module.operation.Operation
+import com.adealink.weparty.module.operation.data.RewardDetailData
+import com.adealink.weparty.module.room.Room
+import com.adealink.weparty.module.room.RoomModule
+import com.adealink.weparty.stat.constant.Page
+import com.adealink.weparty.stat.reportEnterPage
+import com.adealink.weparty.util.goLocalLinkPage
+import kotlinx.coroutines.launch
+import okhttp3.MediaType.Companion.toMediaTypeOrNull
+import okhttp3.RequestBody.Companion.toRequestBody
+
+/**
+ * 通用活动奖励弹窗
+ * Created by XiaoDongLin.
+ * Date: 2025/3/21
+ */
+@RouterUri(path = [Operation.CommonActivityRewardDialog.PATH], desc = "通用活动奖励弹窗")
+class CommonActivityRewardDialog : BaseDialogFragment(R.layout.layout_activity_reward_dialog) {
+
+    @BindExtra(Operation.CommonActivityRewardDialog.EXTRA_DIALOG_DATA)
+    var rewardMessage: CommonActivityRewardMessage? = null
+
+    private val binding by viewBinding(LayoutActivityRewardDialogBinding::bind)
+
+    private val taskHttpService by lazy {
+        App.instance.networkService.getHttpService(UserTaskHttpService::class.java)
+    }
+
+    override fun resetWindowAttributes(window: Window) {
+        super.resetWindowAttributes(window)
+        window.setLayout(
+            WindowManager.LayoutParams.MATCH_PARENT,
+            WindowManager.LayoutParams.MATCH_PARENT
+        )
+        window.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
+        val attr = window.attributes
+        attr.gravity = Gravity.CENTER
+        attr.dimAmount = 0.7f
+        window.attributes = attr
+    }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        Router.bind(this)
+    }
+
+
+    override fun initViews() {
+        super.initViews()
+        binding.btnClose.onClick {
+            dismiss()
+        }
+
+        val firstButton = rewardMessage?.buttons?.getOrNull(0)
+        if (firstButton != null) {
+            binding.btnFirst.show()
+            binding.btnFirst.text = firstButton.buttonText
+            binding.btnFirst.onClick {
+                onMsgButtonClick(firstButton)
+            }
+        } else {
+            binding.btnFirst.gone()
+        }
+
+        val secondButton = rewardMessage?.buttons?.getOrNull(1)
+        if (secondButton != null) {
+            binding.btnSecond.show()
+            binding.btnSecond.text = secondButton.buttonText
+            binding.btnSecond.onClick {
+                onMsgButtonClick(secondButton)
+            }
+        } else {
+            binding.btnSecond.gone()
+        }
+
+        binding.tvRewardMessage.text = rewardMessage?.text
+        binding.tvTitle.text = rewardMessage?.title
+
+    }
+
+    private fun onMsgButtonClick(messageButton: AnchorMessageButton) {
+        when (AnchorMsgBtnAction.map(messageButton.actionType)) {
+            AnchorMsgBtnAction.Deeplink -> {
+                val activity = AppUtil.currentActivity ?: return
+                val deeplink = messageButton.buttonActionUrl
+                var path: String? = null
+                if (deeplink.isEmpty().not()) {
+                    path = deeplink.toUri().path
+                }
+                var canDispatch = false
+                if (path != null) {
+                    canDispatch = Router.getClazz(path) != null
+                }
+                if (canDispatch) {
+                    goLocalLinkPage(activity, messageButton.buttonActionUrl)
+                }
+            }
+
+            AnchorMsgBtnAction.HttpPost -> {
+                val body =
+                    messageButton.buttonActionBody?.toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
+                AnchorModule.msgBtnHttpReq(url = messageButton.buttonActionUrl, body = body)
+            }
+
+            AnchorMsgBtnAction.Logout -> {
+            }
+
+            AnchorMsgBtnAction.ExitRoom -> {
+            }
+
+            AnchorMsgBtnAction.GO_ROOM_AND_OPEN_GAME -> {
+                lifecycleScope.launch {
+                    val recentRoomList = RoomModule.loadRecentRoomIdList()
+                    if (recentRoomList.isNotEmpty()) {
+                        val roomId = recentRoomList.first()
+                        Router.build(requireActivity(), Room.Room.PATH)
+                            .putExtra(Room.Room.EXTRA_ENTER_ROOM_ID, roomId.toString())
+                            .putExtra(Room.Room.EXTRA_ENTER_ROOM_WITH_ACTION, "playCenterOperate")
+                            .start()
+                        dismiss()
+                        return@launch
+                    }
+                    val roomIdRtl = RoomModule.getRandomHotRoomId()
+                    if (roomIdRtl is Rlt.Success) {
+                        val roomId = roomIdRtl.data
+                        Router.build(requireActivity(), Room.Room.PATH)
+                            .putExtra(Room.Room.EXTRA_ENTER_ROOM_ID, roomId.toString())
+                            .putExtra(Room.Room.EXTRA_ENTER_ROOM_WITH_ACTION, "playCenterOperate")
+                            .start()
+                        dismiss()
+                        return@launch
+                    }
+                    dismiss()
+                }
+            }
+
+            AnchorMsgBtnAction.NO -> {}
+            else -> {}
+        }
+
+    }
+
+
+    override fun onResume() {
+        super.onResume()
+        reportEnterPage(Page.ACTIVITY_REWARD_DIALOG)
+    }
+
+    override fun loadData() {
+        super.loadData()
+
+        val packageId = rewardMessage?.rewardPackageId
+        if (packageId != null) {
+            lifecycleScope.launch {
+                val rlt = taskHttpService.getRewardPackageInfoByIds(listOf(packageId))
+                rlt.onSuccess { data ->
+                    val rewardList = data.data?.get(packageId) ?: return@launch
+                    if (rewardList.size == 1) {
+                        binding.oneRewardLayout.show()
+                        binding.rewardList.gone()
+                        val rewardInfo = rewardList.first()
+                        binding.ivOneReward.setImageUrl(rewardInfo.rewardResourceUrl)
+                        binding.tvOneReward.text = rewardInfo.getCountDesc()
+
+                    } else {
+                        binding.oneRewardLayout.gone()
+                        binding.rewardList.show()
+
+                        val listAdapter = MultiTypeAdapter()
+                        val itemWidth = if (rewardList.size == 2) {
+                            84.dp()
+                        } else {
+                            74.dp()
+                        }
+                        val itemMargin = if (rewardList.size == 2) {
+                            30
+                        } else {
+                            4
+                        }
+
+                        listAdapter.register(ActivityItemRewardViewBinder(itemWidth))
+                        binding.rewardList.apply {
+                            adapter = listAdapter
+                            addItemDecoration(
+                                HorizontalItemDecoration(
+                                    itemMargin.toFloat(), 0f, 0f
+                                )
+                            )
+                            layoutManager =
+                                LinearLayoutManager(
+                                    requireContext(),
+                                    LinearLayoutManager.HORIZONTAL,
+                                    false
+                                )
+                        }
+
+                        listAdapter.items = rewardList
+                        listAdapter.notifyDataSetChanged()
+
+                    }
+                }
+            }
+        }
+    }
+
+}
+
+
+class ActivityItemRewardViewBinder(val itemWidth: Int) :
+    ItemViewBinder<RewardDetailData, BindingViewHolder<ItemTaskRewardDialogBinding>>() {
+    override fun onCreateViewHolder(
+        inflater: LayoutInflater,
+        parent: ViewGroup
+    ): BindingViewHolder<ItemTaskRewardDialogBinding> {
+        return BindingViewHolder(
+            ItemTaskRewardDialogBinding.inflate(
+                inflater,
+                parent,
+                false
+            )
+        ).apply {
+            itemView.updateLayoutParams {
+                width = itemWidth
+            }
+        }
+    }
+
+    override fun onBindViewHolder(
+        holder: BindingViewHolder<ItemTaskRewardDialogBinding>,
+        item: RewardDetailData
+    ) {
+        holder.binding.ivOneReward.setImageUrl(item.rewardResourceUrl)
+        holder.binding.tvOneReward.text = item.getCountDesc()
+    }
+
+}

+ 64 - 0
app/src/main/java/com/adealink/weparty/module/task/CommonActivityRewardDialogTask.kt

@@ -0,0 +1,64 @@
+package com.adealink.weparty.module.task
+
+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.anchor.AnchorModule
+import com.adealink.weparty.module.anchor.data.AnchorMessageReplyCode
+import com.adealink.weparty.module.anchor.data.CommonActivityRewardMessage
+import com.adealink.weparty.module.operation.Operation
+
+/**
+ * Created by XiaoDongLin.
+ * Date: 2025/7/10
+ */
+class CommonActivityRewardDialogTask(
+    private val messageId: Long,
+    private val messageType: Int,
+    private val message: CommonActivityRewardMessage
+) : BaseDialogTask<BaseDialogData.CommonActivityDialogData>() {
+    override val priority: Int
+        get() = Priority.NORMAL.priority
+    override val tag: String
+        get() = "CommonActivityRewardDialogTask"
+
+    override suspend fun canShow(): Rlt<BaseDialogData.CommonActivityDialogData> {
+        return Rlt.Success(
+            BaseDialogData.CommonActivityDialogData(
+                messageId,
+                messageType,
+                message
+            )
+        )
+    }
+
+    override fun showDialog(
+        dialogData: BaseDialogData.CommonActivityDialogData,
+        fragmentActivity: FragmentActivity
+    ): Int {
+        val anchorViewModel = AnchorModule.getAnchorViewModel(fragmentActivity)
+        anchorViewModel?.replyAnchorMessage(
+            dialogData.messageId,
+            dialogData.messageType,
+            AnchorMessageReplyCode.REPLY_READ
+        )
+        val fragment =
+            Router.getRouterInstance<BaseDialogFragment>(Operation.CommonActivityRewardDialog.PATH)
+                ?.apply {
+                    arguments = Bundle().apply {
+                        putParcelable(
+                            Operation.AnchorDiamondRewardDialog.EXTRA_DIALOG_DATA,
+                            message
+                        )
+                    }
+                }
+        fragment?.show(fragmentActivity.supportFragmentManager)
+        return fragment.hashCode()
+
+    }
+}

+ 1 - 1
app/src/main/java/com/adealink/weparty/module/task/Data.kt

@@ -155,7 +155,7 @@ data class CommonTaskCompleteRewardNotify(
     @SerializedName("userDimension") val userDimension: String, //任务关联的用户数据维度
 
     @GsonNullable
-    @SerializedName("rewardItems") val rewardItems: List<RewardDetailData> //奖励信息
+    @SerializedName("rewardItems") val rewardItems: List<RewardDetailData>, //奖励信息
 ) : Parcelable
 
 

+ 5 - 0
app/src/main/java/com/adealink/weparty/module/task/UserTaskHttpService.kt

@@ -2,6 +2,8 @@ package com.adealink.weparty.module.task
 
 import com.adealink.frame.base.Rlt
 import com.adealink.frame.network.data.Res
+import com.adealink.weparty.module.level.data.RewardItem
+import com.adealink.weparty.module.operation.data.RewardDetailData
 import retrofit2.http.Body
 import retrofit2.http.GET
 import retrofit2.http.POST
@@ -31,6 +33,9 @@ interface UserTaskHttpService {
     @POST("activity/reportUserAction")
     suspend fun reportUserAction(@Body req: UserTaskActionReq): Rlt<Res<Any>>
 
+
+    @GET("activity/getRewardPackageInfoByIds")
+    suspend fun getRewardPackageInfoByIds(@Query("packageIds") packageIds: List<Long>): Rlt<Res<Map<Long, List<RewardDetailData>>>>
 }
 
 

+ 25 - 2
app/src/main/java/com/adealink/weparty/module/wallet/data/WalletData.kt

@@ -4,14 +4,14 @@ import android.os.Parcelable
 import com.adealink.frame.util.PackageUtil
 import com.adealink.weparty.module.operation.data.ProductTypeConstant
 import com.google.gson.annotations.GsonNullable
-import com.google.gson.annotations.JsonAdapter
 import com.google.gson.annotations.SerializedName
 import kotlinx.parcelize.Parcelize
 
 enum class Currency(val value: Byte) {
     Coin(0),
     Diamond(1),
-    GameCoins(10);
+    GameCoins(10),
+    LuckyCoins(20); // 代币记录
 
     companion object {
 
@@ -555,4 +555,27 @@ data class MerchantDiamondAgencyConfig(
     @GsonNullable
     @SerializedName("mySupportedCurrencies")
     val mySupportedCurrencies: String? = null,
+)
+
+/**
+ * LuckyCoins 变更类型
+ */
+enum class LuckyCoinsRecordType(val value: Int) {
+    Reduce(0), ADD(1);
+
+    //类资源 类似于static但特性更多
+    companion object {
+        fun map(type: Int): LuckyCoinsRecordType {
+            return LuckyCoinsRecordType.values().firstOrNull { it.value == type }
+                ?: throw IllegalArgumentException("Unknown LuckyCoinsRecordType value: $type")
+        }
+    }
+}
+
+data class UserChargeNotify(
+    @SerializedName("uid") val uid: Long,
+    @SerializedName("orderId") val orderId: String,
+    @SerializedName("currencyType") val currencyType: Int, // 货币类型
+    @SerializedName("chargeAmount") val chargeAmount: Long, // 充值金额
+    @SerializedName("chargeTs") val chargeTs: Long, // 充值时间戳
 )

+ 10 - 0
app/src/main/java/com/adealink/weparty/module/wallet/viewmodel/IWalletViewModel.kt

@@ -31,6 +31,7 @@ interface IWalletViewModel {
     val diamondEntranceLd: LiveData<DiamondEntrance>
     val exchangeRateLd: LiveData<Rlt<Long>>
     val minDiamondLd: LiveData<Rlt<Long>>
+    val luckyCoinsList: LiveData<Rlt<List<UserCurrencyRecordInfo>>>
 
     fun getWalletBanner()
 
@@ -81,4 +82,13 @@ interface IWalletViewModel {
     fun getMerchantDiamondAgencyConfig(merchantId: Long)
 
     fun updateDiamondAgentRecord(orderId: String, orderStatus: Int, url: String?): LiveData<Rlt<Any>>
+
+    /**
+     *  获取幸运币记录,只支持拉取最近1个月的数据记录
+     */
+    fun getLuckyCoinsList(currentType: Int, limit: Int)
+
+    fun getUserChargeNotify(): LiveData<Rlt<List<UserChargeNotify>>>
+
+    fun addGlobalConfigListener()
 }

+ 1 - 0
app/src/main/java/com/adealink/weparty/stat/constant/Page.kt

@@ -39,5 +39,6 @@ enum class Page(override val value: String, override val desc: String) : IEventV
     TASK_REWARD_DIALOG("task_reward_notify", "任务奖励通知"),
     COMPLETE_PROFILE_TASK_DIALOG("complete_profile_task_notify", "完善资料任务弹窗"),
     WELCOME_TASK_DIALOG("welcome_task_notify", "欢迎任务弹窗"),
+    ACTIVITY_REWARD_DIALOG("activity_reward_notify", "活动奖励通知"),
 
 }

+ 7 - 8
app/src/main/java/com/adealink/weparty/ui/MainStartUpFragment.kt

@@ -2,11 +2,7 @@ package com.adealink.weparty.ui
 
 import android.os.Bundle
 import android.os.SystemClock
-import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.withResumed
 import com.adealink.frame.base.fastLazy
-import com.adealink.frame.ext.isViewBindingValid
-import com.adealink.frame.ext.launchIfViewLifecycleInitialized
 import com.adealink.frame.log.Log
 import com.adealink.frame.push.manager.pushService
 import com.adealink.frame.startup.executor.TaskExecutorManager
@@ -17,6 +13,7 @@ import com.adealink.frame.util.runOnUiThread
 import com.adealink.weparty.BuildConfig
 import com.adealink.weparty.commonui.BaseActivity
 import com.adealink.weparty.commonui.BaseFragment
+import com.adealink.weparty.commonui.ext.isUiValid
 import com.adealink.weparty.config.globalConfigManager
 import com.adealink.weparty.log.manager.logManager
 import com.adealink.weparty.module.account.AccountModule
@@ -92,7 +89,7 @@ class MainStartUpFragment : BaseFragment() {
         RoomModule.init()
         GameModule.checkPlayingGame()
 
-        if (isViewBindingValid()) {
+        if (activity?.isUiValid()==true) {
             roomAttrViewModel?.getMyRoomInfo(true)
             countryViewModel?.getCountryList(true)
             walletViewModel?.checkThirdPayOpen()
@@ -116,6 +113,8 @@ class MainStartUpFragment : BaseFragment() {
         WalletModule.init()
         FamilyModule.pullFamilyLevelConfig()
         ProfileModule.pullUserNoteNameData()
+        walletViewModel?.addGlobalConfigListener()
+        walletViewModel?.getUserChargeNotify()
         globalConfigManager.getAllGlobalConfig(true)
     }
 
@@ -140,13 +139,13 @@ class MainStartUpFragment : BaseFragment() {
         RoomTaskMonitor.init()
         OperationModule.init()
         OperationModule.checkSuperSupporterWhatsAppFillStatus()
-        if (isViewBindingValid()) {
+        if (activity?.isUiValid()==true) {
             familyInfoViewModel?.getApplyJoinFamilyUnHandleNum()
         }
     }
 
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
+    override fun onActivityCreated(savedInstanceState: Bundle?) {
+        super.onActivityCreated(savedInstanceState)
         val startTs = SystemClock.elapsedRealtime()
         Log.i(TAG, "StartUpFragment.onCreate() start")
         runOnUiThread(importantTask)

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

@@ -34,6 +34,7 @@ import com.adealink.weparty.module.anchor.constant.TAG_ANCHOR
 import com.adealink.weparty.module.anchor.data.AnchorMessage
 import com.adealink.weparty.module.anchor.data.AnchorMessageReplyCode
 import com.adealink.weparty.module.anchor.data.AnchorMessageTemplateBody
+import com.adealink.weparty.module.anchor.data.AnchorMessageTemplateType
 import com.adealink.weparty.module.anchor.data.AnchorMessageType
 import com.adealink.weparty.module.level.data.TAG_LEVEL
 import com.adealink.weparty.module.medal.MedalModule
@@ -45,6 +46,7 @@ import com.adealink.weparty.module.operation.OperationModule
 import com.adealink.weparty.module.operation.newuser.HomeBannerEntranceFloatViewComp
 import com.adealink.weparty.module.profile.Profile
 import com.adealink.weparty.module.room.Room
+import com.adealink.weparty.module.task.CommonActivityRewardDialogTask
 import com.adealink.weparty.module.task.DailySignInComp
 import com.adealink.weparty.module.task.HomeIncomeViewComp
 import com.adealink.weparty.module.task.HomeTaskCountDownViewComp
@@ -250,7 +252,38 @@ abstract class BaseHomeFragment : BaseFragment, ITabManager {
                 DialogTaskManager.submit(task)
             }
 
+            AnchorMessageType.COMMON_ACTIVITY_REWARD_NOTIFY->{
+                val messageBody = message.commonActivityRewardMessage
+                if (messageBody == null) {
+                    Log.e(TAG_ANCHOR, "消息体解析失败")
+                    return
+                }
+                val task = CommonActivityRewardDialogTask(
+                    message.messageId,
+                    message.messageType,
+                    messageBody
+                )
+                // 提交任务到 DialogTaskManager
+                DialogTaskManager.submit(task)
+            }
+
+
             else -> {
+                // 是否是模版消息(非Non)
+                val templateType = AnchorMessageTemplateType.map(message.templateMessage)
+                // 是否是模版消息(非Non)
+                val isTemplateMessage = templateType != null && templateType != AnchorMessageTemplateType.Non
+                if (isTemplateMessage){
+                    AnchorModule.handleAnchorTemplateMessage(message)
+                    return
+                }
+
+                //对于一个当前版本不支持的消息,这边收到也干脆改成已读
+                anchorViewModel?.replyAnchorMessage(
+                    message.messageId,
+                    message.messageType,
+                    AnchorMessageReplyCode.REPLY_READ
+                )
                 Log.d(TAG_LEVEL, "handleAnchorMessage: $message")
             }
         }

+ 12 - 1
app/src/main/java/com/adealink/weparty/webview/datasource/local/WebLocalService.kt

@@ -4,8 +4,8 @@ import android.content.Context
 import com.adealink.frame.log.Log
 import com.adealink.frame.storage.sp.TypeDelegationPrefs
 import com.adealink.frame.util.AppUtil
-import com.adealink.weparty.webview.constant.TAG_WEB_VIEW
 import com.adealink.weparty.module.profile.ProfileModule
+import com.adealink.weparty.webview.constant.TAG_WEB_VIEW
 
 /**
  * Created by sunxiaodong on 2022/7/20.
@@ -19,6 +19,17 @@ object WebLocalService : TypeDelegationPrefs(
     }
 ) {
 
+    //幸运币弹窗控制:告诉h5是否需要弹出幸运币弹窗
+    const val KEY_NEED_TO_SHOW_LUCKY_DIALOG = "key_need_to_show_lucky_dialog"
+    var needToShowLuckyDialog: String by PrefUserKey(KEY_NEED_TO_SHOW_LUCKY_DIALOG, "false")
+
+    //上次幸运币更新的时间,单位为毫秒
+    var lastUpdateTime: Long by PrefUserKey("key_last_get_lucky_dialog_time", 0L)
+
+    //代币功能开关
+    var luckCoinRegionEnable: String by PrefUserKey("key_luck_coin_region_enable", "false")
+
+
     var webVersion: String by PrefKey("key_web_version", "")
 
     var offlineH5UrlsJson: String by PrefKey("key_offline_urls_json", "")

+ 42 - 0
app/src/main/java/com/adealink/weparty/webview/jsbridge/method/KVStorageGetJSNativeMethod.kt

@@ -0,0 +1,42 @@
+package com.adealink.weparty.webview.jsnativemethod
+
+import com.adealink.weparty.module.profile.ProfileModule
+import com.adealink.weparty.webview.datasource.local.WebLocalService
+import com.adealink.weparty.webview.jsbridge.callback.JSBridgeCallback
+import com.adealink.weparty.webview.jsbridge.method.JSNativeMethod
+import com.google.gson.annotations.SerializedName
+
+/**
+ * Created by XiaoDongLin.
+ * Date: 2025/6/9
+ */
+
+data class KVStorageGetReq(
+    @SerializedName("key") val key: String,
+    @SerializedName("defaultValue") val defaultValue: String? = null,
+    @SerializedName("scope") val scope: String = "user" //sp数据维度
+)
+
+enum class DataScope(val value: String) {
+    USER("user"),   // 用户维度(uid)
+    DEVICE("device");  // 设备维度(deviceId)
+
+    companion object {
+        fun map(value: String): DataScope {
+            return values().firstOrNull { it.value == value } ?: USER
+        }
+    }
+}
+
+
+class KVStorageGetJSNativeMethod : JSNativeMethod<KVStorageGetReq, String> {
+    override val methodName: String = "KVStorageGet"
+    override fun handleMethodCall(req: KVStorageGetReq, callback: JSBridgeCallback<String>?) {
+        val scope = DataScope.map(req.scope)
+        val key = req.key + if (scope == DataScope.USER) ProfileModule.getMyUid().toString() else ""
+        val defaultValue = req.defaultValue ?: ""
+        val sp = WebLocalService.prefs.invoke()
+        val result = sp.getString(key, defaultValue) ?: defaultValue
+        callback?.resolve(result)
+    }
+}

+ 33 - 0
app/src/main/java/com/adealink/weparty/webview/jsbridge/method/KVStorageSetJSNativeMethod.kt

@@ -0,0 +1,33 @@
+package com.adealink.weparty.webview.jsbridge.method
+
+import com.adealink.weparty.module.profile.ProfileModule
+import com.adealink.weparty.webview.datasource.local.WebLocalService
+import com.adealink.weparty.webview.jsbridge.callback.JSBridgeCallback
+import com.adealink.weparty.webview.jsnativemethod.DataScope
+import com.google.gson.annotations.SerializedName
+
+/**
+ * Created by XiaoDongLin.
+ * Date: 2025/6/9
+ */
+
+data class KVStorageSetReq(
+    @SerializedName("key") val key: String,
+    @SerializedName("value") val value: String? = null,
+    @SerializedName("scope") val scope: String = "user" //sp数据维度
+)
+
+
+class KVStorageSetJSNativeMethod : JSNativeMethod<KVStorageSetReq, String> {
+    override val methodName: String = "KVStorageSet"
+
+    override fun handleMethodCall(req: KVStorageSetReq, callback: JSBridgeCallback<String>?) {
+        val scope = DataScope.map(req.scope)
+        val key = req.key + if (scope == DataScope.USER) ProfileModule.getMyUid().toString() else ""
+        val value = req.value ?: ""
+        val sp = WebLocalService.prefs.invoke()
+        sp.edit()
+            .putString(key, value)
+            .apply()
+    }
+}

+ 166 - 0
app/src/main/res/layout/layout_activity_reward_dialog.xml

@@ -0,0 +1,166 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:background="#80000000">
+
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/dialog_content"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginHorizontal="46dp"
+        android:layout_marginTop="41dp"
+        android:background="@drawable/bg_task_reward_dialog"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintTop_toTopOf="parent">
+
+
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:id="@+id/one_reward_layout"
+            android:layout_width="165dp"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="9dp"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent">
+
+            <com.opensource.svgaplayer.WenextSvgaView
+                android:id="@+id/svga_one_reward"
+                android:layout_width="165dp"
+                android:layout_height="165dp"
+                app:autoPlay="true"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="parent"
+                app:source="common_task_reward_dialog.svga" />
+
+            <com.adealink.frame.image.view.NetworkImageView
+                android:id="@+id/iv_one_reward"
+                android:layout_width="120dp"
+                android:layout_height="120dp"
+                app:layout_constraintBottom_toBottomOf="@+id/svga_one_reward"
+                app:layout_constraintEnd_toEndOf="@+id/svga_one_reward"
+                app:layout_constraintStart_toStartOf="@+id/svga_one_reward"
+                app:layout_constraintTop_toTopOf="@+id/svga_one_reward" />
+
+            <androidx.appcompat.widget.AppCompatTextView
+                android:id="@+id/tv_one_reward"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="-5dp"
+                android:text="x1000"
+                android:textColor="@color/color_FFFF8700"
+                android:textSize="18sp"
+                app:layout_constraintEnd_toEndOf="@+id/svga_one_reward"
+                app:layout_constraintStart_toStartOf="@+id/svga_one_reward"
+                app:layout_constraintTop_toBottomOf="@+id/svga_one_reward" />
+
+        </androidx.constraintlayout.widget.ConstraintLayout>
+
+
+        <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/reward_list"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:minHeight="100dp"
+            android:layout_marginTop="30dp"
+            app:layout_constrainedWidth="true"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent" />
+
+
+        <androidx.constraintlayout.widget.Barrier
+            android:id="@+id/barrier"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            app:barrierDirection="bottom"
+            app:constraint_referenced_ids="reward_list,one_reward_layout" />
+
+        <androidx.appcompat.widget.AppCompatTextView
+            android:id="@+id/tv_reward_message"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginHorizontal="16dp"
+            android:layout_marginTop="8dp"
+            android:gravity="center"
+            android:lineSpacingMultiplier="1.3"
+            android:textColor="@color/color_FFFF8700"
+            android:textSize="12sp"
+            app:layout_constraintTop_toBottomOf="@+id/barrier"
+            tools:text="Complete the Create a Chat Room quest and get a diamond reward" />
+
+
+        <androidx.appcompat.widget.AppCompatTextView
+            android:id="@+id/btn_first"
+            android:layout_width="160dp"
+            android:layout_height="40dp"
+            android:layout_marginTop="16dp"
+            android:background="@drawable/common_ff8700_radius_25_bg"
+            android:gravity="center"
+            android:textColor="@color/white"
+            android:textSize="16sp"
+            android:textStyle="bold"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/tv_reward_message"
+            tools:text="@string/task_reward_i_know" />
+
+        <androidx.appcompat.widget.AppCompatTextView
+            android:id="@+id/btn_second"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="12dp"
+            android:layout_marginBottom="20dp"
+            android:drawableEnd="@drawable/bg_task_reward_right_arrow"
+            android:textColor="@color/color_FF777777"
+            android:textSize="12sp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/btn_first"
+            tools:text="@string/task_reward_more_task" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+
+    <androidx.appcompat.widget.AppCompatImageView
+        android:id="@+id/iv_task_reward_title"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="-42dp"
+        android:src="@drawable/bg_task_reward_title"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="@+id/dialog_content" />
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/tv_title"
+        android:layout_width="200dp"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="7dp"
+        android:ellipsize="end"
+        android:gravity="center"
+        android:maxLines="1"
+        android:text="@string/common_congratulations"
+        android:textColor="@color/white"
+        android:textSize="18sp"
+        android:textStyle="bold"
+        app:layout_constraintEnd_toEndOf="@+id/iv_task_reward_title"
+        app:layout_constraintStart_toStartOf="@+id/iv_task_reward_title"
+        app:layout_constraintTop_toTopOf="@+id/iv_task_reward_title" />
+
+    <androidx.appcompat.widget.AppCompatImageView
+        android:id="@+id/btn_close"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="26dp"
+        android:src="@drawable/ic_task_reward_close"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="@+id/iv_task_reward_title" />
+
+
+</androidx.constraintlayout.widget.ConstraintLayout>

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

@@ -677,4 +677,9 @@
    <string name="profile_sid">المعرف الأمني</string>
    <string name="reward_resource_name_moment_bg">بطاقة خلفية اللحظة</string>
    <string name="module_certification">إصدار الشهادات</string>
+   <string name="time_just_online">متصل الآن</string>
+   <string name="time_minutes_ago">منذ %1$s دقيقة</string>
+   <string name="time_hours_ago">منذ %1$s ساعة</string>
+   <string name="time_days_ago">منذ %1$s يومًا</string>
+   <string name="time_month_ago">منذ شهر</string>
 </resources>

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

@@ -675,4 +675,9 @@
    <string name="reward_resource_name_moment_bg">Moment 背景卡</string>
    <string name="user_task_today_earnings">今日 收益</string>
    <string name="module_certification">认证</string>
+   <string name="time_just_online">刚刚在线</string>
+   <string name="time_minutes_ago">%1$s分钟前在线</string>
+   <string name="time_hours_ago">%1$s小时前在线</string>
+   <string name="time_days_ago">%1$s天前在线</string>
+   <string name="time_month_ago">2个月前在线</string>
 </resources>

+ 6 - 1
app/src/main/res/values/strings.xml

@@ -788,5 +788,10 @@
    <string name="common_version_too_old">Your app version is too low. Please update to continue</string>
    <string name="common_later">Later</string>
    <string name="common_set">Set</string>
-    <string name="common_confirm_message">Confirm %s?</string>
+   <string name="common_confirm_message">Confirm %s?</string>
+   <string name="time_just_online">Just online</string>
+   <string name="time_minutes_ago">%1$s minutes ago</string>
+   <string name="time_hours_ago">%1$s hours ago</string>
+   <string name="time_days_ago">%1$s days ago</string>
+   <string name="time_month_ago">1 month ago</string>
 </resources>

+ 5 - 0
module/anchor/src/main/java/com/adealink/weparty/anchor/AnchorServiceImpl.kt

@@ -12,6 +12,7 @@ import com.adealink.weparty.anchor.viewmodel.AnchorViewModelFactory
 import com.adealink.weparty.module.anchor.viewmodel.IAnchorViewModel
 import com.adealink.weparty.module.anchor.data.AnchorMessage
 import com.adealink.weparty.module.anchor.data.AnchorMessageReplyCode
+import okhttp3.RequestBody
 
 
 @RegisterService(IAnchorService::class)
@@ -37,6 +38,10 @@ class AnchorServiceImpl : IAnchorService {
         anchorManager.handleAnchorTemplateMessage(message)
     }
 
+    override fun msgBtnHttpReq(url: String, body: RequestBody?) {
+        anchorManager.msgBtnHttpReq(url, body)
+    }
+
     override fun getService(): IAnchorService {
         return this
     }

+ 29 - 0
module/anchor/src/main/java/com/adealink/weparty/anchor/manager/AnchorManager.kt

@@ -40,10 +40,12 @@ import com.adealink.weparty.module.operation.OperationModule
 import com.adealink.weparty.module.room.Room
 import com.adealink.weparty.module.room.RoomModule
 import com.adealink.weparty.util.goLocalLinkPage
+import com.adealink.weparty.module.task.CommonActivityRewardDialogTask
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 import okhttp3.MediaType.Companion.toMediaTypeOrNull
+import okhttp3.RequestBody
 import okhttp3.RequestBody.Companion.toRequestBody
 
 val anchorManager: IAnchorManager by lazy { AnchorManager() }
@@ -94,6 +96,7 @@ class AnchorManager : BaseFrame<IAnchorListener>(), IAnchorManager {
             val handle = messageType == AnchorMessageType.MESSAGR_DIAMOND_REWARD_NOTIFY
                     || messageType == AnchorMessageType.MESSAGE_IM_DAILY_GREETING_NOTIFY
                     || messageType == AnchorMessageType.MESSAGE_USER_CHANGE_GENDER
+                    || messageType == AnchorMessageType.COMMON_ACTIVITY_REWARD_NOTIFY
                     || isTemplateMessage
             if (handle) {
                 handleAnchorMessage(data, messageType, isTemplateMessage)
@@ -146,6 +149,21 @@ class AnchorManager : BaseFrame<IAnchorListener>(), IAnchorManager {
                 }
             }
 
+            AnchorMessageType.COMMON_ACTIVITY_REWARD_NOTIFY->{
+                val messageBody = data.commonActivityRewardMessage
+                if (messageBody == null) {
+                    Log.e(TAG_ANCHOR, "消息体解析失败")
+                    return
+                }
+                val task = CommonActivityRewardDialogTask(
+                    data.messageId,
+                    data.messageType,
+                    messageBody
+                )
+                // 提交任务到 DialogTaskManager
+                DialogTaskManager.submit(task)
+            }
+
             else -> {
                 if (isTemplateMessage) {
                     handleAnchorTemplateMessage(data)
@@ -347,4 +365,15 @@ class AnchorManager : BaseFrame<IAnchorListener>(), IAnchorManager {
             else -> {}
         }
     }
+
+    override fun msgBtnHttpReq(url: String, body: RequestBody?) {
+        launch {
+            val result = anchorHttpService.msgBtnHttpReq(url, body)
+            if (result is Rlt.Failed) {
+                Log.e(TAG_ANCHOR, "msgBtnHttpReq failed: ${result.error}")
+            } else {
+                Log.i(TAG_ANCHOR, "msgBtnHttpReq success")
+            }
+        }
+    }
 }

+ 2 - 0
module/anchor/src/main/java/com/adealink/weparty/anchor/manager/IAnchorManager.kt

@@ -6,6 +6,7 @@ import com.adealink.frame.network.data.Res
 import com.adealink.weparty.anchor.listener.IAnchorListener
 import com.adealink.weparty.module.anchor.data.AnchorMessage
 import com.adealink.weparty.module.anchor.data.AnchorMessageReplyCode
+import okhttp3.RequestBody
 
 interface IAnchorManager : IBaseFrame<IAnchorListener> {
     suspend fun getAnchorMessages(): Rlt<List<AnchorMessage>>
@@ -20,4 +21,5 @@ interface IAnchorManager : IBaseFrame<IAnchorListener> {
      * @message:AnchorMessage
      */
     fun handleAnchorTemplateMessage(message: AnchorMessage)
+    fun msgBtnHttpReq(url: String, body: RequestBody?)
 }

+ 10 - 9
module/certification/src/main/java/com/adealink/weparty/certification/activity/BaseLivenessActivity.kt

@@ -159,16 +159,17 @@ abstract class BaseLivenessActivity : BaseKeepScreenActivity(), OnLogsListener,
     }
 
     private fun initCamera(cameraID: Int) {
-        mCameraID = getCameraID(cameraID)
-        mFaceOrientation = getCameraOrigin(
-            this, mCameraID
-        )
-        mSenseCamera?.release()
-        this.mSenseCamera = SenseCamera.Builder(this)
-            .setCameraId(mCameraID)
-            .setRequestedPreviewSize(WIDTH, HEIGHT)
-            .build()
         try {
+            mCameraID = getCameraID(cameraID)
+            mFaceOrientation = getCameraOrigin(
+                this, mCameraID
+            )
+            mSenseCamera?.release()
+            this.mSenseCamera = SenseCamera.Builder(this)
+                .setCameraId(mCameraID)
+                .setRequestedPreviewSize(WIDTH, HEIGHT)
+                .build()
+
             mFaceFinderView?.startCamera(this.mSenseCamera)
             mSenseCamera?.setOnPreviewFrameCallback(this)
         } catch (e: Exception) {

+ 24 - 1
module/message/src/main/java/com/adealink/weparty/message/conversation/comp/ConversationTargetInfoComp.kt

@@ -17,6 +17,7 @@ import com.adealink.frame.oss.ossService
 import com.adealink.frame.router.Router
 import com.adealink.weparty.commonui.dialogfragment.BaseDialogFragment
 import com.adealink.weparty.commonui.ext.gone
+import com.adealink.weparty.commonui.ext.hide
 import com.adealink.weparty.commonui.ext.onSuccess
 import com.adealink.weparty.commonui.ext.show
 import com.adealink.weparty.effect.EffectViewModel
@@ -38,7 +39,6 @@ import com.adealink.weparty.module.couple.data.CoupleUserInfo
 import com.adealink.weparty.module.message.data.OFFICIAL_TARGET_ID
 import com.adealink.weparty.module.message.data.isSystemTarget
 import com.adealink.weparty.module.profile.ProfileModule
-import com.adealink.weparty.module.profile.data.NORMAL_USER
 import com.adealink.weparty.module.profile.data.UserInfo
 import kotlinx.coroutines.launch
 
@@ -48,6 +48,7 @@ class ConversationTargetInfoComp(
     private val effectContainerView: EffectView,
     private val callback: IConversationCompCallback
 ) : ViewComponent(lifecycleOwner) {
+    private val profileViewModel by fastLazy { ProfileModule.getProfileViewModel(requireActivity()) }
     private val effectViewModel by activityViewModels<EffectViewModel>()
     private val messageViewModel by activityViewModels<MessageViewModel>()
     private val coupleViewModel by fastLazy { CoupleModule.getCoupleViewModel(requireActivity()) }
@@ -64,6 +65,7 @@ class ConversationTargetInfoComp(
         super.onCreate()
         observerViewModels()
         checkIsSystem()
+        checkOnLine()
     }
 
     private fun observerViewModels() {
@@ -277,10 +279,31 @@ class ConversationTargetInfoComp(
         }
     }
 
+    private fun checkOnLine() {
+        profileViewModel?.getUserOnlineInfo(uids = setOf(callback.getTargetId()))
+            ?.observe(viewLifecycleOwner) { map ->
+                val onlineInfo = map[callback.getTargetId()]
+                if (onlineInfo == null) {
+                    binding.svgaInRoom.gone()
+                    binding.ivOnline.gone()
+                    return@observe
+                }
+                binding.ivOnline.show(isShow = onlineInfo.onlineStatus == 1)
+                val roomId = onlineInfo.chatRoom
+                if (roomId != 0L) {
+                    binding.svgaInRoom.show()
+                    binding.svgaInRoom.setAsset("im_user_in_room.svga")
+                } else {
+                    binding.svgaInRoom.hide()
+                }
+            }
+    }
+
     override fun onNewIntent(intent: Intent?) {
         super.onNewIntent(intent)
         inProgressUpgradeLevelSet.clear()
         checkIsSystem()
+        checkOnLine()
     }
 
     override fun onDestroy() {

+ 13 - 3
module/message/src/main/java/com/adealink/weparty/message/conversation/extension/longclick/MessageItemLongClickActionManager.kt

@@ -9,18 +9,18 @@ import com.adealink.frame.aab.util.getCompatString
 import com.adealink.frame.coroutine.dispatcher.Dispatcher
 import com.adealink.frame.data.json.toJsonErrorNull
 import com.adealink.frame.imkit.IMService
-import com.adealink.weparty.message.R
-import com.adealink.weparty.message.config.IMConfigCenter
 import com.adealink.frame.imkit.manager.AudioPlayManager
 import com.adealink.frame.imkit.model.State
 import com.adealink.frame.imkit.model.TAG_IM_UI
-import com.adealink.weparty.message.data.UiMessage
 import com.adealink.frame.imkit.resend.ResendManager
 import com.adealink.frame.log.Log
 import com.adealink.weparty.App
 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.config.IMConfigCenter
 import com.adealink.weparty.message.conversation.message.TransferDealMessage
+import com.adealink.weparty.message.data.UiMessage
 import com.adealink.weparty.message.datasource.remote.MessageHttpService
 import com.adealink.weparty.module.message.data.RecallMessageInfo
 import com.adealink.weparty.module.message.data.RecallReq
@@ -133,6 +133,16 @@ object MessageItemLongClickActionManager : CoroutineScope {
             MessageItemLongClickAction.Builder()
                 .titleResId(R.string.rc_dialog_item_message_delete)
                 .iconResId(R.drawable.im_message_delete_ic)
+                .showFilter(object : MessageItemLongClickAction.Filter {
+                    override fun filter(uiMessage: UiMessage): Boolean {
+                        val message = uiMessage.getMessage()
+                        if (message.conversationType  == Conversation.ConversationType.SYSTEM){
+                            //官号消息不可删除
+                            return false
+                        }
+                        return true
+                    }
+                })
                 .actionListener(
                     object : MessageItemLongClickAction.MessageItemLongClickListener {
                         override fun onMessageItemLongClick(

+ 25 - 0
module/message/src/main/res/layout/layout_conversation_top_bar_center_new.xml

@@ -116,9 +116,34 @@
             android:layout_width="28dp"
             android:layout_height="28dp"
             android:layout_marginTop="5.5dp"
+            android:layout_marginStart="2dp"
             app:layout_constraintStart_toStartOf="parent"
             app:layout_constraintTop_toTopOf="parent" />
 
+        <com.opensource.svgaplayer.WenextSvgaView
+            android:id="@+id/svga_in_room"
+            android:layout_width="32dp"
+            android:layout_height="32dp"
+            android:layout_gravity="center"
+            android:scaleType="fitCenter"
+            android:visibility="gone"
+            app:layout_constraintBottom_toBottomOf="@id/target_avatar"
+            app:layout_constraintEnd_toEndOf="@id/target_avatar"
+            app:layout_constraintStart_toStartOf="@id/target_avatar"
+            app:layout_constraintTop_toTopOf="@id/target_avatar"
+            tools:visibility="visible" />
+
+        <androidx.appcompat.widget.AppCompatImageView
+            android:id="@+id/iv_online"
+            android:layout_width="8dp"
+            android:layout_height="8dp"
+            android:layout_gravity="top|end"
+            android:background="@drawable/common_online_ic"
+            android:visibility="gone"
+            app:layout_constraintEnd_toEndOf="@id/target_avatar"
+            app:layout_constraintTop_toTopOf="@id/target_avatar"
+            tools:visibility="visible" />
+
         <com.adealink.weparty.commonui.imageview.AvatarView
             android:id="@+id/self_avatar"
             android:layout_width="28dp"

+ 1 - 1
module/operation/src/main/java/com/adealink/weparty/operation/rechargepackage/RechargeDailyFragment.kt

@@ -215,7 +215,7 @@ class RechargeDailyFragment : BottomDialogFragment(R.layout.fragment_recharge_da
                     Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
                 )
             }
-            append(getCompatString(APP_R.string.common_coins).replace("%s", "")) // 占位
+            append(getCompatString(APP_R.string.common_coin)) // 占位
         }
     }
 

+ 9 - 0
module/profile/src/main/java/com/adealink/weparty/profile/datasource/ProfileHttpService.kt

@@ -9,6 +9,8 @@ import com.adealink.weparty.module.profile.data.BatchGetRemarkReq
 import com.adealink.weparty.module.profile.data.BatchGetRemarkRes
 import com.adealink.weparty.module.profile.data.GetCountryConfigReq
 import com.adealink.weparty.module.profile.data.GetCountryConfigRes
+import com.adealink.weparty.module.profile.data.GetOnlineInfoReq
+import com.adealink.weparty.module.profile.data.GetOnlineInfoRes
 import com.adealink.weparty.module.profile.data.GetUserInRoomInfoRes
 import com.adealink.weparty.module.profile.data.GetUserInRoomRes
 import com.adealink.weparty.module.profile.data.GetUserInfosRes
@@ -176,4 +178,11 @@ interface ProfileHttpService {
      */
     @GET("user/remark/get")
     suspend fun getUserRemarkAndDesc(@Query("toUid") toUid: Long): Rlt<Res<RemarkAndDescRes>>
+
+    /**
+     * 获取用户在线信息
+     */
+    @POST("user/getOnlineInfo")
+    suspend fun getUserOnlineInfo(@Body req: GetOnlineInfoReq): Rlt<Res<GetOnlineInfoRes>>
+
 }

+ 3 - 0
module/profile/src/main/java/com/adealink/weparty/profile/manager/IProfileManager.kt

@@ -6,6 +6,7 @@ import com.adealink.weparty.module.profile.data.OnlineInRoomStatus
 import com.adealink.weparty.module.profile.data.RegionUserRes
 import com.adealink.weparty.module.profile.data.UserConfigType
 import com.adealink.weparty.module.profile.data.UserInfo
+import com.adealink.weparty.module.profile.data.UserOnlineInfo
 import com.adealink.weparty.module.profile.listener.IProfileListener
 import com.adealink.weparty.module.profile.tags.data.UserLabel
 import com.adealink.weparty.profile.job.JobInfo
@@ -78,4 +79,6 @@ interface IProfileManager : IBaseFrame<IProfileListener> {
     suspend fun getUsersOnlineInRoomStatus(uids: Set<Long>): Map<Long, OnlineInRoomStatus>
 
     fun checkChatAchievement()
+
+    suspend fun getUserOnlineInfo(uids: Set<Long>): Rlt<Map<Long, UserOnlineInfo>>
 }

+ 13 - 0
module/profile/src/main/java/com/adealink/weparty/profile/manager/ProfileManager.kt

@@ -28,6 +28,7 @@ import com.adealink.weparty.module.network.data.ServerCode
 import com.adealink.weparty.module.profile.ProfileModule
 import com.adealink.weparty.module.profile.data.AddRemarkAndDescReq
 import com.adealink.weparty.module.profile.data.BatchGetRemarkReq
+import com.adealink.weparty.module.profile.data.GetOnlineInfoReq
 import com.adealink.weparty.module.profile.data.OnlineInRoomStatus
 import com.adealink.weparty.module.profile.data.QUERY_NOTE_PAGE_SIZE
 import com.adealink.weparty.module.profile.data.RegionUserRes
@@ -37,6 +38,7 @@ import com.adealink.weparty.module.profile.data.UserCommonConfigInfo
 import com.adealink.weparty.module.profile.data.UserConfigType
 import com.adealink.weparty.module.profile.data.UserInfo
 import com.adealink.weparty.module.profile.data.UserInfo.Companion.NECESSARY_USER_ATTR_SET
+import com.adealink.weparty.module.profile.data.UserOnlineInfo
 import com.adealink.weparty.module.profile.dot.meChatAchievementDot
 import com.adealink.weparty.module.profile.listener.IProfileListener
 import com.adealink.weparty.module.profile.tags.data.UserLabel
@@ -736,4 +738,15 @@ class ProfileManager : BaseFrame<IProfileListener>(), IProfileManager {
             }
         }
     }
+
+    override suspend fun getUserOnlineInfo(uids: Set<Long>): Rlt<Map<Long, UserOnlineInfo>> {
+        val rlt = profileHttpService.getUserOnlineInfo(req = GetOnlineInfoReq(uids.toList()))
+        if (rlt is Rlt.Success) {
+            val data = rlt.data.data
+            if (data != null) {
+                return Rlt.Success(data.uid2OnlineInfoMap)
+            }
+        }
+        return Rlt.Failed(CommonDataNullError())
+    }
 }

+ 2 - 1
module/profile/src/main/java/com/adealink/weparty/profile/userprofile/NewUserProfileActivity.kt

@@ -123,7 +123,8 @@ class NewUserProfileActivity : BaseActivity() {
         ProfileUserBasisInfoComp(
             lifecycleOwner = this,
             binding = binding.profileBasicInfoLayout,
-            userUid = userUid
+            userUid = userUid,
+            inRoomLayout = binding.clInRoom
         ).attach()
         ProfileCoverViewComp(
             lifecycleOwner = this,

+ 24 - 4
module/profile/src/main/java/com/adealink/weparty/profile/userprofile/component/ProfileUserBasisInfoComp.kt

@@ -1,8 +1,14 @@
 package com.adealink.weparty.profile.userprofile.component
 
+import android.view.View
+import android.view.ViewGroup.MarginLayoutParams
+import androidx.core.view.updateLayoutParams
 import androidx.lifecycle.LifecycleOwner
+import com.adealink.frame.util.DisplayUtil
+import com.adealink.weparty.commonui.ext.dp
 import com.adealink.weparty.commonui.ext.mediumText
 import com.adealink.weparty.commonui.ext.setVisible
+import com.adealink.weparty.commonui.ext.show
 import com.adealink.weparty.module.profile.data.UserInfo
 import com.adealink.weparty.module.profile.util.updateCharmLevelLabel
 import com.adealink.weparty.module.profile.util.updateChatAchievementLabel
@@ -16,27 +22,41 @@ class ProfileUserBasisInfoComp(
     lifecycleOwner: LifecycleOwner,
     private val userUid: Long,
     private val binding: LayoutProfileBasicInfoBinding,
+    private val inRoomLayout: View,
 ) : BaseProfileViewComp(lifecycleOwner, userUid) {
 
     override fun initView() {
         binding.userNameTextView.mediumText()
+        inRoomLayout.updateLayoutParams<MarginLayoutParams> {
+            topMargin = DisplayUtil.getStatusBarHeight(requireActivity()) + 44.dp()
+        }
+        //写死gone,其他需求来做
+        inRoomLayout.show(isShow = false)
     }
 
     override fun observeViewModel() {
-
+        profileViewModel?.getUserOnlineInfo(uids = setOf(userUid))
+            ?.observe(viewLifecycleOwner) { map ->
+                val onlineInfo = map[userUid] ?: return@observe
+                binding.userOnlineStatusView.setOnlineStatus(userUid, onlineInfo)
+//                inRoomLayout.show(isShow = onlineInfo.chatRoom != 0L)
+            }
     }
 
     override fun updateUI(userInfo: UserInfo, fromCache: Boolean) {
-        binding.userNameTextView.setDisplayName(userUid, userInfo.name?:"")
+        binding.userNameTextView.setDisplayName(userUid, userInfo.name ?: "")
         binding.userIdView.setUserInfo(userInfo)
         binding.userCountryView.setUserInfo(userInfo)
         binding.userSexView.setSex(userInfo.gender, userInfo.birthday)
         binding.merchantLabelView.setMerchantType(userInfo.merchantType)
         binding.vipRechargeLabelView.updateVipRechargeLevel(userInfo.getVipRechargeLevel())
-        binding.userVoiceView.setVoiceData(userInfo.getUserVoiceIntroduction(), userInfo.voiceSec?:0)
+        binding.userVoiceView.setVoiceData(
+            userInfo.getUserVoiceIntroduction(),
+            userInfo.voiceSec ?: 0
+        )
         binding.officialLabelView.setVisible(userInfo.isOfficial())
         binding.userCertificationView.setCertificationStatus(userInfo.commonConfigInfo)
-        binding.userLevelView.updateLevel(userInfo.level?:0)
+        binding.userLevelView.updateLevel(userInfo.level ?: 0)
 
         updateCharmLevelLabel(
             userInfo.isFemale(),

+ 6 - 1
module/profile/src/main/java/com/adealink/weparty/profile/userprofile/fragment/PersonalFragment.kt

@@ -5,6 +5,7 @@ import androidx.fragment.app.activityViewModels
 import androidx.recyclerview.widget.LinearLayoutManager
 import com.adealink.frame.base.Rlt
 import com.adealink.frame.base.fastLazy
+import com.adealink.frame.ext.isViewBindingValid
 import com.adealink.frame.mvvm.view.viewBinding
 import com.adealink.frame.router.Router
 import com.adealink.frame.router.annotation.BindExtra
@@ -94,7 +95,11 @@ class PersonalFragment : BaseFragment(R.layout.fragment_personal), IUserGoodItem
     override fun observeViewModel() {
         super.observeViewModel()
         profileViewModel.personalTabListDataLD.observe(viewLifecycleOwner) { list ->
-            listAdapter.submitList(list)
+            listAdapter.submitList(list){
+                if (isViewBindingValid()){
+                    binding.recyclerView.scrollToPosition(0)
+                }
+            }
         }
     }
 

+ 23 - 1
module/profile/src/main/java/com/adealink/weparty/profile/viewmodel/ProfileViewModel.kt

@@ -37,6 +37,7 @@ import com.adealink.weparty.module.profile.data.RegionUserRes
 import com.adealink.weparty.module.profile.data.RemarkAndDescRes
 import com.adealink.weparty.module.profile.data.UserConfigType
 import com.adealink.weparty.module.profile.data.UserInfo
+import com.adealink.weparty.module.profile.data.UserOnlineInfo
 import com.adealink.weparty.module.profile.data.UserPrivilegeInfo
 import com.adealink.weparty.module.profile.listener.IProfileListener
 import com.adealink.weparty.module.profile.viewmodel.IProfileViewModel
@@ -615,7 +616,12 @@ class ProfileViewModel : BaseViewModel(), IProfileViewModel, IProfileListener,
 
             if (userInfoRlt is Rlt.Success) {
                 val userInfo = userInfoRlt.data
-                personTabListData.add(ProfilePrivacyAlbumData(userInfo.getPrivacyAlbumList(), privacyAlbumOpenRlt))
+                personTabListData.add(
+                    ProfilePrivacyAlbumData(
+                        userInfo.getPrivacyAlbumList(),
+                        privacyAlbumOpenRlt
+                    )
+                )
                 personTabListData.add(ProfileAboutMeData(userInfo))
             }
 
@@ -816,4 +822,20 @@ class ProfileViewModel : BaseViewModel(), IProfileViewModel, IProfileListener,
     override fun lowQualityAvatarNotify() {
         lowQualityNotifyLD.send(Unit)
     }
+
+    override fun getUserOnlineInfo(uids: Set<Long>): LiveData<Map<Long, UserOnlineInfo>> {
+        val liveData = OnceMutableLiveData<Map<Long, UserOnlineInfo>>()
+        viewModelScope.launch {
+            when (val rlt = profileManager.getUserOnlineInfo(uids)) {
+                is Rlt.Success -> {
+                    liveData.send(rlt.data)
+                }
+
+                is Rlt.Failed -> {
+                    liveData.send(emptyMap())
+                }
+            }
+        }
+        return liveData
+    }
 }

+ 3 - 3
module/profile/src/main/res/drawable-ldrtl/profile_in_room_white_bg.xml

@@ -1,11 +1,11 @@
 <?xml version="1.0" encoding="utf-8"?>
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
     android:shape="rectangle">
-    <solid android:color="@color/white" />
+    <solid android:color="#FF50AE" />
     <corners
         android:bottomLeftRadius="0dp"
-        android:bottomRightRadius="16dp"
+        android:bottomRightRadius="30dp"
         android:topLeftRadius="0dp"
-        android:topRightRadius="16dp" />
+        android:topRightRadius="30dp" />
 
 </shape>

+ 3 - 3
module/profile/src/main/res/drawable/profile_in_room_white_bg.xml

@@ -1,11 +1,11 @@
 <?xml version="1.0" encoding="utf-8"?>
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
     android:shape="rectangle">
-    <solid android:color="@color/white" />
+    <solid android:color="#FF50AE" />
     <corners
-        android:bottomLeftRadius="16dp"
+        android:bottomLeftRadius="30dp"
         android:bottomRightRadius="0dp"
-        android:topLeftRadius="16dp"
+        android:topLeftRadius="30dp"
         android:topRightRadius="0dp" />
 
 </shape>

+ 41 - 2
module/profile/src/main/res/layout/activity_user_profile_new.xml

@@ -133,9 +133,9 @@
             android:layout_marginTop="-2dp"
             android:layout_marginEnd="-7dp"
             android:visibility="gone"
-            tools:visibility="visible"
             app:layout_constraintEnd_toEndOf="@+id/btn_edit"
-            app:layout_constraintTop_toTopOf="@+id/btn_edit" />
+            app:layout_constraintTop_toTopOf="@+id/btn_edit"
+            tools:visibility="visible" />
 
     </androidx.constraintlayout.widget.ConstraintLayout>
 
@@ -155,5 +155,44 @@
         android:layout_width="match_parent"
         android:layout_height="match_parent" />
 
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/cl_in_room"
+        android:layout_width="wrap_content"
+        android:layout_height="32dp"
+        android:layout_gravity="top|end"
+        android:background="@drawable/profile_in_room_white_bg"
+        android:elevation="0dp"
+        android:paddingStart="16dp"
+        android:paddingEnd="16dp"
+        android:visibility="gone"
+        tools:layout_marginTop="80dp"
+        tools:visibility="visible">
+
+        <com.adealink.weparty.commonui.widget.MediumTextView
+            android:id="@+id/tv_in_room"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="4dp"
+            android:gravity="center"
+            android:includeFontPadding="false"
+            android:text="@string/profile_room"
+            android:textColor="@color/white"
+            android:textSize="14sp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toEndOf="@+id/iv_in_room_wave"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:visibility="visible" />
+
+        <com.adealink.frame.image.view.NetworkImageView
+            android:id="@+id/iv_in_room_wave"
+            android:layout_width="14dp"
+            android:layout_height="14dp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
 
 </androidx.coordinatorlayout.widget.CoordinatorLayout>

+ 4 - 0
module/room/src/main/java/com/adealink/weparty/room/RoomServiceImpl.kt

@@ -524,4 +524,8 @@ class RoomServiceImpl : IRoomService, IWPJoinListener, IWPSeatListener {
         return roomService.roomGameController.getPlayerMicStatus()
     }
 
+    override suspend fun loadRecentRoomIdList(): List<Long> {
+        return recentRoomListManager.loadRecentRoomIdList()
+    }
+
 }

+ 7 - 0
module/room/src/main/java/com/adealink/weparty/room/operate/RoomBottomOperateFragment.kt

@@ -222,6 +222,13 @@ open class RoomBottomOperateFragment : BaseFragment(R.layout.fragment_room_botto
         applyOnMicGuide.register(this)
         applyOnMicListGuide.register(this)
         applyOnMicWaitingGuide.register(this)
+
+        activity?.intent?.let {
+            val action = it.getStringExtra(Room.Room.EXTRA_ENTER_ROOM_WITH_ACTION)
+            if (action == "playCenterOperate") {
+                onItemClick(playCenterOperateItem)
+            }
+        }
     }
 
     override fun observeViewModel() {

+ 207 - 0
module/wallet/src/main/java/com/adealink/weparty/wallet/coin/CoinHistoryFragment.kt

@@ -0,0 +1,207 @@
+package com.adealink.weparty.wallet.coin
+
+import android.os.Bundle
+import androidx.fragment.app.viewModels
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.adealink.frame.aab.util.getCompatColor
+import com.adealink.frame.base.Rlt
+import com.adealink.frame.base.fastLazy
+import com.adealink.frame.mvvm.view.viewBinding
+import com.adealink.frame.util.timeToEnYM
+import com.adealink.weparty.commonui.BaseFragment
+import com.adealink.weparty.commonui.datepicker.CustomDatePicker
+import com.adealink.weparty.commonui.recycleview.adapter.MultiTypeListAdapter
+import com.adealink.weparty.commonui.recycleview.commonviewholder.CommonListEmptyItemData
+import com.adealink.weparty.commonui.recycleview.commonviewholder.CommonListEmptyViewBinder
+import com.adealink.weparty.commonui.recycleview.commonviewholder.CommonListErrorEmptyType
+import com.adealink.weparty.commonui.recycleview.diffutil.BaseListDiffUtil
+import com.adealink.weparty.wallet.R
+import com.adealink.weparty.wallet.coin.CoinHistoryListActivity.Companion.PAGE_SIZE
+import com.adealink.weparty.wallet.data.CurrencyCategory
+import com.adealink.weparty.wallet.data.CurrencyHistoryListItemData
+import com.adealink.weparty.wallet.data.CurrencySource
+import com.adealink.weparty.wallet.databinding.FragmentGoldCoinHistoryBinding
+import com.adealink.weparty.wallet.diamond.CategorySelectDialog
+import com.adealink.weparty.wallet.view.CurrencyHistoryListItemViewBinder
+import com.adealink.weparty.wallet.viewmodel.WalletViewModel
+import com.adealink.weparty.wallet.viewmodel.WalletViewModelFactory
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import java.util.Date
+import java.util.Locale
+
+/**
+ * Created by XiaoDongLin.
+ * Date: 2025/7/7
+ */
+class CoinHistoryFragment : BaseFragment(R.layout.fragment_gold_coin_history) {
+
+    private val binding by viewBinding(FragmentGoldCoinHistoryBinding::bind)
+
+    private val walletViewModel by viewModels<WalletViewModel> { WalletViewModelFactory() }
+
+    private val listAdapter by fastLazy { MultiTypeListAdapter(BaseListDiffUtil()) }
+    private val coinSource = arrayListOf(
+        CurrencySource(CurrencyCategory.ALL.value, CurrencyCategory.ALL.detail),
+        CurrencySource(CurrencyCategory.OTHER.value, CurrencyCategory.OTHER.detail),
+        CurrencySource(CurrencyCategory.COIN_CHAT.value, CurrencyCategory.COIN_CHAT.detail),
+        CurrencySource(
+            CurrencyCategory.COIN_SEND_GIFT.value,
+            CurrencyCategory.COIN_SEND_GIFT.detail
+        ),
+        CurrencySource(CurrencyCategory.COIN_SHOPPING.value, CurrencyCategory.COIN_SHOPPING.detail),
+        CurrencySource(CurrencyCategory.COIN_RECHARGE.value, CurrencyCategory.COIN_RECHARGE.detail),
+        CurrencySource(
+            CurrencyCategory.COIN_TASK_REWARD.value,
+            CurrencyCategory.COIN_TASK_REWARD.detail
+        ),
+        CurrencySource(CurrencyCategory.COIN_EVENT.value, CurrencyCategory.COIN_EVENT.detail),
+        CurrencySource(CurrencyCategory.COIN_EXCHANGE.value, CurrencyCategory.COIN_EXCHANGE.detail)
+    )
+
+    private var category: Int? = null
+    private var date = SimpleDateFormat("yyyyMM", Locale.ENGLISH).format(Date())
+    private var selectTime = System.currentTimeMillis()
+
+    private var currentPage = 0
+    private var isLoading = false
+    private var hasMore = true
+
+
+    override fun initViews() {
+        super.initViews()
+        binding.tvTime.text = timeToEnYM(System.currentTimeMillis())
+        binding.tvTime.setOnClickListener {
+            showDatePicker()
+        }
+        binding.tvFilter.text = CurrencyCategory.getCategoryById(CurrencyCategory.ALL.detail)
+        binding.tvFilter.setOnClickListener {
+            CategorySelectDialog().apply {
+                arguments = Bundle().apply {
+                    putParcelableArrayList(CategorySelectDialog.TYPE_LIST, coinSource)
+                }
+                selectCallback = {
+                    category = it.category
+                    coinSource.forEach { source ->
+                        source.isSelected = it.category == source.category
+                    }
+                    if (it.category == CurrencyCategory.ALL.value) {
+                        category = null
+                        binding.tvFilter.isSelected = false
+                        binding.tvFilter.setTextColor(getCompatColor(com.adealink.weparty.R.color.color_222222))
+                    } else {
+                        binding.tvFilter.isSelected = true
+                        binding.tvFilter.setTextColor(getCompatColor(com.adealink.weparty.R.color.color_FF9352FC))
+                    }
+                    binding.tvFilter.text = CurrencyCategory.getCategoryById(it.detail)
+                    resetAndReload()
+                }
+            }.show(childFragmentManager)
+        }
+        listAdapter.register(CurrencyHistoryListItemViewBinder(true))
+        listAdapter.register(CommonListEmptyViewBinder())
+        binding.refreshLayout.setOnRefreshListener {
+            resetAndReload()
+        }
+        binding.refreshLayout.setEnableLoadMore(true)
+        setupRecyclerView()
+    }
+
+    override fun loadData() {
+        super.loadData()
+        requestData()
+    }
+
+    private fun setupRecyclerView() {
+        binding.rvRecord.apply {
+            adapter = listAdapter
+            layoutManager = LinearLayoutManager(context)
+            addOnScrollListener(object : RecyclerView.OnScrollListener() {
+                override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
+                    super.onScrolled(recyclerView, dx, dy)
+                    val layoutManager = recyclerView.layoutManager as LinearLayoutManager
+                    val visibleItemCount = layoutManager.childCount
+                    val totalItemCount = layoutManager.itemCount
+                    val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
+
+                    if (!isLoading && hasMore && (visibleItemCount + firstVisibleItemPosition) >= totalItemCount) {
+                        requestData()
+                    }
+                }
+            })
+        }
+    }
+
+    private fun resetAndReload() {
+        currentPage = 0
+        hasMore = true
+        listAdapter.clear()
+        requestData()
+    }
+
+    private fun requestData() {
+        if (!hasMore || isLoading) return
+        isLoading = true
+        walletViewModel.getCoinHistoryList(date, currentPage, PAGE_SIZE, category)
+    }
+
+    override fun observeViewModel() {
+        super.observeViewModel()
+        walletViewModel.coinHistoryListLd.observe(this) {
+            isLoading = false
+            binding.refreshLayout.finishRefresh()
+            if (it is Rlt.Success) {
+                if (it.data.isEmpty()) {
+                    listAdapter.submitList(
+                        listOf(
+                            CommonListEmptyItemData(
+                                CommonListErrorEmptyType.NoData
+                            )
+                        )
+                    )
+                } else {
+                    val newItems = it.data.map { data ->
+                        CurrencyHistoryListItemData(data)
+                    }
+                    hasMore = newItems.size >= PAGE_SIZE
+                    if (hasMore) {
+                        currentPage++
+                    }
+                    val currentList =
+                        listAdapter.getCurrentList().toMutableList().apply { addAll(newItems) }
+                    listAdapter.submitList(currentList)
+                }
+            } else {
+                listAdapter.submitList(
+                    listOf(
+                        CommonListEmptyItemData(
+                            CommonListErrorEmptyType.NetError
+                        )
+                    )
+                )
+            }
+        }
+    }
+
+
+    private fun showDatePicker() {
+        val currentTime = System.currentTimeMillis()
+        val twoMonthAgo = Calendar.getInstance().apply {
+            add(Calendar.MONTH, -2)
+        }.timeInMillis
+        val dialog =
+            CustomDatePicker.newInstance(twoMonthAgo, currentTime, selectTime, false, false)
+        dialog.mCallback = object : CustomDatePicker.Callback {
+            override fun onTimeSelected(timestamp: Long) {
+                binding.tvTime.text = timeToEnYM(timestamp)
+                selectTime = timestamp
+                date = SimpleDateFormat("yyyyMM", Locale.ENGLISH).format(timestamp)
+                resetAndReload()
+            }
+        }
+        dialog.show(childFragmentManager)
+    }
+
+
+}

+ 43 - 165
module/wallet/src/main/java/com/adealink/weparty/wallet/coin/CoinHistoryListActivity.kt

@@ -1,206 +1,84 @@
 package com.adealink.weparty.wallet.coin
 
-import android.os.Bundle
-import androidx.activity.viewModels
-import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.fragment.app.Fragment
 import androidx.recyclerview.widget.RecyclerView
-import com.adealink.frame.aab.util.getCompatColor
-import com.adealink.frame.base.Rlt
-import com.adealink.frame.base.fastLazy
+import com.adealink.frame.aab.util.getCompatString
 import com.adealink.frame.mvvm.view.viewBinding
 import com.adealink.frame.router.annotation.RouterUri
-import com.adealink.frame.util.timeToEnYM
 import com.adealink.weparty.commonui.BaseActivity
-import com.adealink.weparty.commonui.datepicker.CustomDatePicker
-import com.adealink.weparty.commonui.recycleview.adapter.MultiTypeListAdapter
-import com.adealink.weparty.commonui.recycleview.commonviewholder.CommonListEmptyItemData
-import com.adealink.weparty.commonui.recycleview.commonviewholder.CommonListEmptyViewBinder
-import com.adealink.weparty.commonui.recycleview.commonviewholder.CommonListErrorEmptyType
-import com.adealink.weparty.commonui.recycleview.diffutil.BaseListDiffUtil
+import com.adealink.weparty.commonui.ext.setOverScrollModeToNever
+import com.adealink.weparty.commonui.recycleview.adapter.BaseActivityTabFragmentStateAdapter
+import com.adealink.weparty.commonui.viewpager.Tab
+import com.adealink.weparty.commonui.widget.EmptyFragment
 import com.adealink.weparty.module.wallet.Wallet
 import com.adealink.weparty.stat.constant.Page
 import com.adealink.weparty.stat.reportEnterPage
-import com.adealink.weparty.wallet.data.CurrencyCategory
-import com.adealink.weparty.wallet.data.CurrencyHistoryListItemData
-import com.adealink.weparty.wallet.data.CurrencySource
+import com.adealink.weparty.wallet.R
 import com.adealink.weparty.wallet.databinding.ActivityCoinHistoryBinding
-import com.adealink.weparty.wallet.diamond.CategorySelectDialog
-import com.adealink.weparty.wallet.view.CurrencyHistoryListItemViewBinder
-import com.adealink.weparty.wallet.viewmodel.WalletViewModel
-import com.adealink.weparty.wallet.viewmodel.WalletViewModelFactory
+import com.adealink.weparty.wallet.orderhistory.OrderHistoryActivity.HistoryPageAdapter
+import com.adealink.weparty.webview.datasource.local.WebLocalService
 import com.qmuiteam.qmui.widget.util.QMUIStatusBarHelper
-import java.text.SimpleDateFormat
-import java.util.Calendar
-import java.util.Date
-import java.util.Locale
-import com.adealink.weparty.R as APP_R
 
 /**
  * Created by PengWuLiang on 2025/3/12
  */
 @RouterUri(path = [Wallet.CoinHistory.PATH], desc = "金币流水")
-class CoinHistoryListActivity: BaseActivity() {
+class CoinHistoryListActivity : BaseActivity() {
 
     companion object {
         const val PAGE_SIZE = 20
     }
+
     private val binding by viewBinding(ActivityCoinHistoryBinding::inflate)
-    private val walletViewModel by viewModels<WalletViewModel> { WalletViewModelFactory() }
-    private val listAdapter by fastLazy { MultiTypeListAdapter(BaseListDiffUtil()) }
-    private val coinSource = arrayListOf(
-        CurrencySource(CurrencyCategory.ALL.value, CurrencyCategory.ALL.detail),
-        CurrencySource(CurrencyCategory.OTHER.value, CurrencyCategory.OTHER.detail),
-        CurrencySource(CurrencyCategory.COIN_CHAT.value, CurrencyCategory.COIN_CHAT.detail),
-        CurrencySource(CurrencyCategory.COIN_SEND_GIFT.value, CurrencyCategory.COIN_SEND_GIFT.detail),
-        CurrencySource(CurrencyCategory.COIN_SHOPPING.value, CurrencyCategory.COIN_SHOPPING.detail),
-        CurrencySource(CurrencyCategory.COIN_RECHARGE.value, CurrencyCategory.COIN_RECHARGE.detail),
-        CurrencySource(CurrencyCategory.COIN_TASK_REWARD.value, CurrencyCategory.COIN_TASK_REWARD.detail),
-        CurrencySource(CurrencyCategory.COIN_EVENT.value, CurrencyCategory.COIN_EVENT.detail),
-        CurrencySource(CurrencyCategory.COIN_EXCHANGE.value, CurrencyCategory.COIN_EXCHANGE.detail)
-    )
-
-    private var category: Int? = null
-    private var date = SimpleDateFormat("yyyyMM", Locale.ENGLISH).format(Date())
-    private var selectTime = System.currentTimeMillis()
-
-    private var currentPage = 0
-    private var isLoading = false
-    private var hasMore = true
+    private lateinit var historyPageAdapter: HistoryPageAdapter
+
 
     override fun initViews() {
         QMUIStatusBarHelper.setStatusBarLightMode(this)
         setContentView(binding.root)
-        binding.tvTime.text = timeToEnYM(System.currentTimeMillis())
-        binding.tvTime.setOnClickListener {
-            showDatePicker()
-        }
-        binding.tvFilter.text = CurrencyCategory.getCategoryById(CurrencyCategory.ALL.detail)
-        binding.tvFilter.setOnClickListener {
-            CategorySelectDialog().apply {
-                arguments = Bundle().apply {
-                    putParcelableArrayList(CategorySelectDialog.TYPE_LIST, coinSource)
-                }
-                selectCallback = {
-                    category = it.category
-                    coinSource.forEach { source ->
-                        source.isSelected = it.category == source.category
-                    }
-                    if(it.category == CurrencyCategory.ALL.value) {
-                        category = null
-                        binding.tvFilter.isSelected = false
-                        binding.tvFilter.setTextColor(getCompatColor(APP_R.color.color_222222))
-                    } else {
-                        binding.tvFilter.isSelected = true
-                        binding.tvFilter.setTextColor(getCompatColor(APP_R.color.color_FF9352FC))
-                    }
-                    binding.tvFilter.text = CurrencyCategory.getCategoryById(it.detail)
-                    resetAndReload()
-                }
-            }.show(supportFragmentManager)
-        }
-        listAdapter.register(CurrencyHistoryListItemViewBinder(true))
-        listAdapter.register(CommonListEmptyViewBinder())
-        binding.refreshLayout.setOnRefreshListener {
-            resetAndReload()
-        }
-        binding.refreshLayout.setEnableLoadMore(true)
-        setupRecyclerView()
-    }
 
-    override fun loadData() {
-        super.loadData()
-        requestData()
-    }
+        val tabList = mutableListOf<Tab>()
+        tabList.add(TAB_GOLD_COIN)
 
-    private fun setupRecyclerView() {
-        binding.rvRecord.apply {
-            adapter = listAdapter
-            layoutManager = LinearLayoutManager(context)
-            addOnScrollListener(object : RecyclerView.OnScrollListener() {
-                override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
-                    super.onScrolled(recyclerView, dx, dy)
-                    val layoutManager = recyclerView.layoutManager as LinearLayoutManager
-                    val visibleItemCount = layoutManager.childCount
-                    val totalItemCount = layoutManager.itemCount
-                    val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
-
-                    if (!isLoading && hasMore && (visibleItemCount + firstVisibleItemPosition) >= totalItemCount) {
-                        requestData()
-                    }
-                }
-            })
+        if (WebLocalService.luckCoinRegionEnable == "true") {
+            tabList.add(TAB_LUCKY_COINS)
         }
-    }
 
-    private fun resetAndReload() {
-        currentPage = 0
-        hasMore = true
-        listAdapter.clear()
-        requestData()
-    }
+        historyPageAdapter = HistoryPageAdapter(tabList)
+        binding.historyViewPage.adapter = historyPageAdapter
+        binding.historyViewPage.isSaveEnabled = false
+        (binding.historyViewPage.getChildAt(0) as? RecyclerView)?.setOverScrollModeToNever()
+        binding.commonTabLayout.createMediatorAndAttach(
+            binding.historyViewPage,
+            historyPageAdapter,
+            0
+        )
 
-    private fun requestData() {
-        if(!hasMore || isLoading) return
-        isLoading = true
-        walletViewModel.getCoinHistoryList(date, currentPage, PAGE_SIZE, category)
     }
 
-    override fun observeViewModel() {
-        super.observeViewModel()
-        walletViewModel.coinHistoryListLd.observe(this) {
-            isLoading = false
-            binding.refreshLayout.finishRefresh()
-            if (it is Rlt.Success) {
-                if (it.data.isEmpty()) {
-                    listAdapter.submitList(
-                        listOf(
-                            CommonListEmptyItemData(
-                                CommonListErrorEmptyType.NoData
-                            )
-                        )
-                    )
-                } else {
-                    val newItems = it.data.map { data ->
-                        CurrencyHistoryListItemData(data)
-                    }
-                    hasMore = newItems.size >= PAGE_SIZE
-                    if(hasMore) {
-                        currentPage++
-                    }
-                    val currentList = listAdapter.getCurrentList().toMutableList().apply { addAll(newItems) }
-                    listAdapter.submitList(currentList)
-                }
-            } else {
-                listAdapter.submitList(
-                    listOf(
-                        CommonListEmptyItemData(
-                            CommonListErrorEmptyType.NetError
-                        )
-                    )
-                )
-            }
+    internal inner class HistoryPageAdapter(val tabList: List<Tab>) :
+        BaseActivityTabFragmentStateAdapter(this) {
+
+        override fun getTabName(pos: Int): String {
+            return tabList.getOrNull(pos)?.let { getCompatString(it.titleResId) } ?: ""
         }
-    }
 
-    private fun showDatePicker() {
-        val currentTime = System.currentTimeMillis()
-        val twoMonthAgo = Calendar.getInstance().apply {
-            add(Calendar.MONTH, -2)
-        }.timeInMillis
-        val dialog = CustomDatePicker.newInstance(twoMonthAgo, currentTime, selectTime,false, false)
-        dialog.mCallback = object : CustomDatePicker.Callback {
-            override fun onTimeSelected(timestamp: Long) {
-                binding.tvTime.text = timeToEnYM(timestamp)
-                selectTime = timestamp
-                date = SimpleDateFormat("yyyyMM", Locale.ENGLISH).format(timestamp)
-                resetAndReload()
-            }
+
+        override fun getItemCount(): Int {
+            return tabList.size
+        }
+
+        override fun createFragment(position: Int): Fragment {
+            return tabList.getOrNull(position)?.instance?.invoke() ?: EmptyFragment()
         }
-        dialog.show(supportFragmentManager)
     }
 
     override fun onResume() {
         super.onResume()
         reportEnterPage(Page.COIN_HISTORY_LIST)
     }
-}
+}
+
+val TAB_GOLD_COIN = Tab(R.string.wallet_gold_coin) { CoinHistoryFragment() }
+
+val TAB_LUCKY_COINS = Tab(R.string.wallet_lucky_coins) { LuckyCoinsFragment() }

+ 104 - 0
module/wallet/src/main/java/com/adealink/weparty/wallet/coin/LuckyCoinsFragment.kt

@@ -0,0 +1,104 @@
+package com.adealink.weparty.wallet.coin
+
+import androidx.fragment.app.viewModels
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.adealink.frame.base.Rlt
+import com.adealink.frame.base.fastLazy
+import com.adealink.frame.log.Log
+import com.adealink.frame.mvvm.view.viewBinding
+import com.adealink.weparty.commonui.BaseFragment
+import com.adealink.weparty.commonui.ext.dp
+import com.adealink.weparty.commonui.recycleview.adapter.multitype.MultiTypeAdapter
+import com.adealink.weparty.commonui.recycleview.itemdecoration.VerticalSpaceItemDecoration
+import com.adealink.weparty.module.wallet.data.Currency
+import com.adealink.weparty.wallet.R
+import com.adealink.weparty.wallet.data.LuckyCoinsHistoryListData
+import com.adealink.weparty.wallet.data.OrderHistoryListData
+import com.adealink.weparty.wallet.data.OrderHistoryListErrorEmptyData
+import com.adealink.weparty.wallet.data.OrderHistoryListErrorEmptyType
+import com.adealink.weparty.wallet.databinding.FragmentLuckyCoinsHistoryBinding
+import com.adealink.weparty.wallet.view.LuckyCoinsItemViewBinder
+import com.adealink.weparty.wallet.view.OrderHistoryErrorEmptyViewBinder
+import com.adealink.weparty.wallet.viewmodel.WalletViewModel
+import com.adealink.weparty.wallet.viewmodel.WalletViewModelFactory
+
+/**
+ * Created by XiaoDongLin.
+ * Date: 2025/7/7
+ */
+class LuckyCoinsFragment : BaseFragment(R.layout.fragment_lucky_coins_history) {
+
+    private val binding by viewBinding(FragmentLuckyCoinsHistoryBinding::bind)
+    private val listAdapter by fastLazy { MultiTypeAdapter() }
+    private val walletViewMode by viewModels<WalletViewModel> { WalletViewModelFactory() }
+
+    // 只支持拉取最近1个月最多100条数据记录
+    private var limit: Int = 100
+
+    // 初始化视图
+    override fun initViews() {
+        super.initViews()
+        listAdapter.register(LuckyCoinsItemViewBinder()) // 数据源
+        listAdapter.register(OrderHistoryErrorEmptyViewBinder()) // Empty&&Error
+
+        val rvLinearLayoutManager: LinearLayoutManager = LinearLayoutManager(context)
+        binding.rvLuckyCoins.apply {
+            adapter = listAdapter
+            layoutManager = rvLinearLayoutManager
+            addItemDecoration(
+                VerticalSpaceItemDecoration(
+                    8.dp(),
+                    40.dp()
+                )
+            )
+        }
+
+        // SRL
+        binding.refreshLayout.setEnableLoadMore(false)
+        binding.refreshLayout.setOnRefreshListener { getLuckyCoinsList() }
+    }
+
+    // 加载数据
+    override fun loadData() {
+        super.loadData()
+        getLuckyCoinsList()
+    }
+
+    // VM监听
+    override fun observeViewModel() {
+        super.observeViewModel()
+        walletViewMode.luckyCoinsList.observe(viewLifecycleOwner) {
+            binding.refreshLayout.finishRefresh()
+            when (it) {
+                is Rlt.Success -> {
+                    Log.d("OrderHistoryActivity", it.data.size.toString())
+                    if (it.data.isNullOrEmpty()) {
+                        showErrorEmptyView(OrderHistoryListErrorEmptyType.ListEmpty)
+                        return@observe
+                    }
+                    val itemsData = mutableListOf<OrderHistoryListData>()
+                    it.data.forEach { item ->
+                        itemsData.add(LuckyCoinsHistoryListData(item))
+                    }
+                    listAdapter.items = itemsData
+                    listAdapter.notifyDataSetChanged()
+                }
+
+                else -> {
+                    showErrorEmptyView(OrderHistoryListErrorEmptyType.NetError)
+                }
+            }
+        }
+    }
+
+    private fun getLuckyCoinsList() {
+        walletViewMode.getLuckyCoinsList(Currency.LuckyCoins.value.toInt(), limit)
+    }
+
+    private fun showErrorEmptyView(emptyType: OrderHistoryListErrorEmptyType = OrderHistoryListErrorEmptyType.ListEmpty) {
+        listAdapter.items = listOf(OrderHistoryListErrorEmptyData(emptyType))
+        listAdapter.notifyDataSetChanged()
+    }
+
+
+}

+ 2 - 0
module/wallet/src/main/java/com/adealink/weparty/wallet/data/RechargeHistoryListData.kt

@@ -16,6 +16,8 @@ data class ConsumptionHistoryListItemData(val consumeRecordInfo: UserCoinConsume
 
 data class GameCoinsConsumptionHistoryListItemData(val gameCoinsRecordInfo: UserCurrencyRecordInfo): OrderHistoryListData()
 
+data class LuckyCoinsHistoryListData(val luckyCoinsInfo: UserCurrencyRecordInfo): OrderHistoryListData()
+
 enum class OrderHistoryListErrorEmptyType {
     NetError,
     ListEmpty

+ 13 - 0
module/wallet/src/main/java/com/adealink/weparty/wallet/datasource/remote/WalletHttpService.kt

@@ -22,6 +22,7 @@ import com.adealink.weparty.module.wallet.data.PayerMaxOrderStatus
 import com.adealink.weparty.module.wallet.data.PayerMaxTypeInfo
 import com.adealink.weparty.module.wallet.data.SellDiamondReq
 import com.adealink.weparty.module.wallet.data.UpdateDiamondAgentRecordReq
+import com.adealink.weparty.module.wallet.data.UserChargeNotify
 import com.adealink.weparty.module.wallet.data.UserCoinConsumeRecordInfo
 import com.adealink.weparty.module.wallet.data.UserCurrencyRecordInfo
 import com.adealink.weparty.module.wallet.data.UserDiamondRecordInfo
@@ -150,4 +151,16 @@ interface WalletHttpService {
     suspend fun updateDiamondAgentRecord(
         @Body req: UpdateDiamondAgentRecordReq,
     ): Rlt<Res<Any>>
+
+    /**
+     * 获取幸运币记录,只支持拉取最近1个月的数据记录
+     */
+    @GET("currency/getCurrencyRecordList")
+    suspend fun getCurrencyRecordList(@Query("currencyType") currencyType: Int,@Query("limit") limit:Int): Rlt<Res<List<UserCurrencyRecordInfo>>>
+
+    @GET("currency/getUserChargeNotify")
+    suspend fun getUserChargeNotify(
+        @Query("uid") uid: Long,
+        @Query("currencyTypes") currencyTypes: List<Int>
+    ): Rlt<Res<List<UserChargeNotify>>>
 }

+ 4 - 0
module/wallet/src/main/java/com/adealink/weparty/wallet/listener/IWalletListener.kt

@@ -2,6 +2,7 @@ package com.adealink.weparty.wallet.listener
 
 import com.adealink.frame.frame.IListener
 import com.adealink.weparty.module.wallet.data.Currency
+import com.adealink.weparty.module.wallet.data.UserChargeNotify
 
 /**
  * Created by sunxiaodong on 2021/6/2.
@@ -11,4 +12,7 @@ interface IWalletListener : IListener {
     fun onCurrencyChanged(changed: Map<Currency, Long>)
 
     fun onCurrencyTodayChanged(changed: Map<Currency, Long>)
+
+    //用户发生充值事件发送
+    fun onUserCoinChargeChanged(data: UserChargeNotify)
 }

+ 10 - 0
module/wallet/src/main/java/com/adealink/weparty/wallet/manager/IWalletManager.kt

@@ -71,4 +71,14 @@ interface IWalletManager : IBaseFrame<IWalletListener> {
     suspend fun getMerchantDiamondAgencyConfig(merchantId: Long): Rlt<MerchantDiamondAgencyConfig>
 
     suspend fun updateDiamondAgentRecord(orderId: String, orderStatus: Int, url: String?): Rlt<Res<Any>>
+
+    /**
+     * 获取货币账户变更记录,只支持拉取最近1个月最多100条数据记录
+     */
+    suspend fun getLuckyCoinsList(currentType: Int, limit: Int):Rlt<Res<List<UserCurrencyRecordInfo>>>
+
+    suspend fun getUserChargeNotify(
+        currencyTypes: List<Int>,
+        uid: Long
+    ): Rlt<List<UserChargeNotify>>
 }

+ 38 - 0
module/wallet/src/main/java/com/adealink/weparty/wallet/manager/WalletManager.kt

@@ -59,6 +59,7 @@ import com.adealink.weparty.wallet.view.floatview.PurchaseCoinFloatData
 import com.adealink.weparty.wallet.view.floatview.PurchaseCoinFloatView
 import com.adealink.weparty.commonui.widget.floatview.FloatViewFactory
 import com.adealink.weparty.commonui.widget.floatview.data.MODE_APPLICATION
+import com.adealink.weparty.module.wallet.data.UserChargeNotify
 import kotlinx.coroutines.launch
 import java.util.LinkedList
 import java.util.concurrent.ConcurrentHashMap
@@ -113,11 +114,25 @@ class WalletManager : BaseFrame<IWalletListener>(), IWalletManager, OnNetworkLis
         }
     }
 
+    //用户发生充值事件发送
+    private val userChargeNotify =object : ISocketNotify<UserChargeNotify> {
+        override val uri: String ="URI_COIN_CHARGE_NOTIFY"
+
+        override fun needHandle(data: UserChargeNotify?): Boolean {
+            return data != null && data.uid == ProfileModule.getMyUid()
+        }
+        override fun onNotify(data: UserChargeNotify) {
+            dispatch {
+                it.onUserCoinChargeChanged(data)
+            }
+        }
+    }
 
     init {
         registerNetworkListener(this)
         App.instance.networkService.subscribeNotify(purchaseCoinNotify)
         App.instance.networkService.subscribeNotify(currencyChangeNotify)
+        App.instance.networkService.subscribeNotify(userChargeNotify)
         globalConfigManager.addListener(GlobalConfigType.GLOBAL_RECHARGE_PACKAGE, this)
         globalConfigManager.getConfig(GlobalConfigType.GLOBAL_RECHARGE_PACKAGE)?.let {
             onConfigGet(GlobalConfigType.GLOBAL_RECHARGE_PACKAGE, it)
@@ -429,4 +444,27 @@ class WalletManager : BaseFrame<IWalletListener>(), IWalletManager, OnNetworkLis
     ): Rlt<Res<Any>> {
         return walletHttpService.updateDiamondAgentRecord(UpdateDiamondAgentRecordReq(orderId, orderStatus, url))
     }
+
+    override suspend fun getLuckyCoinsList(
+        currentType: Int,
+        limit: Int
+    ): Rlt<Res<List<UserCurrencyRecordInfo>>> {
+        return walletHttpService.getCurrencyRecordList(currentType,limit)
+    }
+
+    override suspend fun getUserChargeNotify(
+        currencyTypes: List<Int>,
+        uid: Long
+    ): Rlt<List<UserChargeNotify>> {
+        val rlt = walletHttpService.getUserChargeNotify(uid = uid, currencyTypes = currencyTypes)
+        return when (rlt) {
+            is Rlt.Success -> if (rlt.data.data != null) {
+                Rlt.Success(rlt.data.data!!)
+            } else {
+                Rlt.Failed(CommonDataNullError())
+            }
+
+            is Rlt.Failed -> rlt
+        }
+    }
 }

+ 75 - 0
module/wallet/src/main/java/com/adealink/weparty/wallet/util/WalletUIUtil.kt

@@ -4,6 +4,8 @@ import android.text.SpannableStringBuilder
 import android.text.Spanned
 import android.text.style.ImageSpan
 import android.view.View
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
 import androidx.appcompat.widget.AppCompatTextView
 import com.adealink.frame.aab.util.getCompatDrawable
 import com.adealink.frame.aab.util.getCompatString
@@ -40,6 +42,10 @@ fun setBalanceChangedText(
             balanceChangeResId = R.string.wallet_game_coins_balance
             balanceIconImgRes =  APP_R.drawable.common_game_coin_56_ic
         }
+        Currency.LuckyCoins -> {
+            balanceChangeResId = R.string.wallet_lucky_coins_balance
+            balanceIconImgRes = R.drawable.wallet_lucky_coin_ic
+        }
     }
     val balanceChangeDescSb =
         SpannableStringBuilder(getCompatString(balanceChangeResId, fromBalance, toBalance))
@@ -55,4 +61,73 @@ fun setBalanceChangedText(
         balanceChangeDescSb.length,
         Spanned.SPAN_INCLUSIVE_INCLUSIVE)
     textView.text = balanceChangeDescSb
+}
+
+
+fun setLuckyCoinsBalanceChangeText(
+    textView: AppCompatTextView,
+    fromBalance: Long,
+    toBalance: Long
+) {
+    setGenericBalanceText(
+        textView,
+        R.string.wallet_lucky_coins_balance,
+        R.drawable.wallet_lucky_coin_ic,
+        fromBalance,
+        toBalance
+    )
+}
+
+/**
+ * 变更金额组合
+ */
+fun setGenericBalanceText(
+    textView: AppCompatTextView,
+    @StringRes balanceChangeResId: Int,
+    @DrawableRes balanceIconImgRes: Int,
+    fromBalance: Long,
+    toBalance: Long
+) {
+    setGenericBalanceText(
+        textView,
+        balanceChangeResId,
+        balanceIconImgRes,
+        fromBalance.toString(),
+        toBalance.toString()
+    )
+}
+
+fun setGenericBalanceText(
+    textView: AppCompatTextView,
+    @StringRes balanceChangeResId: Int,
+    @DrawableRes balanceIconImgRes: Int,
+    fromBalance: String,
+    toBalance: String
+) {
+    if (fromBalance.isEmpty() || toBalance.isEmpty()) {
+        textView.visibility = View.GONE
+        return
+    }
+    /* 没有涉及计算操作 转string写业务即可
+    if (fromBalance <= 0 && toBalance <= 0) {
+        textView.visibility = View.GONE
+        return
+    }
+    */
+    textView.visibility = View.VISIBLE
+    val balanceChangeDescSb =
+        SpannableStringBuilder(getCompatString(balanceChangeResId, fromBalance, toBalance))
+    val balanceIconPlaceHolder = "[icon]"
+    balanceChangeDescSb.append(balanceIconPlaceHolder)
+    val iconPlaceHolderIndex = balanceChangeDescSb.indexOf(balanceIconPlaceHolder)
+    val iconDrawable = getCompatDrawable(balanceIconImgRes)
+    iconDrawable.setBounds(0, 0, DisplayUtil.dp2px(10f), DisplayUtil.dp2px(10f))
+    val imageSpan = CenterImageSpan(iconDrawable)
+    balanceChangeDescSb.safeSetSpan(
+        imageSpan,
+        iconPlaceHolderIndex,
+        balanceChangeDescSb.length,
+        Spanned.SPAN_INCLUSIVE_INCLUSIVE
+    )
+    textView.text = balanceChangeDescSb
 }

+ 61 - 0
module/wallet/src/main/java/com/adealink/weparty/wallet/view/LuckyCoinsItemViewBinder.kt

@@ -0,0 +1,61 @@
+package com.adealink.weparty.wallet.view
+
+import android.annotation.SuppressLint
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import com.adealink.frame.util.timeToEnYMDHMS
+import com.adealink.weparty.commonui.recycleview.adapter.BindingViewHolder
+import com.adealink.weparty.commonui.recycleview.adapter.multitype.ItemViewBinder
+import com.adealink.weparty.module.wallet.data.LuckyCoinsRecordType
+import com.adealink.weparty.wallet.data.LuckyCoinsHistoryListData
+import com.adealink.weparty.wallet.databinding.LayoutLuckyCoinsItemBinding
+import com.adealink.weparty.wallet.util.setLuckyCoinsBalanceChangeText
+
+/**
+ * Created by XiaoDongLin.
+ * Date: 2025/7/7
+ */
+class LuckyCoinsItemViewBinder :
+    ItemViewBinder<LuckyCoinsHistoryListData, BindingViewHolder<LayoutLuckyCoinsItemBinding>>() {
+
+    override fun onCreateViewHolder(
+        inflater: LayoutInflater,
+        parent: ViewGroup
+    ): BindingViewHolder<LayoutLuckyCoinsItemBinding> {
+        return BindingViewHolder(LayoutLuckyCoinsItemBinding.inflate(inflater, parent, false))
+    }
+
+    @SuppressLint("SetTextI18n")
+    override fun onBindViewHolder(
+        holder: BindingViewHolder<LayoutLuckyCoinsItemBinding>,
+        item: LuckyCoinsHistoryListData
+    ) {
+        // 绑定数据
+        // 来源渠道或消费渠道
+        holder.binding.tvLuckyCoinName.text = item.luckyCoinsInfo.channel
+
+        // 变更金额
+        when (item.luckyCoinsInfo.op) {
+            LuckyCoinsRecordType.ADD.value -> {
+                holder.binding.tvLuckyCoinNum.text = "+${item.luckyCoinsInfo.amount}"
+            }
+
+            LuckyCoinsRecordType.Reduce.value -> {
+                holder.binding.tvLuckyCoinNum.text = "-${item.luckyCoinsInfo.amount}"
+            }
+
+            else -> holder.binding.tvLuckyCoinNum.visibility = View.GONE
+        }
+
+        // 原有金额和现有金额
+        setLuckyCoinsBalanceChangeText(
+            holder.binding.tvLuckyCoinBalance,
+            item.luckyCoinsInfo.lastBalance,
+            item.luckyCoinsInfo.newBalance
+        )
+
+        // 变更日期
+        holder.binding.tvRecordTime.text = timeToEnYMDHMS(item.luckyCoinsInfo.recordTs)
+    }
+}

+ 92 - 1
module/wallet/src/main/java/com/adealink/weparty/wallet/viewmodel/WalletViewModel.kt

@@ -7,6 +7,7 @@ import androidx.lifecycle.MutableLiveData
 import com.adealink.frame.base.CommonDataNullError
 import com.adealink.frame.base.IError
 import com.adealink.frame.base.Rlt
+import com.adealink.frame.data.json.froJsonErrorNull
 import com.adealink.frame.locale.country.getCountryCode
 import com.adealink.frame.log.Log
 import com.adealink.frame.mvvm.livedata.ExtLiveData
@@ -15,9 +16,16 @@ import com.adealink.frame.mvvm.livedata.OnceMutableLiveData
 import com.adealink.frame.mvvm.viewmodel.BaseViewModel
 import com.adealink.frame.statistics.CommonEventValue
 import com.adealink.frame.util.PackageUtil
+import com.adealink.weparty.commonui.ext.getOrNull
+import com.adealink.weparty.commonui.ext.isSuccess
+import com.adealink.weparty.commonui.ext.onSuccess
 import com.adealink.weparty.commonui.toast.util.showToast
+import com.adealink.weparty.config.GlobalConfigType
+import com.adealink.weparty.config.IGlobalConfigListener
+import com.adealink.weparty.config.globalConfigManager
 import com.adealink.weparty.module.operation.banner.data.BannerInfo
 import com.adealink.weparty.module.profile.ProfileModule
+import com.adealink.weparty.module.wallet.WalletModule
 import com.adealink.weparty.module.wallet.data.CoinTransactionInfo
 import com.adealink.weparty.module.wallet.data.Currency
 import com.adealink.weparty.module.wallet.data.DiamondEntrance
@@ -27,6 +35,7 @@ import com.adealink.weparty.module.wallet.data.GoldGameCoinsConfigInfo
 import com.adealink.weparty.module.wallet.data.PayChannel
 import com.adealink.weparty.module.wallet.data.PayerMaxOrderStatus
 import com.adealink.weparty.module.wallet.data.ProductInfo
+import com.adealink.weparty.module.wallet.data.UserChargeNotify
 import com.adealink.weparty.module.wallet.data.UserCoinConsumeRecordInfo
 import com.adealink.weparty.module.wallet.data.UserCurrencyRecordInfo
 import com.adealink.weparty.module.wallet.data.UserRechargeRecordInfo
@@ -41,9 +50,10 @@ import com.adealink.weparty.wallet.pay.PayRes
 import com.adealink.weparty.wallet.pay.payManager
 import com.adealink.weparty.wallet.stat.PayStatEvent
 import com.adealink.weparty.wallet.stat.getReportPayType
+import com.adealink.weparty.webview.datasource.local.WebLocalService
 import kotlinx.coroutines.launch
 
-class WalletViewModel : BaseViewModel(), IWalletViewModel, IWalletListener {
+class WalletViewModel : BaseViewModel(), IWalletViewModel, IWalletListener, IGlobalConfigListener {
     override val myCurrencyList: LiveData<Rlt<GetUserCurrencyRes>> = MutableLiveData()
     override val myCoinCount: LiveData<Rlt<Long>> = MutableLiveData()
     override val myDiamondCount: LiveData<Rlt<Long>> = MutableLiveData()
@@ -68,6 +78,7 @@ class WalletViewModel : BaseViewModel(), IWalletViewModel, IWalletListener {
     override val currencyList: LiveData<Rlt<List<String>>> = MutableLiveData()
     override val exchangeRateLd: LiveData<Rlt<Long>> = MutableLiveData()
     override val minDiamondLd: LiveData<Rlt<Long>> = MutableLiveData()
+    override val luckyCoinsList: LiveData<Rlt<List<UserCurrencyRecordInfo>>> = MutableLiveData()
 
     init {
         walletManager.addListener(this)
@@ -420,6 +431,7 @@ class WalletViewModel : BaseViewModel(), IWalletViewModel, IWalletListener {
                 Currency.Coin -> myCoinCount.send(Rlt.Success(it.value))
                 Currency.Diamond -> myDiamondCount.send(Rlt.Success(it.value))
                 Currency.GameCoins -> myGameCoinsCount.send(Rlt.Success(it.value))
+                Currency.LuckyCoins -> {}
             }
         }
     }
@@ -434,7 +446,43 @@ class WalletViewModel : BaseViewModel(), IWalletViewModel, IWalletListener {
                 Currency.GameCoins ->{
 
                 }
+                Currency.LuckyCoins->{
+
+                }
+            }
+        }
+    }
+
+    override fun getLuckyCoinsList(currentType: Int, limit: Int) {
+        // 协程异步处理
+        viewModelScope.launch {
+            when (val rlt = walletManager.getLuckyCoinsList(currentType, limit)) {
+                is Rlt.Success -> {
+                    if (rlt.data.data == null) {
+                        luckyCoinsList.send(Rlt.Failed(CommonDataNullError()))
+                    } else {
+                        luckyCoinsList.send(Rlt.Success(rlt.data.data!!))
+                    }
+                }
+
+                is Rlt.Failed -> {
+                    luckyCoinsList.send(rlt)
+                }
+            }
+        }
+    }
+
+    override fun onUserCoinChargeChanged(data: UserChargeNotify) {
+        if (data.currencyType == Currency.LuckyCoins.value.toInt()) {
+            //幸运币,修改本地sp
+            if (data.chargeAmount <= 0) {
+                return
             }
+            if (data.chargeTs <= WebLocalService.lastUpdateTime) {
+                return
+            }
+            WebLocalService.lastUpdateTime = data.chargeTs
+            WebLocalService.needToShowLuckyDialog = "true"
         }
     }
 
@@ -443,4 +491,47 @@ class WalletViewModel : BaseViewModel(), IWalletViewModel, IWalletListener {
         walletManager.removeListener(this)
     }
 
+    override fun getUserChargeNotify(): LiveData<Rlt<List<UserChargeNotify>>> {
+        val liveData = OnceMutableLiveData<Rlt<List<UserChargeNotify>>>()
+        viewModelScope.launch {
+            val uid = ProfileModule.getMyUserInfo()?.uid ?: 0L
+            if (uid <= 0L) {
+                liveData.send(Rlt.Failed(IError("User not logged in")))
+                return@launch
+            }
+            // 检查幸运币数量
+            val luckyCoinCountRlt = WalletModule.getCurrencyCount(Currency.LuckyCoins)
+            luckyCoinCountRlt.onSuccess { cnt ->
+                if (cnt <= 0) {
+                    WebLocalService.needToShowLuckyDialog = "false"
+                    return@launch
+                }
+            }
+
+            val currencyTypes = listOf(Currency.LuckyCoins.value.toInt())
+
+            val rlt = walletManager.getUserChargeNotify(currencyTypes, uid)
+            if (rlt.isSuccess) {
+                val data = rlt.getOrNull() ?: return@launch
+                data.forEach {
+                    onUserCoinChargeChanged(it)
+                }
+            }
+        }
+        return liveData
+    }
+
+    override fun onConfigGet(configType: GlobalConfigType, config: List<String>) {
+        if (configType == GlobalConfigType.GLOBAL_CONFIG_LUCKY_COIN_REGION) {
+            val config = config.firstOrNull() ?: return
+            val regions = froJsonErrorNull<List<String>>(config)
+            val regionEnable = regions?.contains(ProfileModule.getMyUserInfo()?.region) ?: false
+            WebLocalService.luckCoinRegionEnable = if (regionEnable) "true" else "false"
+        }
+    }
+
+    override fun addGlobalConfigListener() {
+        globalConfigManager.addListener(GlobalConfigType.GLOBAL_CONFIG_LUCKY_COIN_REGION,this)
+    }
+
 }

BIN
module/wallet/src/main/res/drawable-xhdpi/wallet_lucky_coin_ic.png


+ 16 - 46
module/wallet/src/main/res/layout/activity_coin_history.xml

@@ -3,7 +3,6 @@
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    xmlns:tools="http://schemas.android.com/tools"
     android:fitsSystemWindows="true">
 
     <com.adealink.weparty.commonui.widget.CommonTopBar
@@ -11,57 +10,28 @@
         android:layout_width="match_parent"
         android:layout_height="44dp"
         app:layout_constraintTop_toTopOf="parent"
-        app:top_bar_title="@string/wallet_coin_record"/>
+        app:top_bar_title="@string/wallet_coin_record" />
 
-    <View
-        android:id="@+id/v_time_bg"
+    <com.adealink.weparty.commonui.widget.CommonTabLayout
+        android:id="@+id/common_tab_layout"
         android:layout_width="match_parent"
-        android:layout_height="36dp"
-        app:layout_constraintTop_toBottomOf="@id/top_bar"
-        android:background="@color/color_F5F7FA" />
-
-    <TextView
-        android:id="@+id/tv_time"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        tools:text="2025.03.03"
-        android:textColor="@color/color_222222"
-        android:textSize="14sp"
-        app:drawableRightCompat="@drawable/wallet_down_black_24_ic"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toTopOf="@id/v_time_bg"
-        app:layout_constraintBottom_toBottomOf="@id/v_time_bg"
-        android:layout_marginStart="16dp" />
-
-    <androidx.appcompat.widget.AppCompatTextView
-        android:id="@+id/tv_filter"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        tools:text="All"
-        android:textSize="13sp"
-        android:textColor="@color/color_222222"
-        android:gravity="center"
-        app:drawableEndCompat="@drawable/common_ic_filter_purple_state"
-        android:drawablePadding="4dp"
-        app:layout_constraintTop_toTopOf="@id/v_time_bg"
-        app:layout_constraintBottom_toBottomOf="@id/v_time_bg"
+        android:layout_height="44dp"
+        app:fixed_tab_scrollable_mode="true"
         app:layout_constraintEnd_toEndOf="parent"
-        android:layout_marginEnd="16dp"/>
-
-    <com.scwang.smart.refresh.layout.SmartRefreshLayout
-        android:id="@+id/refresh_layout"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/top_bar"
+        app:normal_text_size="16sp"
+        app:selected_text_size="16sp"
+        app:indicator_color="@color/color_FFFF50AE"
+        app:tab_mode="fixed"
+        app:tab_type="match_parent" />
+
+    <androidx.viewpager2.widget.ViewPager2
+        android:id="@+id/history_view_page"
         android:layout_width="match_parent"
         android:layout_height="0dp"
-        app:layout_constraintTop_toBottomOf="@id/v_time_bg"
         app:layout_constraintBottom_toBottomOf="parent"
-        android:orientation="vertical">
-
-        <androidx.recyclerview.widget.RecyclerView
-            android:id="@+id/rv_record"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            tools:listitem="@layout/layout_currency_history_item"/>
+        app:layout_constraintTop_toBottomOf="@+id/common_tab_layout" />
 
-    </com.scwang.smart.refresh.layout.SmartRefreshLayout>
 
 </androidx.constraintlayout.widget.ConstraintLayout>

+ 59 - 0
module/wallet/src/main/res/layout/fragment_gold_coin_history.xml

@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <View
+        android:id="@+id/v_time_bg"
+        android:layout_width="match_parent"
+        android:layout_height="36dp"
+        android:background="@color/color_F5F7FA"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <TextView
+        android:id="@+id/tv_time"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="16dp"
+        android:textColor="@color/color_222222"
+        android:textSize="14sp"
+        app:drawableRightCompat="@drawable/wallet_down_black_24_ic"
+        app:layout_constraintBottom_toBottomOf="@id/v_time_bg"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="@id/v_time_bg"
+        tools:text="2025.03.03" />
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/tv_filter"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="16dp"
+        android:drawablePadding="4dp"
+        android:gravity="center"
+        android:textColor="@color/color_222222"
+        android:textSize="13sp"
+        app:drawableEndCompat="@drawable/common_ic_filter_purple_state"
+        app:layout_constraintBottom_toBottomOf="@id/v_time_bg"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="@id/v_time_bg"
+        tools:text="All" />
+
+    <com.scwang.smart.refresh.layout.SmartRefreshLayout
+        android:id="@+id/refresh_layout"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:orientation="vertical"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/v_time_bg">
+
+        <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/rv_record"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            tools:listitem="@layout/layout_currency_history_item" />
+
+    </com.scwang.smart.refresh.layout.SmartRefreshLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 34 - 0
module/wallet/src/main/res/layout/fragment_lucky_coins_history.xml

@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:background="@color/color_F5F7FA">
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/tv_history_tip"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:padding="12dp"
+        android:text="@string/wallet_history_tip"
+        android:textColor="@color/color_777777"
+        android:textSize="14sp"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <com.scwang.smart.refresh.layout.SmartRefreshLayout
+        android:id="@+id/refresh_layout"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/tv_history_tip">
+
+        <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/rv_lucky_coins"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            tools:listitem="@layout/layout_lucky_coins_item" />
+
+    </com.scwang.smart.refresh.layout.SmartRefreshLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 104 - 0
module/wallet/src/main/res/layout/layout_lucky_coins_item.xml

@@ -0,0 +1,104 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:background="@color/white">
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/cl_lucky_coin_desc"
+        android:layout_width="match_parent"
+        android:layout_height="44dp"
+        app:layout_constraintTop_toTopOf="parent">
+
+        <androidx.appcompat.widget.AppCompatTextView
+            android:id="@+id/tv_lucky_coin_name"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="16dp"
+            android:layout_marginEnd="8dp"
+            android:ellipsize="end"
+            android:includeFontPadding="false"
+            android:lines="1"
+            android:textColor="@color/color_222222"
+            android:textDirection="locale"
+            android:textSize="15sp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toStartOf="@id/tv_lucky_coin_num"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:text="Lucky Game Challenge" />
+
+        <androidx.appcompat.widget.AppCompatTextView
+            android:id="@+id/tv_lucky_coin_num"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginEnd="4dp"
+            android:includeFontPadding="false"
+            android:textColor="@color/color_FF9352FC"
+            android:textSize="17sp"
+            android:textStyle="bold"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toStartOf="@id/iv_lucky_coin_icon"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:text="+200" />
+
+        <androidx.appcompat.widget.AppCompatImageView
+            android:id="@+id/iv_lucky_coin_icon"
+            android:layout_width="20dp"
+            android:layout_height="20dp"
+            android:layout_marginEnd="16dp"
+            android:src="@drawable/wallet_lucky_coin_ic"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toTopOf="parent" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+    <View
+        android:id="@+id/line"
+        android:layout_width="match_parent"
+        android:layout_height="1dp"
+        android:layout_marginHorizontal="16dp"
+        android:background="@color/color_E9E9EA"
+        app:layout_constraintTop_toBottomOf="@id/cl_lucky_coin_desc" />
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/cl_lucky_coin_info"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:paddingHorizontal="16dp"
+        android:paddingVertical="12dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/line">
+
+        <androidx.appcompat.widget.AppCompatTextView
+            android:id="@+id/tv_lucky_coin_balance"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:includeFontPadding="false"
+            android:text="@string/wallet_lucky_coins_balance"
+            android:textColor="@color/color_AAAAAA"
+            android:textSize="12sp"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:text="Lucky Coins balance: 1000 -> 1200" />
+
+        <androidx.appcompat.widget.AppCompatTextView
+            android:id="@+id/tv_record_time"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="3dp"
+            android:includeFontPadding="false"
+            android:textColor="@color/color_AAAAAA"
+            android:textSize="12sp"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@id/tv_lucky_coin_balance"
+            tools:text="2021.12.12 09:24" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>

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

@@ -75,4 +75,7 @@
    <string name="wallet_recharge_daily_count_down_title">الوقت المتبقي:</string>
    <string name="wallet_recharge_amount_title">الرجاء تحديد مبلغ إعادة الشحن</string>
    <string name="wallet_category_union_leader_incentive">مكافأة الوكيل</string>
+   <string name="wallet_gold_coin">عملة ذهبية</string>
+   <string name="wallet_lucky_coins">عملات الحظ</string>
+   <string name="wallet_lucky_coins_balance">رصيد عملات Lucky:%1$s-&gt;%2$s</string>
 </resources>

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

@@ -75,4 +75,7 @@
    <string name="wallet_recharge_daily_count_down_title">剩余时间:</string>
    <string name="wallet_recharge_amount_title">请选择充值金额</string>
    <string name="wallet_category_union_leader_incentive">公会长奖励</string>
+   <string name="wallet_gold_coin">金币</string>
+   <string name="wallet_lucky_coins">幸运币</string>
+   <string name="wallet_lucky_coins_balance">幸运币余额:%1$s-&gt;%2$s</string>
 </resources>

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

@@ -75,4 +75,7 @@
    <string name="wallet_recharge_daily_count_down_title">Time left:</string>
    <string name="wallet_recharge_amount_title">Please select the recharge amount</string>
    <string name="wallet_category_union_leader_incentive">Agency leader Reward</string>
+   <string name="wallet_gold_coin">Gold coin</string>
+   <string name="wallet_lucky_coins">Lucky Coins</string>
+   <string name="wallet_lucky_coins_balance">Lucky Coins balance:%1$s-&gt;%2$s</string>
 </resources>

+ 6 - 0
module/webview/src/main/java/com/adealink/weparty/webview/WeNextWebView.kt

@@ -10,6 +10,7 @@ import com.adealink.weparty.webview.constant.NATIVE_JS_BRIDGE
 import com.adealink.weparty.webview.constant.TAG_WEB_VIEW
 import com.adealink.weparty.webview.jsbridge.JSBridge
 import com.adealink.weparty.webview.jsbridge.JSBridgeImpl
+import com.adealink.weparty.webview.jsbridge.method.KVStorageSetJSNativeMethod
 import com.adealink.weparty.webview.jsbridge.method.LogJSNativeMethod
 import com.adealink.weparty.webview.jsbridge.method.StatJSNativeMethod
 import com.adealink.weparty.webview.jsnativemethod.ClosePreloadManagerJSNativeMethod
@@ -33,9 +34,11 @@ import com.adealink.weparty.webview.jsnativemethod.GoLuckyGameRankJSNativeMethod
 import com.adealink.weparty.webview.jsnativemethod.GoRechargeJSNativeMethod
 import com.adealink.weparty.webview.jsnativemethod.HideNavigationBarJSNativeMethod
 import com.adealink.weparty.webview.jsnativemethod.IsHighPotentialVibrationOpenJSNativeMethod
+import com.adealink.weparty.webview.jsnativemethod.KVStorageGetJSNativeMethod
 import com.adealink.weparty.webview.jsnativemethod.LaunchGooglePayJSNativeMethod
 import com.adealink.weparty.webview.jsnativemethod.OpenFullPageWebView
 import com.adealink.weparty.webview.jsnativemethod.OpenInPhoneBrowser
+import com.adealink.weparty.webview.jsnativemethod.PerformanceHelperJSNativeMethod
 import com.adealink.weparty.webview.jsnativemethod.PlaySoundJSNativeMethod
 import com.adealink.weparty.webview.jsnativemethod.ReportAfBackEventJSNativeMethod
 import com.adealink.weparty.webview.jsnativemethod.ReportWebInfoJSNativeMethod
@@ -125,6 +128,9 @@ class WeNextWebView : BaseWebView {
         jsBridge.addNativeMethod(SignInSuccessJsNativeMethod())
         jsBridge.addNativeMethod(OnDeeplinkJsMethod())
         jsBridge.addNativeMethod(GetLabelUrlJsNativeMethod())
+        jsBridge.addNativeMethod(KVStorageSetJSNativeMethod())
+        jsBridge.addNativeMethod(KVStorageGetJSNativeMethod())
+        jsBridge.addNativeMethod(PerformanceHelperJSNativeMethod())
     }
 
     override fun loadUrl(urlP: String) {

+ 19 - 0
module/webview/src/main/java/com/adealink/weparty/webview/jsnativemethod/PerformanceHelperJSNativeMethod.kt

@@ -0,0 +1,19 @@
+package com.adealink.weparty.webview.jsnativemethod
+
+import com.adealink.frame.apm.monitor.PerformanceHelper
+import com.adealink.frame.log.Log
+import com.adealink.weparty.webview.constant.TAG_WEB_VIEW_JS_BRIDGE
+import com.adealink.weparty.webview.jsbridge.callback.JSBridgeCallback
+import com.adealink.weparty.webview.jsbridge.method.JSNativeMethod
+
+class PerformanceHelperJSNativeMethod : JSNativeMethod<Any, Any> {
+
+    override val methodName = "performanceHelper"
+
+    override fun handleMethodCall(data: Any, callback: JSBridgeCallback<Any>?) {
+        val isMemoryHigh = PerformanceHelper.isWeakDeviceMemoryUsageHigh()
+        Log.d(TAG_WEB_VIEW_JS_BRIDGE, "PerformanceHelper, isMemoryHigh:$isMemoryHigh")
+        callback?.resolve(isMemoryHigh)
+    }
+
+}