Przeglądaj źródła

Merge remote-tracking branch 'origin/feat/room_gift' into dev

* origin/feat/room_gift: (42 commits)
  feat: 调整应用内 url 声明
  feat: 替换 SPN 资源链接
  feat: 替换 SPN 资源链接
  feat: 房间列表进入房间失败时,需要上报后端进行房间状态检测
  feat: 房间内下单引导背景色调亮
  feat: 礼物面板顶部麦位序号文案替换
  feat: 调整房间内个人卡片的技能价格与金币之间的间隙
  feat: 调整礼物面板底部菜单距离屏幕底部的间隙
  feat: 房间公屏和动态评论增加 腾讯emoji 显示支持(目前仅 android 可发,iOS 仅展示)
  feat: 修复退房重进会导致 metaData 丢失的问题
  feat: 调整礼物面板底部钻石图标的展示
  feat: 礼物面板增加选中时的跳动动画
  feat: 礼物公屏的字体改为 h5
  feat: 调整礼物顶部头像区的间隙
  feat: 增加腾讯错误信息展示过滤
  feat: 礼物面板顶部头像区增加右边的渐变遮罩
  fix: 修复麦克风图标显示异常的问题
  feat: 上麦申请列表的文案调整,右边过滤器宽度限制
  feat: 个人页在房图标大小改为 37
  fix: 修复麦位申请列表时间展示异常的问题
  ...
陈文艺 3 dni temu
rodzic
commit
ea2faa021f
100 zmienionych plików z 2089 dodań i 290 usunięć
  1. 34 16
      Lanu.xcodeproj/project.pbxproj
  2. 12 12
      Lanu.xcworkspace/xcshareddata/swiftpm/Package.resolved
  3. 1 0
      Lanu/AppDelegate.swift
  4. 6 0
      Lanu/Assets.xcassets/Common/Warning/Contents.json
  5. 22 0
      Lanu/Assets.xcassets/Common/Warning/ic_warning.imageset/Contents.json
  6. BIN
      Lanu/Assets.xcassets/Common/Warning/ic_warning.imageset/ic_warning@2x.png
  7. BIN
      Lanu/Assets.xcassets/Common/Warning/ic_warning.imageset/ic_warning@3x.png
  8. 22 0
      Lanu/Assets.xcassets/Room/ic_gift_selected_bg.imageset/Contents.json
  9. BIN
      Lanu/Assets.xcassets/Room/ic_gift_selected_bg.imageset/ic_gift_selected_bg@2x.png
  10. BIN
      Lanu/Assets.xcassets/Room/ic_gift_selected_bg.imageset/ic_gift_selected_bg@3x.png
  11. 10 28
      Lanu/Common/Config/String+Urls.swift
  12. 18 0
      Lanu/Common/Extension/Date+Extension.swift
  13. 40 0
      Lanu/Common/Extension/TimeInterval+Extension.swift
  14. 2 2
      Lanu/Common/Extension/UIImage+Extension.swift
  15. 2 0
      Lanu/Common/Storage/LNUserDefaultsKey.swift
  16. 3 0
      Lanu/Common/Theme/UIFont+Theme.swift
  17. 54 0
      Lanu/Common/Theme/UIView+Theme.swift
  18. 1 1
      Lanu/Common/Views/ImagePreview/LNImagePreviewCell.swift
  19. 1 1
      Lanu/Common/Views/ImagePreview/LNImagePreviewController.swift
  20. 1 1
      Lanu/Common/Views/ImageUpload/LNImageUploadView.swift
  21. 1 1
      Lanu/Common/Views/ImageUpload/LNMultiImagesUploadView.swift
  22. 1 1
      Lanu/Common/Views/LNCaptchaInputView.swift
  23. 23 3
      Lanu/Common/Views/LNNestedScrollView.swift
  24. 1 1
      Lanu/Common/Views/LNSortedEditView.swift
  25. 1 1
      Lanu/Common/Views/LNTextField.swift
  26. 4 4
      Lanu/Common/Views/LNVideoPlayerView.swift
  27. 1 1
      Lanu/Common/Views/LNVoiceEditView.swift
  28. 37 13
      Lanu/Common/Views/Menu/LNCommonAlertView.swift
  29. 1 1
      Lanu/Common/Views/Selection/LNHourRangePickerPanel.swift
  30. 1 1
      Lanu/Common/Views/StarScore/LNFiveStarScoreView.swift
  31. 1 1
      Lanu/Common/Views/VideoPreview/LNVideoPreviewCell.swift
  32. 1 1
      Lanu/Common/Views/VideoPreview/LNVideoPreviewController.swift
  33. 3 3
      Lanu/Common/Views/VideoUpload/LNVideoUploadView.swift
  34. 1 1
      Lanu/Common/Voice/LNVoiceRecorder.swift
  35. 1 1
      Lanu/Common/Voice/LNVoiceResourceManager.swift
  36. 6 6
      Lanu/GoogleService-Info-Release.plist
  37. 282 6
      Lanu/Localizable.xcstrings
  38. 1 1
      Lanu/Manager/Account/LNAccountManager.swift
  39. 2 2
      Lanu/Manager/Deeplink/LNDeeplinkManager.swift
  40. 183 0
      Lanu/Manager/Gift/LNGiftManager.swift
  41. 46 0
      Lanu/Manager/Gift/Network/LNGiftResponse.swift
  42. 55 0
      Lanu/Manager/Gift/Network/LNHttpManager+Gift.swift
  43. 1 1
      Lanu/Manager/IM/LNIMManager.swift
  44. 5 5
      Lanu/Manager/Network/Download/LNFileDownloader.swift
  45. 3 3
      Lanu/Manager/Network/Upload/LNFileUploader.swift
  46. 2 1
      Lanu/Manager/Order/LNOrderManager.swift
  47. 1 1
      Lanu/Manager/Profile/LNProfileManager.swift
  48. 10 0
      Lanu/Manager/Purchase/LNPurchaseManager.swift
  49. 24 0
      Lanu/Manager/Purchase/Network/LNPurchaseResponse.swift
  50. 19 2
      Lanu/Manager/Room/LNRoomManager.swift
  51. 11 0
      Lanu/Manager/Room/Network/LNHttpManager+Room.swift
  52. 0 25
      Lanu/Manager/Room/Network/LNRoomResponse.swift
  53. 4 4
      Lanu/Views/Game/Category/LNGameCategoryListView.swift
  54. 1 1
      Lanu/Views/Game/Category/LNGameCategoryTabView.swift
  55. 1 1
      Lanu/Views/Game/Join/Input/BaseInfo/LNJoinUsInputInfoView.swift
  56. 1 1
      Lanu/Views/Game/Join/Input/BindPhone/LNJoinUsInputCaptchaView.swift
  57. 1 1
      Lanu/Views/Game/Join/Input/BindPhone/LNJoinUsInputPhoneView.swift
  58. 1 1
      Lanu/Views/Game/Join/Input/SkillInfo/LNJoinUsSelectSkillView.swift
  59. 2 2
      Lanu/Views/Game/MateFilter/LNGameCategoryFilterPanel.swift
  60. 1 1
      Lanu/Views/Game/MateFilter/LNGameFilterPanel.swift
  61. 1 1
      Lanu/Views/Game/MateFilter/LNGameMateFilterPanel.swift
  62. 1 1
      Lanu/Views/Game/MateList/LNGameMateListMenuView.swift
  63. 1 1
      Lanu/Views/Game/Skill/Edit/LNSkillFieldBaseEditView.swift
  64. 1 1
      Lanu/Views/Home/GameTab/LNHomeActivityTabView.swift
  65. 1 1
      Lanu/Views/Home/GameTab/LNMainGameTabView.swift
  66. 1 1
      Lanu/Views/Home/LNHomeTopTabView.swift
  67. 1 1
      Lanu/Views/IM/Chat/Emoji/LNIMChatEmojiPanel.swift
  68. 1 1
      Lanu/Views/IM/Chat/GameMate/LNIMChatGameMateSkillCell.swift
  69. 1 1
      Lanu/Views/IM/Chat/InputMenu/LNIMChatTextInputView.swift
  70. 1 1
      Lanu/Views/IM/Chat/InputMenu/LNIMChatVoiceInputView.swift
  71. 1 1
      Lanu/Views/Order/OrderQR/LNOrderGenerateQRCodePanel.swift
  72. 1 1
      Lanu/Views/Order/OrderQR/LNOrderQRTabView.swift
  73. 1 1
      Lanu/Views/Profile/Edit/LNEditProfilePhotoWallView.swift
  74. 1 1
      Lanu/Views/Profile/Feed/LNFeedCommentCell.swift
  75. 1 1
      Lanu/Views/Profile/Feed/LNProfileFeedItemCell.swift
  76. 1 1
      Lanu/Views/Profile/Profile/Detail/LNProfileUserDetailView.swift
  77. 1 1
      Lanu/Views/Profile/Profile/LNProfileInRoomView.swift
  78. 1 1
      Lanu/Views/Profile/Profile/LNProfilePhotoWall.swift
  79. 1 1
      Lanu/Views/Profile/Profile/LNProfileTabView.swift
  80. 1 1
      Lanu/Views/Profile/Relation/LNUserRelationListView.swift
  81. 2 2
      Lanu/Views/Room/Bottom/Input/LNRoomMessageInputView.swift
  82. 9 3
      Lanu/Views/Room/Bottom/LNRoomBottomMenuView.swift
  83. 4 4
      Lanu/Views/Room/Bottom/Mic/LNRoomBottomMicView.swift
  84. 198 0
      Lanu/Views/Room/Gift/LNRoomGiftBottomView.swift
  85. 296 0
      Lanu/Views/Room/Gift/LNRoomGiftHeaderView.swift
  86. 101 0
      Lanu/Views/Room/Gift/LNRoomGiftPanel.swift
  87. 125 0
      Lanu/Views/Room/Gift/List/LNRoomGiftItemCell.swift
  88. 115 0
      Lanu/Views/Room/Gift/List/LNRoomGiftListView.swift
  89. 1 1
      Lanu/Views/Room/Join/Apply/LNRoomApplySeatCell.swift
  90. 15 18
      Lanu/Views/Room/Join/Apply/LNRoomApplySeatListPanel.swift
  91. 1 1
      Lanu/Views/Room/Join/Manage/LNRoomManageSeatCell.swift
  92. 51 49
      Lanu/Views/Room/Join/Manage/LNRoomManageSeatListView.swift
  93. 1 1
      Lanu/Views/Room/LNRoomOrderGuideView.swift
  94. 4 4
      Lanu/Views/Room/Message/Cells/LNRoomChatMessageCell.swift
  95. 124 0
      Lanu/Views/Room/Message/Cells/LNRoomGiftMessageCell.swift
  96. 0 0
      Lanu/Views/Room/Message/Cells/LNRoomSystemMessageCell.swift
  97. 0 0
      Lanu/Views/Room/Message/Cells/LNRoomUnknownMessageCell.swift
  98. 6 5
      Lanu/Views/Room/Message/Cells/LNRoomWelcomeMessageCell.swift
  99. 30 12
      Lanu/Views/Room/Message/LNRoomMessageView.swift
  100. 16 10
      Lanu/Views/Room/Profile/LNRoomProfileBottomMenu.swift

+ 34 - 16
Lanu.xcodeproj/project.pbxproj

@@ -70,6 +70,7 @@
 				"Common/Theme/UIFont+Theme.swift",
 				"Common/Theme/UIImage+Theme.swift",
 				"Common/Theme/UIImageView+Theme.swift",
+				"Common/Theme/UIView+Theme.swift",
 				Common/Views/Base/LNFakeNaviBar.swift,
 				Common/Views/Base/LNNavigationController.swift,
 				Common/Views/Base/LNViewController.swift,
@@ -135,6 +136,9 @@
 				Manager/GameMate/LNGameMateManager.swift,
 				Manager/GameMate/Network/LNGameMateResponse.swift,
 				"Manager/GameMate/Network/LNHttpManager+GameMate.swift",
+				Manager/Gift/LNGiftManager.swift,
+				Manager/Gift/Network/LNGiftResponse.swift,
+				"Manager/Gift/Network/LNHttpManager+Gift.swift",
 				Manager/IM/Emoji/LNEmojiData.swift,
 				Manager/IM/Emoji/LNIMEmojiManager.swift,
 				"Manager/IM/Emoji/String+TUIEmoji.swift",
@@ -358,6 +362,11 @@
 				Views/Room/Bottom/Mic/LNRoomBottomMicView.swift,
 				Views/Room/Create/LNCreateRoomPanel.swift,
 				Views/Room/Create/LNRoomNameInputPanel.swift,
+				Views/Room/Gift/List/LNRoomGiftItemCell.swift,
+				Views/Room/Gift/List/LNRoomGiftListView.swift,
+				Views/Room/Gift/LNRoomGiftBottomView.swift,
+				Views/Room/Gift/LNRoomGiftHeaderView.swift,
+				Views/Room/Gift/LNRoomGiftPanel.swift,
 				Views/Room/Join/Apply/LNRoomApplySeatCell.swift,
 				Views/Room/Join/Apply/LNRoomApplySeatListPanel.swift,
 				Views/Room/Join/Apply/LNRoomApplySeatPanel.swift,
@@ -373,11 +382,12 @@
 				Views/Room/LNRoomOrderGuideView.swift,
 				Views/Room/LNRoomSheetMenu.swift,
 				Views/Room/LNRoomViewController.swift,
-				Views/Room/Message/LNRoomChatMessageCell.swift,
+				Views/Room/Message/Cells/LNRoomChatMessageCell.swift,
+				Views/Room/Message/Cells/LNRoomGiftMessageCell.swift,
+				Views/Room/Message/Cells/LNRoomSystemMessageCell.swift,
+				Views/Room/Message/Cells/LNRoomUnknownMessageCell.swift,
+				Views/Room/Message/Cells/LNRoomWelcomeMessageCell.swift,
 				Views/Room/Message/LNRoomMessageView.swift,
-				Views/Room/Message/LNRoomSystemMessageCell.swift,
-				Views/Room/Message/LNRoomUnknownMessageCell.swift,
-				Views/Room/Message/LNRoomWelcomeMessageCell.swift,
 				Views/Room/Profile/LNRoomProfileBottomMenu.swift,
 				Views/Room/Profile/LNRoomProfileCardPanel.swift,
 				Views/Room/Profile/LNRoomProfileSkillView.swift,
@@ -387,10 +397,11 @@
 				Views/Room/Settings/LNRoomInfoEditPanel.swift,
 				Views/Room/Settings/LNRoomSettingMenuPanel.swift,
 				Views/Room/Top/LNRoomTopMenuView.swift,
-				"Views/Room/ViewModel/LiveInfo+Extension.swift",
 				Views/Room/ViewModel/LNRoomViewModel.swift,
-				Views/Room/ViewModel/Message/LNRoomChatMessageItem.swift,
-				Views/Room/ViewModel/Message/LNRoomSystemMessageItem.swift,
+				"Views/Room/ViewModel/Message/Barrage+Extension.swift",
+				Views/Room/ViewModel/Message/LNRoomMessageItem.swift,
+				Views/Room/ViewModel/Message/LNRoomPushMessage.swift,
+				Views/Room/ViewModel/Message/LNRoomUserMessage.swift,
 				Views/Room/ViewModel/RoomInfo/LNRoomInfo.swift,
 				Views/Room/ViewModel/Seat/LNRoomSeatItem.swift,
 				Views/Search/LNUserSearchHistoryView.swift,
@@ -411,6 +422,7 @@
 				Views/Wallet/Coin/LNCoinViewController.swift,
 				Views/Wallet/Diamond/LNDiamondViewController.swift,
 				Views/Wallet/LNExchangePanel.swift,
+				Views/Wallet/LNMoneyNotEnoughAlertView.swift,
 				Views/Wallet/LNPurchasePanel.swift,
 				Views/Wallet/LNPurchaseProductView.swift,
 				Views/Wallet/LNWalletViewController.swift,
@@ -431,8 +443,6 @@
 		};
 		FBB67E232EC48B440070E686 /* ThirdParty */ = {
 			isa = PBXFileSystemSynchronizedRootGroup;
-			exceptions = (
-			);
 			path = ThirdParty;
 			sourceTree = "<group>";
 		};
@@ -585,10 +595,14 @@
 			inputFileListPaths = (
 				"${PODS_ROOT}/Target Support Files/Pods-Gami/Pods-Gami-resources-${CONFIGURATION}-input-files.xcfilelist",
 			);
+			inputPaths = (
+			);
 			name = "[CP] Copy Pods Resources";
 			outputFileListPaths = (
 				"${PODS_ROOT}/Target Support Files/Pods-Gami/Pods-Gami-resources-${CONFIGURATION}-output-files.xcfilelist",
 			);
+			outputPaths = (
+			);
 			runOnlyForDeploymentPostprocessing = 0;
 			shellPath = /bin/sh;
 			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Gami/Pods-Gami-resources.sh\"\n";
@@ -624,10 +638,14 @@
 			inputFileListPaths = (
 				"${PODS_ROOT}/Target Support Files/Pods-Gami/Pods-Gami-frameworks-${CONFIGURATION}-input-files.xcfilelist",
 			);
+			inputPaths = (
+			);
 			name = "[CP] Embed Pods Frameworks";
 			outputFileListPaths = (
 				"${PODS_ROOT}/Target Support Files/Pods-Gami/Pods-Gami-frameworks-${CONFIGURATION}-output-files.xcfilelist",
 			);
+			outputPaths = (
+			);
 			runOnlyForDeploymentPostprocessing = 0;
 			shellPath = /bin/sh;
 			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Gami/Pods-Gami-frameworks.sh\"\n";
@@ -898,7 +916,7 @@
 /* Begin XCRemoteSwiftPackageReference section */
 		FB696C152EC96C0F00FAD639 /* XCRemoteSwiftPackageReference "MJRefresh" */ = {
 			isa = XCRemoteSwiftPackageReference;
-			repositoryURL = "http://8.134.139.102:10880/chenwenyi/MJRefresh.git";
+			repositoryURL = "http://git.gami-internal.vip/chenwenyi/MJRefresh.git";
 			requirement = {
 				kind = upToNextMajorVersion;
 				minimumVersion = 3.7.9;
@@ -906,7 +924,7 @@
 		};
 		FB8316662F334C2C000396D5 /* XCRemoteSwiftPackageReference "AutoCodable" */ = {
 			isa = XCRemoteSwiftPackageReference;
-			repositoryURL = "http://8.134.139.102:10880/chenwenyi/AutoCodable.git";
+			repositoryURL = "http://git.gami-internal.vip/chenwenyi/AutoCodable.git";
 			requirement = {
 				kind = upToNextMajorVersion;
 				minimumVersion = 1.0.1;
@@ -914,7 +932,7 @@
 		};
 		FB9CD1172EC1EEA10033B14B /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = {
 			isa = XCRemoteSwiftPackageReference;
-			repositoryURL = "http://8.134.139.102:10880/chenwenyi/firebase-ios-sdk.git";
+			repositoryURL = "http://git.gami-internal.vip/chenwenyi/firebase-ios-sdk.git";
 			requirement = {
 				kind = upToNextMajorVersion;
 				minimumVersion = 12.5.0;
@@ -922,7 +940,7 @@
 		};
 		FB9CD11C2EC1EEF30033B14B /* XCRemoteSwiftPackageReference "GoogleSignIn-iOS" */ = {
 			isa = XCRemoteSwiftPackageReference;
-			repositoryURL = "http://8.134.139.102:10880/chenwenyi/GoogleSignIn-iOS.git";
+			repositoryURL = "http://git.gami-internal.vip/chenwenyi/GoogleSignIn-iOS.git";
 			requirement = {
 				kind = upToNextMajorVersion;
 				minimumVersion = 9.0.0;
@@ -930,7 +948,7 @@
 		};
 		FB9FCD242EF25D6B00DDAAC9 /* XCRemoteSwiftPackageReference "SDWebImage" */ = {
 			isa = XCRemoteSwiftPackageReference;
-			repositoryURL = "http://8.134.139.102:10880/chenwenyi/SDWebImage.git";
+			repositoryURL = "http://git.gami-internal.vip/chenwenyi/SDWebImage.git";
 			requirement = {
 				kind = upToNextMajorVersion;
 				minimumVersion = 5.21.3;
@@ -938,7 +956,7 @@
 		};
 		FBA06B672F18F7D300DDD745 /* XCRemoteSwiftPackageReference "SnapKit" */ = {
 			isa = XCRemoteSwiftPackageReference;
-			repositoryURL = "http://8.134.139.102:10880/chenwenyi/SnapKit.git";
+			repositoryURL = "http://git.gami-internal.vip/chenwenyi/SnapKit.git";
 			requirement = {
 				kind = upToNextMajorVersion;
 				minimumVersion = 5.7.2;
@@ -954,7 +972,7 @@
 		};
 		FBECAA192EC1C8860013A5E6 /* XCRemoteSwiftPackageReference "CocoaLumberjack" */ = {
 			isa = XCRemoteSwiftPackageReference;
-			repositoryURL = "http://8.134.139.102:10880/chenwenyi/CocoaLumberjack.git";
+			repositoryURL = "http://git.gami-internal.vip/chenwenyi/CocoaLumberjack.git";
 			requirement = {
 				kind = upToNextMajorVersion;
 				minimumVersion = 3.9.0;

+ 12 - 12
Lanu.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -1,5 +1,5 @@
 {
-  "originHash" : "bf0254c02f28b6a521e188811c39548ebf8a2568c812a254677f42a61aec88bb",
+  "originHash" : "f41e3ff6071308207bd8b0a1b710827d2490c909dca2c4208dfe61753bb5493b",
   "pins" : [
     {
       "identity" : "abseil-cpp-binary",
@@ -31,7 +31,7 @@
     {
       "identity" : "autocodable",
       "kind" : "remoteSourceControl",
-      "location" : "http://8.134.139.102:10880/chenwenyi/AutoCodable.git",
+      "location" : "http://git.gami-internal.vip/chenwenyi/AutoCodable.git",
       "state" : {
         "revision" : "730ad21275813109ae1e4329a1d7b8d3326b4f26",
         "version" : "1.0.1"
@@ -40,7 +40,7 @@
     {
       "identity" : "cocoalumberjack",
       "kind" : "remoteSourceControl",
-      "location" : "http://8.134.139.102:10880/chenwenyi/CocoaLumberjack.git",
+      "location" : "http://git.gami-internal.vip/chenwenyi/CocoaLumberjack.git",
       "state" : {
         "revision" : "a9ed4b6f9bdedce7d77046f43adfb8ce1fd54114",
         "version" : "3.9.0"
@@ -49,7 +49,7 @@
     {
       "identity" : "firebase-ios-sdk",
       "kind" : "remoteSourceControl",
-      "location" : "http://8.134.139.102:10880/chenwenyi/firebase-ios-sdk.git",
+      "location" : "http://git.gami-internal.vip/chenwenyi/firebase-ios-sdk.git",
       "state" : {
         "revision" : "793b67f4652e1a39d03fab6650033768afe6d15e",
         "version" : "12.5.0"
@@ -85,7 +85,7 @@
     {
       "identity" : "googlesignin-ios",
       "kind" : "remoteSourceControl",
-      "location" : "http://8.134.139.102:10880/chenwenyi/GoogleSignIn-iOS.git",
+      "location" : "http://git.gami-internal.vip/chenwenyi/GoogleSignIn-iOS.git",
       "state" : {
         "revision" : "3996d908c7b3ce8a87d39c808f9a6b2a08fbe043",
         "version" : "9.0.0"
@@ -148,7 +148,7 @@
     {
       "identity" : "mjrefresh",
       "kind" : "remoteSourceControl",
-      "location" : "http://8.134.139.102:10880/chenwenyi/MJRefresh.git",
+      "location" : "http://git.gami-internal.vip/chenwenyi/MJRefresh.git",
       "state" : {
         "revision" : "535d39c86e592d73f8b8e75f9bf1eda62ca4a4ce",
         "version" : "3.7.9"
@@ -175,7 +175,7 @@
     {
       "identity" : "sdwebimage",
       "kind" : "remoteSourceControl",
-      "location" : "http://8.134.139.102:10880/chenwenyi/SDWebImage.git",
+      "location" : "http://git.gami-internal.vip/chenwenyi/SDWebImage.git",
       "state" : {
         "revision" : "2053b120767c42a70bcba21095f34e4cfb54a75d",
         "version" : "5.21.3"
@@ -184,7 +184,7 @@
     {
       "identity" : "snapkit",
       "kind" : "remoteSourceControl",
-      "location" : "http://8.134.139.102:10880/chenwenyi/SnapKit.git",
+      "location" : "http://git.gami-internal.vip/chenwenyi/SnapKit.git",
       "state" : {
         "revision" : "0456911aa90276968dbcee48f011629aef6f2f4e",
         "version" : "5.7.2"
@@ -195,8 +195,8 @@
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/apple/swift-log",
       "state" : {
-        "revision" : "ce592ae52f982c847a4efc0dd881cc9eb32d29f2",
-        "version" : "1.6.4"
+        "revision" : "8c0f217f01000dd30f60d6e536569ad4e74291f9",
+        "version" : "1.11.0"
       }
     },
     {
@@ -204,8 +204,8 @@
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/apple/swift-protobuf.git",
       "state" : {
-        "revision" : "c169a5744230951031770e27e475ff6eefe51f9d",
-        "version" : "1.33.3"
+        "revision" : "a008af1a102ff3dd6cc3764bb69bf63226d0f5f6",
+        "version" : "1.36.1"
       }
     },
     {

+ 1 - 0
Lanu/AppDelegate.swift

@@ -32,6 +32,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
         _ = LNConfigManager.shared
         _ = LNOrderManager.shared
         _ = LNRoomManager.shared
+        _ = LNGiftManager.shared
         
         LNEventDeliver.notifyAppLaunchFinished()
         

+ 6 - 0
Lanu/Assets.xcassets/Common/Warning/Contents.json

@@ -0,0 +1,6 @@
+{
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 22 - 0
Lanu/Assets.xcassets/Common/Warning/ic_warning.imageset/Contents.json

@@ -0,0 +1,22 @@
+{
+  "images" : [
+    {
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "filename" : "ic_warning@2x.png",
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "filename" : "ic_warning@3x.png",
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
Lanu/Assets.xcassets/Common/Warning/ic_warning.imageset/ic_warning@2x.png


BIN
Lanu/Assets.xcassets/Common/Warning/ic_warning.imageset/ic_warning@3x.png


+ 22 - 0
Lanu/Assets.xcassets/Room/ic_gift_selected_bg.imageset/Contents.json

@@ -0,0 +1,22 @@
+{
+  "images" : [
+    {
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "filename" : "ic_gift_selected_bg@2x.png",
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "filename" : "ic_gift_selected_bg@3x.png",
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
Lanu/Assets.xcassets/Room/ic_gift_selected_bg.imageset/ic_gift_selected_bg@2x.png


BIN
Lanu/Assets.xcassets/Room/ic_gift_selected_bg.imageset/ic_gift_selected_bg@3x.png


+ 10 - 28
Lanu/Common/Config/String+Urls.swift

@@ -13,38 +13,20 @@ extension String {
         lowercased().starts(with: LNDeeplinkUrls.appScheme)
     }
     
-    static var webUrlHost: String {
+    static var webUrlHost: String = {
         LNAppConfig.shared.curEnv == .test ? "https://test-web.gami.vip" : "https://web.gami.vip"
-    }
-    static var privacyUrl: String = {
-        "\(webUrlHost)/about/privacyPolicy"
-    }()
-    static var serviceUrl: String = {
-        "\(webUrlHost)/about/termsOfService"
-    }()
-    static var communityUrl: String = {
-        "\(webUrlHost)/about/communityGuideline"
     }()
+    static var privacyUrl: String = "\(webUrlHost)/about/privacyPolicy"
+    static var serviceUrl: String =  "\(webUrlHost)/about/termsOfService"
+    static var communityUrl: String = "\(webUrlHost)/about/communityGuideline"
     
-    static var deleteAccountUrl: String = {
-        "\(webUrlHost)/mine/cancellation"
-    }()
+    static var deleteAccountUrl: String = "\(webUrlHost)/mine/cancellation"
     
-    static var walletHistoryUrl: String = {
-        "\(webUrlHost)/wallet/record"
-    }()
-    static var beanUrl: String = {
-        "\(webUrlHost)/wallet/wd"
-    }()
+    static var walletHistoryUrl: String = "\(webUrlHost)/wallet/record"
+    static var beanUrl: String = "\(webUrlHost)/wallet/wd"
     
-    static var orderQRShareUrl: String = {
-        "\(webUrlHost)/user/category"
-    }()
-    static var profileShareUrl: String = {
-        "\(webUrlHost)/user/profile"
-    }()
+    static var orderQRShareUrl: String = "\(webUrlHost)/user/category"
+    static var profileShareUrl: String = "\(webUrlHost)/user/profile"
     
-    static var joinUsUrl: String = {
-        "\(webUrlHost)/native/playmate/apply"
-    }()
+    static var joinUsUrl: String = "\(webUrlHost)/native/playmate/apply"
 }

+ 18 - 0
Lanu/Common/Extension/Date+Extension.swift

@@ -114,3 +114,21 @@ extension Date {
         return yearsAgo
     }
 }
+
+extension Date {
+    var inMinutes: Bool {
+        timeIntervalSince1970.inMinutes
+    }
+    
+    var inHour: Bool {
+        timeIntervalSince1970.inHour
+    }
+    
+    var inDay: Bool {
+        timeIntervalSince1970.inDay
+    }
+    
+    var isSameDay: Bool {
+        Calendar.current.isDate(Date(), inSameDayAs: self)
+    }
+}

+ 40 - 0
Lanu/Common/Extension/TimeInterval+Extension.swift

@@ -49,4 +49,44 @@ extension TimeInterval {
     var tencentIMTimeDesc: String {
         Date(timeIntervalSince1970: self).tencentIMTimeDesc
     }
+    
+    var relativeTimeText: String {
+        guard curTime > self else { return .init(key: "A00352") }
+        
+        let diff = Int(curTime - self)
+        if diff < 60 {
+            return .init(key: "A00352")
+        }
+        
+        let minute = diff / 60
+        if minute < 60 {
+            return minute == 1 ? .init(key: "A00353", minute) : .init(key: "A00354", minute)
+        }
+        
+        let hour = minute / 60
+        if hour < 24 {
+            return hour == 1 ? .init(key: "A00355", hour) : .init(key: "A00356", hour)
+        }
+        
+        let day = hour / 24
+        return day == 1 ? .init(key: "A00357", day) : .init(key: "A00358", day)
+    }
+}
+
+extension TimeInterval {
+    var inMinutes: Bool {
+        curTime - self < 60
+    }
+    
+    var inHour: Bool {
+        curTime - self < 3600
+    }
+    
+    var inDay: Bool {
+        curTime - self < 3600 * 24
+    }
+    
+    var isSameDay: Bool {
+        Calendar.current.isDate(Date(), inSameDayAs: Date(timeIntervalSince1970: self))
+    }
 }

+ 2 - 2
Lanu/Common/Extension/UIImage+Extension.swift

@@ -26,7 +26,7 @@ extension UIImage {
     func saveToLibrary(completion: ((Bool, Error?) -> Void)?) {
         // 步骤1:检查并请求相册权限
         PHPhotoLibrary.requestAuthorization { status in
-            DispatchQueue.main.async { // 回调默认在子线程,切回主线程处理UI
+            runOnMain { // 回调默认在子线程,切回主线程处理UI
                 switch status {
                 case .authorized, .limited: // 授权(含iOS 14+有限授权)
                     // 步骤2:异步保存图片到相册
@@ -34,7 +34,7 @@ extension UIImage {
                         // 创建保存请求
                         PHAssetChangeRequest.creationRequestForAsset(from: self)
                     }) { success, error in
-                        DispatchQueue.main.async {
+                        runOnMain {
                             completion?(success, error)
                         }
                     }

+ 2 - 0
Lanu/Common/Storage/LNUserDefaultsKey.swift

@@ -26,4 +26,6 @@ enum LNUserDefaultsKey: String {
     case reportLocationTime
     
     case joinedRoomId
+    
+    case remainExchange
 }

+ 3 - 0
Lanu/Common/Theme/UIFont+Theme.swift

@@ -44,4 +44,7 @@ extension UIFont {
     static let body_xs: UIFont = {
         .systemFont(ofSize: 11)
     }()
+    static let body_xxs: UIFont = {
+        .systemFont(ofSize: 8)
+    }()
 }

+ 54 - 0
Lanu/Common/Theme/UIView+Theme.swift

@@ -0,0 +1,54 @@
+//
+//  UIView+Theme.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/4/1.
+//
+
+import Foundation
+import UIKit
+import Combine
+
+
+enum LNViewGradientDirection {
+    case horizontalLTR
+    case horizontalRTL
+    case verticalUTD
+    case verticalDTU
+    case custom(start: CGPoint, endPoint: CGPoint)
+}
+
+extension UIView {
+    static func gradientView(_ colors: [UIColor], _ direction: LNViewGradientDirection) -> UIView {
+        let view = UIView()
+        view.isUserInteractionEnabled = false
+        
+        let gradientLayer = CAGradientLayer()
+        gradientLayer.colors = colors.map({ $0.cgColor })
+        switch direction {
+        case .horizontalLTR:
+            gradientLayer.startPoint = .zero
+            gradientLayer.endPoint = .init(x: 1, y: 0)
+        case .horizontalRTL:
+            gradientLayer.startPoint = .init(x: 1, y: 0)
+            gradientLayer.endPoint = .zero
+        case .verticalUTD:
+            gradientLayer.startPoint = .zero
+            gradientLayer.endPoint = .init(x: 0, y: 1)
+        case .verticalDTU:
+            gradientLayer.startPoint = .init(x: 0, y: 1)
+            gradientLayer.endPoint = .zero
+        case .custom(let start, let endPoint):
+            gradientLayer.startPoint = start
+            gradientLayer.endPoint = endPoint
+        }
+        view.layer.addSublayer(gradientLayer)
+        view.publisher(for: \.bounds).removeDuplicates().sink
+        { [weak gradientLayer] newValue in
+            guard let gradientLayer else { return }
+            gradientLayer.frame = newValue
+        }.store(in: &view.cancellables)
+        
+        return view
+    }
+}

+ 1 - 1
Lanu/Common/Views/ImagePreview/LNImagePreviewCell.swift

@@ -10,7 +10,7 @@ import UIKit
 import SnapKit
 
 
-protocol LNImagePreviewCellDelegate: NSObject {
+protocol LNImagePreviewCellDelegate: AnyObject {
     func onImagePreviewCellDragToDismiss(cell: LNImagePreviewCell)
 }
 

+ 1 - 1
Lanu/Common/Views/ImagePreview/LNImagePreviewController.swift

@@ -38,7 +38,7 @@ class LNImagePreviewController: LNViewController {
         curIndex = targetIndex
         
         collectionView?.reloadData()
-        DispatchQueue.main.async { [weak self] in
+        runOnMain { [weak self] in
             guard let self else { return }
             collectionView?.scrollToItem(
                 at: .init(row: curIndex, section: 0),

+ 1 - 1
Lanu/Common/Views/ImageUpload/LNImageUploadView.swift

@@ -10,7 +10,7 @@ import UIKit
 import SnapKit
 
 
-protocol LNImageUploadViewDelegate: NSObject {
+protocol LNImageUploadViewDelegate: AnyObject {
     func onImageUploadView(view: LNImageUploadView, didUploadImage url: String)
     func onImageUploadViewStartUpload(view: LNImageUploadView)
     func onImageUploadViewDidClickDelete(view: LNImageUploadView)

+ 1 - 1
Lanu/Common/Views/ImageUpload/LNMultiImagesUploadView.swift

@@ -9,7 +9,7 @@ import UIKit
 import SnapKit
 
 
-protocol LNMultiImagesUploadViewDelegate: NSObject {
+protocol LNMultiImagesUploadViewDelegate: AnyObject {
     func onMultiImagesUploadView(view: LNMultiImagesUploadView, imageUrlsChanged urls: [String])
 }
 

+ 1 - 1
Lanu/Common/Views/LNCaptchaInputView.swift

@@ -10,7 +10,7 @@ import UIKit
 import SnapKit
 
 
-protocol LNCaptchaInputViewDelegate: NSObject {
+protocol LNCaptchaInputViewDelegate: AnyObject {
     func onCaptchaInputChange(view: LNCaptchaInputView)
 }
 

+ 23 - 3
Lanu/Common/Views/LNNestedScrollView.swift

@@ -71,7 +71,7 @@ extension UIScrollView {
 }
 
 
-protocol LNNestedScrollViewDelegate: NSObject {
+protocol LNNestedScrollViewDelegate: AnyObject {
     func listViewDidScroll(_ scrollView: UIScrollView) -> Bool
 }
 
@@ -132,7 +132,17 @@ class LNNestedScrollView: UIScrollView, UIScrollViewDelegate, UIGestureRecognize
     }
     
     public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
-        gestureRecognizer.isKind(of: UIPanGestureRecognizer.self) && otherGestureRecognizer.isKind(of: UIPanGestureRecognizer.self)
+        // 禁止水平滚动的 ScrollView 手势同时响应
+        if let scrollView = gestureRecognizer.view as? UIScrollView,
+           scrollView.contentSize.height == scrollView.bounds.height {
+            return false
+        }
+        if let scrollView = otherGestureRecognizer.view as? UIScrollView,
+           scrollView.contentSize.height == scrollView.bounds.height {
+            return false
+        }
+        
+        return gestureRecognizer.isKind(of: UIPanGestureRecognizer.self) && otherGestureRecognizer.isKind(of: UIPanGestureRecognizer.self)
     }
 }
 
@@ -194,6 +204,16 @@ class LNNestedTableView: UITableView, UITableViewDelegate, UIGestureRecognizerDe
     }
     
     public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
-        gestureRecognizer.isKind(of: UIPanGestureRecognizer.self) && otherGestureRecognizer.isKind(of: UIPanGestureRecognizer.self)
+        // 禁止水平滚动的 ScrollView 手势同时响应
+        if let scrollView = gestureRecognizer.view as? UIScrollView,
+           scrollView.contentSize.height == scrollView.bounds.height {
+            return false
+        }
+        if let scrollView = otherGestureRecognizer.view as? UIScrollView,
+           scrollView.contentSize.height == scrollView.bounds.height {
+            return false
+        }
+        
+        return gestureRecognizer.isKind(of: UIPanGestureRecognizer.self) && otherGestureRecognizer.isKind(of: UIPanGestureRecognizer.self)
     }
 }

+ 1 - 1
Lanu/Common/Views/LNSortedEditView.swift

@@ -25,7 +25,7 @@ enum LNSortedType: Int, CaseIterable {
 }
 
 
-protocol LNSortedEditViewDelegate: NSObject {
+protocol LNSortedEditViewDelegate: AnyObject {
     func sortedEditView(view: LNSortedEditView, sortedTypeChanged: LNSortedType)
 }
 

+ 1 - 1
Lanu/Common/Views/LNTextField.swift

@@ -9,7 +9,7 @@ import Foundation
 import UIKit
 
 
-protocol LNTextFieldDelegate: NSObject {
+protocol LNTextFieldDelegate: AnyObject {
     func onDeleteBackward(_ textField: UITextField, oldText: String?)
 }
 

+ 4 - 4
Lanu/Common/Views/LNVideoPlayerView.swift

@@ -12,7 +12,7 @@ import SnapKit
 import Combine
 
 
-protocol LNVideoPlayerViewDelegate: NSObject {
+protocol LNVideoPlayerViewDelegate: AnyObject {
     func onVideoDidLoad(view: LNVideoPlayerView)
     func onVideoDidStart(view: LNVideoPlayerView)
     func onVideoDidStop(view: LNVideoPlayerView)
@@ -100,11 +100,11 @@ class LNVideoPlayerView: UIView {
                 do {
                     let cgImage = try imageGenerator.copyCGImage(at: time, actualTime: nil)
                     let thumbnailImage = UIImage(cgImage: cgImage)
-                    DispatchQueue.main.async {
+                    runOnMain {
                         self.coverImageView.image = thumbnailImage
                     }
                 } catch {
-                    DispatchQueue.main.async {
+                    runOnMain {
                         self.coverImageView.image = UIImage(systemName: "film")
                     }
                 }
@@ -129,7 +129,7 @@ class LNVideoPlayerView: UIView {
                 videoSize = CGSize(width: naturalSize.height, height: naturalSize.width)
             }
             
-            DispatchQueue.main.async { [weak self] in
+            runOnMain { [weak self] in
                 guard let self else { return }
                 guard curSource == url else { return }
                 delegate?.onVideoDidLoad(view: self)

+ 1 - 1
Lanu/Common/Views/LNVoiceEditView.swift

@@ -10,7 +10,7 @@ import UIKit
 import SnapKit
 
 
-protocol LNVoiceEditViewDelegate: NSObject {
+protocol LNVoiceEditViewDelegate: AnyObject {
     func onVoiceChanged()
 }
 

+ 37 - 13
Lanu/Common/Views/Menu/LNCommonAlertView.swift

@@ -26,14 +26,17 @@ class LNCommonAlertView: UIView {
     private let background = UIView()
     private let container = UIView()
     
-    private let messageViews = UIStackView()
-    private let buttonViews = UIStackView()
-    
     private let miniScale = 0.01
     private let animateDuration = 0.15
     
+    let textViews = UIStackView()
     let titleLabel = UILabel()
+    let messageView = UIStackView()
     let messageLabel = UILabel()
+    let subMessageLabel = UILabel()
+    
+    let buttonViews = UIStackView()
+    
     var touchOutsideCancel = true
     
     override init(frame: CGRect) {
@@ -73,12 +76,23 @@ class LNCommonAlertView: UIView {
 
 extension LNCommonAlertView {
     func popup(_ holder: UIView? = nil) {
-        guard let holder = holder ?? UIView.appKeyWindow else { return }
-        holder.addSubview(self)
-        frame = holder.bounds
+        let parentView: UIView? = if let window = holder as? UIWindow {
+            window
+        } else if let view = holder?.viewController?.view {
+            view
+        } else if let window = UIView.appKeyWindow {
+            window
+        } else {
+            nil
+        }
+        guard let parentView else { return }
+        parentView.addSubview(self)
+        frame = parentView.bounds
         
         titleLabel.isHidden = titleLabel.text?.isEmpty != false
         messageLabel.isHidden = messageLabel.text?.isEmpty != false
+        subMessageLabel.isHidden = subMessageLabel.text?.isEmpty != false
+        messageView.isHidden = messageLabel.isHidden && subMessageLabel.isHidden
         
         layoutIfNeeded()
         
@@ -139,10 +153,10 @@ extension LNCommonAlertView {
             make.width.height.equalTo(24)
         }
         
-        messageViews.axis = .vertical
-        messageViews.spacing = 10
-        container.addSubview(messageViews)
-        messageViews.snp.makeConstraints { make in
+        textViews.axis = .vertical
+        textViews.spacing = 10
+        container.addSubview(textViews)
+        textViews.snp.makeConstraints { make in
             make.horizontalEdges.equalToSuperview().inset(24)
             make.top.equalToSuperview().offset(30)
         }
@@ -151,20 +165,30 @@ extension LNCommonAlertView {
         titleLabel.textColor = .text_4
         titleLabel.textAlignment = .center
         titleLabel.numberOfLines = 0
-        messageViews.addArrangedSubview(titleLabel)
+        textViews.addArrangedSubview(titleLabel)
+        
+        messageView.axis = .vertical
+        messageView.spacing = 6
+        textViews.addArrangedSubview(messageView)
         
         messageLabel.font = .body_m
         messageLabel.textColor = .text_4
         messageLabel.textAlignment = .center
         messageLabel.numberOfLines = 0
-        messageViews.addArrangedSubview(messageLabel)
+        messageView.addArrangedSubview(messageLabel)
+        
+        subMessageLabel.font = .body_xs
+        subMessageLabel.textColor = .text_4
+        subMessageLabel.textAlignment = .center
+        subMessageLabel.numberOfLines = 0
+        messageView.addArrangedSubview(subMessageLabel)
         
         buttonViews.axis = .vertical
         buttonViews.spacing = 16
         container.addSubview(buttonViews)
         buttonViews.snp.makeConstraints { make in
             make.horizontalEdges.equalToSuperview().inset(50)
-            make.top.equalTo(messageViews.snp.bottom).offset(16)
+            make.top.equalTo(textViews.snp.bottom).offset(16)
             make.bottom.equalToSuperview().offset(-30)
         }
     }

+ 1 - 1
Lanu/Common/Views/Selection/LNHourRangePickerPanel.swift

@@ -188,7 +188,7 @@ extension LNHourRangePickerPanel {
             make.top.equalTo(pickerView.snp.bottom).offset(4)
         }
         
-        DispatchQueue.main.async { [weak self] in
+        runOnMain { [weak self] in
             guard let self else { return }
             fromPicker.subviews.forEach {
                 if $0.subviews.isEmpty {

+ 1 - 1
Lanu/Common/Views/StarScore/LNFiveStarScoreView.swift

@@ -10,7 +10,7 @@ import UIKit
 import SnapKit
 
 
-protocol LNFiveStarScoreViewDelegate: NSObject {
+protocol LNFiveStarScoreViewDelegate: AnyObject {
     func onFiveStarScoreView(view: LNFiveStarScoreView, scoreChanged newScore: Double)
 }
 

+ 1 - 1
Lanu/Common/Views/VideoPreview/LNVideoPreviewCell.swift

@@ -10,7 +10,7 @@ import UIKit
 import SnapKit
 
 
-protocol LNVideoPreviewCellDelegate: NSObject {
+protocol LNVideoPreviewCellDelegate: AnyObject {
     func onVideoPreviewCellDragToDismiss(cell: LNVideoPreviewCell)
 }
 

+ 1 - 1
Lanu/Common/Views/VideoPreview/LNVideoPreviewController.swift

@@ -38,7 +38,7 @@ class LNVideoPreviewController: LNViewController {
         curIndex = targetIndex
         
         collectionView?.reloadData()
-        DispatchQueue.main.async { [weak self] in
+        runOnMain { [weak self] in
             guard let self else { return }
             collectionView?.scrollToItem(
                 at: .init(row: curIndex, section: 0),

+ 3 - 3
Lanu/Common/Views/VideoUpload/LNVideoUploadView.swift

@@ -11,7 +11,7 @@ import SnapKit
 import AVFoundation
 
 
-protocol LNVideoUploadViewDelegate: NSObject {
+protocol LNVideoUploadViewDelegate: AnyObject {
     func onVideoUploadView(view: LNVideoUploadView, didUploadVideo url: String, imageUrl: String)
     func onVideoUploadViewUploadFailed(view: LNVideoUploadView)
     func onVideoUploadViewStartUpload(view: LNVideoUploadView)
@@ -74,13 +74,13 @@ class LNVideoUploadView: UIImageView {
                 let cgImage = try imageGenerator.copyCGImage(at: time, actualTime: nil)
                 let thumbnailImage = UIImage(cgImage: cgImage)
                 // 更新UI(必须在主线程)
-                DispatchQueue.main.async {
+                runOnMain {
                     self.image = thumbnailImage
                 }
             } catch {
                 print("生成视频缩略图失败:\(error.localizedDescription)")
                 // 生成失败时显示占位图
-                DispatchQueue.main.async {
+                runOnMain {
                     self.image = UIImage(systemName: "film")
                 }
             }

+ 1 - 1
Lanu/Common/Voice/LNVoiceRecorder.swift

@@ -67,7 +67,7 @@ class LNVoiceRecorder {
         curState = .recording
         curTaskId = "\(curTime)"
         
-        DispatchQueue.main.async { [weak self] in
+        runOnMain { [weak self] in
             guard let self else { return }
             notifyTaskStart()
         }

+ 1 - 1
Lanu/Common/Voice/LNVoiceResourceManager.swift

@@ -131,7 +131,7 @@ class LNVoiceResourceManager {
             var error: NSError?
             let status = asset.statusOfValue(forKey: "duration", error: &error)
             
-            DispatchQueue.main.async { [weak self] in
+            runOnMain { [weak self] in
                 guard let self else { return }
                 switch status {
                 case .loaded:

+ 6 - 6
Lanu/GoogleService-Info-Release.plist

@@ -21,16 +21,16 @@
 	<key>STORAGE_BUCKET</key>
 	<string>gami-74c1a.firebasestorage.app</string>
 	<key>IS_ADS_ENABLED</key>
-	<false></false>
+	<false/>
 	<key>IS_ANALYTICS_ENABLED</key>
-	<false></false>
+	<true/>
 	<key>IS_APPINVITE_ENABLED</key>
-	<true></true>
+	<true/>
 	<key>IS_GCM_ENABLED</key>
-	<true></true>
+	<true/>
 	<key>IS_SIGNIN_ENABLED</key>
-	<true></true>
+	<true/>
 	<key>GOOGLE_APP_ID</key>
 	<string>1:955524882346:ios:ccfadf305dea34a3dc15d3</string>
 </dict>
-</plist>
+</plist>

+ 282 - 6
Lanu/Localizable.xcstrings

@@ -588,7 +588,7 @@
         "id" : {
           "stringUnit" : {
             "state" : "translated",
-            "value" : "Berhenti Mengikuti"
+            "value" : "Berhenti ikuti"
           }
         },
         "zh-Hans" : {
@@ -6344,7 +6344,7 @@
         "zh-Hans" : {
           "stringUnit" : {
             "state" : "translated",
-            "value" : "豆"
+            "value" : "豆"
           }
         }
       }
@@ -7620,19 +7620,19 @@
         "en" : {
           "stringUnit" : {
             "state" : "translated",
-            "value" : "Applying to join mic"
+            "value" : "%d people applying to join mic"
           }
         },
         "id" : {
           "stringUnit" : {
             "state" : "translated",
-            "value" : "Sedang mengajukan bergabung mic"
+            "value" : "%d orang mengajukan mic"
           }
         },
         "zh-Hans" : {
           "stringUnit" : {
             "state" : "translated",
-            "value" : "在申请上麦"
+            "value" : "%d 人在申请上麦"
           }
         }
       }
@@ -8040,7 +8040,7 @@
         "id" : {
           "stringUnit" : {
             "state" : "translated",
-            "value" : "Di Ruangan"
+            "value" : "Di Kamar"
           }
         },
         "zh-Hans" : {
@@ -8902,6 +8902,52 @@
         }
       }
     },
+    "A00390" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Details"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Rincian"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "详情"
+          }
+        }
+      }
+    },
+    "A00391" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Please select a gift recipient first."
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Silakan pilih penerima hadiah terlebih dahulu."
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "请先选择送礼对象"
+          }
+        }
+      }
+    },
     "B00001" : {
       "extractionState" : "manual",
       "localizations" : {
@@ -11846,6 +11892,236 @@
         }
       }
     },
+    "B00129" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "To:"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Untuk:"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "送给:"
+          }
+        }
+      }
+    },
+    "B00130" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "%1$@ sent %2$@ {icon} x%3$d"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "%1$@ memberi %2$@ {icon} x%3$d"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "%1$@ 送 %2$@ {icon} x%3$d"
+          }
+        }
+      }
+    },
+    "B00131" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Insufficient %@, go to recharge now?"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "%@ tidak cukup, apakah ingin isi ulang sekarang?"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "%@不足,是否前往充值?"
+          }
+        }
+      }
+    },
+    "B00132" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Insufficient %@?"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "%@ tidak cukup?"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "%@不足?"
+          }
+        }
+      }
+    },
+    "B00133" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Available %1$@: {icon}%2$@"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "%1$@ Tersedia: {icon}%2$@"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "可用 %1$@:{icon}%2$@"
+          }
+        }
+      }
+    },
+    "B00134" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Don't remind me today"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Jangan ingatkan hari ini"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "今日不再提醒"
+          }
+        }
+      }
+    },
+    "B00135" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "diamond"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "berlian"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "钻石"
+          }
+        }
+      }
+    },
+    "B00136" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "coin"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "coin"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "金币"
+          }
+        }
+      }
+    },
+    "B00137" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "beans"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "kacang"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "金豆"
+          }
+        }
+      }
+    },
+    "B00138" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Rate: {icon1}%1$@ = {icon2}%2$@"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Tingkat: {icon1}%1$@ = {icon2}%2$@"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "比例: {icon1}%1$@ = {icon2}%2$@"
+          }
+        }
+      }
+    },
     "C00001" : {
       "extractionState" : "manual",
       "localizations" : {

+ 1 - 1
Lanu/Manager/Account/LNAccountManager.swift

@@ -248,7 +248,7 @@ extension LNAccountManager {
     }
     
     private func startCaptchaTimer() {
-        DispatchQueue.main.async { [weak self] in
+        runOnMain { [weak self] in
             guard let self else { return }
             stopCaptchaTimer()
             

+ 2 - 2
Lanu/Manager/Deeplink/LNDeeplinkManager.swift

@@ -110,7 +110,7 @@ class LNDeeplinkManager {
             } else {
                 model = nil
             }
-            DispatchQueue.main.async {
+            runOnMain {
                 handler(model)
             }
         }
@@ -120,7 +120,7 @@ class LNDeeplinkManager {
     func register(url: String, handler: @escaping () -> Void) {
         lock.lock()
         routerMap[url] = { _ in
-            DispatchQueue.main.async {
+            runOnMain {
                 handler()
             }
         }

+ 183 - 0
Lanu/Manager/Gift/LNGiftManager.swift

@@ -0,0 +1,183 @@
+//
+//  LNGiftManager.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/3/24.
+//
+
+import Foundation
+
+
+private struct LNGiftCacheList: Codable {
+    var list: [LNGiftResource]
+    var version: String
+}
+
+
+class LNGiftManager {
+    static let shared = LNGiftManager()
+    
+    private let pageSize = 100
+    private let cacheURL = URL.cacheDir
+        .appendingPathComponent("Gift", isDirectory: true)
+        .appendingPathComponent("gift_resources.json")
+    
+    private var resourceMap: [String: LNGiftResource] = [:]
+    private var resourceVersion: String = ""
+    
+    private var isRefreshing = false
+    
+    private init() {
+        DispatchQueue.global().async { [weak self] in
+            guard let self else { return }
+            loadCache()
+        }
+        LNEventDeliver.addObserver(self)
+    }
+    
+    func resource(for id: String) -> LNGiftResource? {
+        resourceMap[id]
+    }
+    
+    func fetchGiftList(roomId: String, queue: DispatchQueue = .main, handler: @escaping ([LNGiftItemVO]?) -> Void) {
+        LNHttpManager.shared.loadGiftList(roomId: roomId) { [weak self] res, err in
+            guard let self else { return }
+            let list = res?.list.filter { self.resourceMap[$0.resId] != nil }
+            queue.asyncIfNotGlobal {
+                handler(list)
+            }
+            if let list, list.count != res?.list.count {
+                updateGiftResource()
+            }
+            if let err {
+                showToast(err.errorDesc)
+            }
+        }
+    }
+}
+
+extension LNGiftManager {
+    func updateGiftResource() {
+        guard !isRefreshing else { return }
+        
+        var changedGifts: [LNGiftResource] = []
+        var newVersion = ""
+        isRefreshing = true
+        
+        func _fetchResource(next: String) {
+            LNHttpManager.shared.loadResourceList(version: resourceVersion, size: pageSize, next: next) { [weak self] res, err in
+                guard let self else { return }
+                guard let res else {
+                    isRefreshing = false
+                    return
+                }
+                if !res.list.isEmpty {
+                    changedGifts.append(contentsOf: res.list)
+                }
+                if !res.version.isEmpty {
+                    newVersion = res.version
+                }
+                if res.list.isEmpty || res.next.isEmpty != false {
+                    mergeChangedResources(changedGifts, version: newVersion)
+                    isRefreshing = false
+                } else {
+                    _fetchResource(next: next)
+                }
+            }
+        }
+        _fetchResource(next: "")
+    }
+    
+    private func mergeChangedResources(_ changedList: [LNGiftResource], version: String) {
+        guard !changedList.isEmpty, !version.isEmpty else { return }
+        
+        var newMap = resourceMap
+        changedList.forEach { item in
+            guard !item.id.isEmpty else { return }
+            newMap[item.id] = item
+        }
+        
+        resourceMap = newMap
+        resourceVersion = version
+        
+        saveCache()
+    }
+}
+ 
+extension LNGiftManager {
+    private func loadCache() {
+        guard let data = try? Data(contentsOf: cacheURL) else { return }
+        
+        do {
+            let snapshot = try JSONDecoder().decode(LNGiftCacheList.self, from: data)
+            resourceVersion = snapshot.version
+            resourceMap = snapshot.list.reduce(into: [:]) { partialResult, item in
+                guard !item.id.isEmpty else { return }
+                partialResult[item.id] = item
+            }
+        } catch {
+            Log.e("load gift resource cache failed: \(error.localizedDescription)")
+        }
+    }
+    
+    private func saveCache() {
+        let snapshot = LNGiftCacheList(
+            list: Array(resourceMap.values),
+            version: resourceVersion
+        )
+        
+        do {
+            let folderURL = cacheURL.deletingLastPathComponent()
+            try FileManager.default.createDirectory(at: folderURL, withIntermediateDirectories: true, attributes: nil)
+            let data = try JSONEncoder().encode(snapshot)
+            try data.write(to: cacheURL, options: .atomic)
+        } catch {
+            Log.e("save gift resource cache failed: \(error.localizedDescription)")
+        }
+    }
+}
+
+extension LNGiftManager {
+    func sendGift(params: LNSendGiftParams, queue: DispatchQueue = .main,
+                  handler: @escaping (Bool) -> Void) {
+        let remain: TimeInterval = LNUserDefaults[.remainExchange, 0]
+        if remain.isSameDay {
+            params.seamlessRedeem = true
+        }
+        
+        LNHttpManager.shared.sendGift(params: params) { res, err in
+            queue.asyncIfNotGlobal {
+                handler(err == nil)
+            }
+            if let err {
+                showToast(err.errorDesc)
+                if case .serverError(let code, _) = err {
+                    runOnMain {
+                        if code == LNOrderErrorCode.NotEnoughMoney.rawValue {
+                            let panel = LNMoneyNotEnoughAlertView()
+                            panel.update(.diamond)
+                            panel.popup()
+                        } else if LNOrderErrorCode.NotEnoughMoneyButCanExchange.rawValue == code {
+                            let panel = LNMoneyNotEnoughAlertView()
+                            panel.update(.diamond, exchange: .coin)
+                            panel.exchangeHandler = {
+                                params.seamlessRedeem = true
+                                self.sendGift(params: params, queue: queue, handler: handler)
+                            }
+                            panel.popup()
+                        }
+                    }
+                }
+            }
+            if let res {
+                LNPurchaseManager.shared.updateWallet(diamond: res.diamond, coin: res.goldcoin)
+            }
+        }
+    }
+}
+
+extension LNGiftManager: LNAccountManagerNotify {
+    func onUserLogin() {
+        updateGiftResource()
+    }
+}

+ 46 - 0
Lanu/Manager/Gift/Network/LNGiftResponse.swift

@@ -0,0 +1,46 @@
+//
+//  LNGiftResponse.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/3/24.
+//
+
+import Foundation
+import AutoCodable
+
+@AutoCodable
+class LNGiftResource: Codable {
+    var id: String = ""
+    var name: String = ""
+    var icon: String = ""
+    var names: [String: String] = [:]
+    
+    var curName: String {
+        names[LNAppConfig.shared.curLang.languageCode] ?? name
+    }
+}
+
+@AutoCodable
+class LNGiftResourceListResponse: Decodable {
+    var list: [LNGiftResource] = []
+    var next: String = ""
+    var version: String = ""
+}
+
+@AutoCodable
+class LNGiftItemVO: Decodable {
+    var id: String = ""
+    var value: Double = 0
+    var resId: String = ""
+}
+
+@AutoCodable
+class LNGiftListResponse: Decodable {
+    var list: [LNGiftItemVO] = []
+}
+
+@AutoCodable
+class LNSendGiftResponse: Decodable {
+    var diamond: Double = 0
+    var goldcoin: Double = 0
+}

+ 55 - 0
Lanu/Manager/Gift/Network/LNHttpManager+Gift.swift

@@ -0,0 +1,55 @@
+//
+//  LNHttpManager+Gift.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/3/24.
+//
+
+import Foundation
+
+private let kNetPath_Gift_Resource_List = "/gift/resource/list"
+private let kNetPath_Gift_List = "/list/gift/list"
+private let kNetPath_Gift_Send = "/list/gift/send"
+
+
+class LNSendGiftParams {
+    var roomId: String = ""
+    var giftId: String = ""
+    var userIds: [String] = []
+    var quantity: Int = 0
+    var seamlessRedeem: Bool = false
+    
+    fileprivate var toParams: [String: Any] {
+        [
+            "roomId": roomId,
+            "giftId": giftId,
+            "userIds": userIds,
+            "quantity": quantity,
+            "seamlessRedeem": seamlessRedeem
+        ]
+    }
+}
+
+
+extension LNHttpManager {
+    func loadResourceList(version: String, size: Int, next: String,
+                          completion: @escaping (LNGiftResourceListResponse?, LNHttpError?) -> Void) {
+        post(path: kNetPath_Gift_Resource_List, params: [
+            "page": [
+                "size": size,
+                "next": next
+            ],
+            "version": version
+        ], completion: completion)
+    }
+    
+    func loadGiftList(roomId: String, completion: @escaping (LNGiftListResponse?, LNHttpError?) -> Void) {
+        post(path: kNetPath_Gift_List, params: [
+            "roomId": roomId,
+        ], completion: completion)
+    }
+    
+    func sendGift(params: LNSendGiftParams, completion: @escaping (LNSendGiftResponse?, LNHttpError?) -> Void) {
+        post(path: kNetPath_Gift_Send, params: params.toParams, completion: completion)
+    }
+}

+ 1 - 1
Lanu/Manager/IM/LNIMManager.swift

@@ -439,7 +439,7 @@ extension LNIMManager: TUICallObserver {
             return
         }
         guard LNRoomManager.shared.curRoom == nil else {
-            // 在麦上,直接拒绝
+            // 在直播间,直接拒绝
             showToast(.init(key: "A00388"))
             rejectVoiceCall()
             return

+ 5 - 5
Lanu/Manager/Network/Download/LNFileDownloader.swift

@@ -111,7 +111,7 @@ class LNFileDownloader: NSObject {
             removeTask(urlString: urlString)
             
             let handlers = task.completionHandler
-            DispatchQueue.main.async {
+            runOnMain {
                 handlers.forEach { $0(.failure(LNFileDownloadError.beCancelled)) }
             }
         }
@@ -173,7 +173,7 @@ extension LNFileDownloader: URLSessionDownloadDelegate {
         
         let progress = totalBytesExpectedToWrite > 0 ? Float(totalBytesWritten) / Float(totalBytesExpectedToWrite) : 0.0
         let handlers = taskModel.progressHandler
-        DispatchQueue.main.async {
+        runOnMain {
             handlers.forEach { $0(progress) }
         }
     }
@@ -211,13 +211,13 @@ extension LNFileDownloader: URLSessionDownloadDelegate {
             
             // 回调成功结果
             let handlers = taskModel.completionHandler
-            DispatchQueue.main.async {
+            runOnMain {
                 handlers.forEach { $0(.success(destinationPath)) }
             }
         } catch {
             // 回调文件移动失败
             let handlers = taskModel.completionHandler
-            DispatchQueue.main.async {
+            runOnMain {
                 handlers.forEach { $0(.failure(LNFileDownloadError.fileMoveFailed)) }
             }
         }
@@ -245,7 +245,7 @@ extension LNFileDownloader: URLSessionTaskDelegate {
             }
             // 其他错误:回调网络错误
             let handlers = taskModel.completionHandler
-            DispatchQueue.main.async {
+            runOnMain {
                 handlers.forEach { $0(.failure(LNFileDownloadError.networkError(error))) }
             }
         }

+ 3 - 3
Lanu/Manager/Network/Upload/LNFileUploader.swift

@@ -111,7 +111,7 @@ class LNFileUploader: NSObject {
         LNHttpManager.shared.getUploadOssUrl(type: type, suffix: suffix) { [weak self] res, err in
             guard let self else { return }
             guard err == nil, let res, let url = URL(string: res.preSignUrl) else {
-                DispatchQueue.main.async {
+                runOnMain {
                     completionHandler?(nil, err?.errorDesc ?? LNHttpError.invalidResponse.errorDesc)
                 }
                 return
@@ -164,7 +164,7 @@ extension LNFileUploader: URLSessionTaskDelegate, URLSessionDataDelegate {
         guard let progressHandler = uploadTasks.first(where: { $0.value.task == task })?.value.progress else { return }
         guard totalBytesExpectedToSend > 0 else { return }
         let progress = Float(totalBytesSent) / Float(totalBytesExpectedToSend)
-        DispatchQueue.main.async {
+        runOnMain {
             progressHandler(progress)
         }
     }
@@ -177,7 +177,7 @@ extension LNFileUploader: URLSessionTaskDelegate, URLSessionDataDelegate {
         uploadTasks.removeValue(forKey: uploadTask.id)
         guard let completionHandler = uploadTask.completion else { return }
         
-        DispatchQueue.main.async {
+        runOnMain {
             if let error = error {
                 if (error as NSError).code == NSURLErrorCancelled {
                     completionHandler(nil, .init(key: "B00015"))

+ 2 - 1
Lanu/Manager/Order/LNOrderManager.swift

@@ -19,6 +19,7 @@ extension LNOrderManagerNotify {
 
 enum LNOrderErrorCode: Int {
     case NotEnoughMoney = 100018
+    case NotEnoughMoneyButCanExchange = 50002
 }
 
 protocol LNOrderProtocol {
@@ -154,7 +155,7 @@ extension LNOrderManager {
             }
             
             if case .serverError(let code, let err) = err {
-                DispatchQueue.main.async {
+                runOnMain {
                     if code == LNOrderErrorCode.NotEnoughMoney.rawValue {
                         let panel = LNPurchasePanel()
                         panel.update(.coin)

+ 1 - 1
Lanu/Manager/Profile/LNProfileManager.swift

@@ -292,7 +292,7 @@ extension LNProfileManager {
     }
     
     private func startCaptchaTimer() {
-        DispatchQueue.main.async { [weak self] in
+        runOnMain { [weak self] in
             guard let self else { return }
             stopCaptchaTimer()
             

+ 10 - 0
Lanu/Manager/Purchase/LNPurchaseManager.swift

@@ -86,6 +86,16 @@ class LNPurchaseManager {
         }
     }
     
+    func updateWallet(diamond: Double? = nil, coin: Double? = nil) {
+        if let diamond {
+            myWalletInfo.diamond = diamond
+        }
+        if let coin {
+            myWalletInfo.coin = coin
+        }
+        notifyWalletInfoChanged()
+    }
+    
     func loadGoodsList(currencyType: LNCurrencyType,
                        queue: DispatchQueue = .main,
                        handler: @escaping ([LNPurchaseGoodsVO]?) -> Void)

+ 24 - 0
Lanu/Manager/Purchase/Network/LNPurchaseResponse.swift

@@ -14,6 +14,30 @@ enum LNCurrencyType: Int, Decodable {
     case coin = 0
     case diamond = 1
     case bean = 2
+    
+    func name(lowcase: Bool = false) -> String {
+        switch self {
+        case .coin: lowcase ? .init(key: "B00136") : .init(key: "A00216")
+        case .diamond: lowcase ? .init(key: "B00135") : .init(key: "A00217")
+        case .bean: lowcase ? .init(key: "B00137") : .init(key: "A00277")
+        }
+    }
+    
+    var icon: UIImage {
+        switch self {
+        case .coin: .icCoin42
+        case .diamond: .icDiamond42
+        case .bean: .icBean
+        }
+    }
+    
+    var currentValue: Double {
+        switch self {
+        case .coin: myWalletInfo.coin
+        case .diamond: myWalletInfo.diamond
+        case .bean: myWalletInfo.bean
+        }
+    }
 }
 
 enum LNPurchasePlatform: Int, Decodable {

+ 19 - 2
Lanu/Manager/Room/LNRoomManager.swift

@@ -20,6 +20,18 @@ extension LNRoomManagerNotify {
 }
 
 
+func showTencentError(_ error: ErrorInfo) {
+    let items = error.message.components(separatedBy: ",")
+    for item in items {
+        let parts = item.components(separatedBy: ":")
+        if parts.first?.trimmingCharacters(in: .whitespaces) == "error_message" {
+            showToast(parts.dropFirst().joined(separator: ":").trimmingCharacters(in: .whitespaces))
+            return
+        }
+    }
+    showToast(error.message)
+}
+
 class LNRoomManager {
     static let shared = LNRoomManager()
     static let RoomNameMinInput = 2
@@ -87,7 +99,8 @@ extension LNRoomManager {
                     LNUserDefaults[.joinedRoomId] = roomId
                 case .failure(let errorInfo):
                     handler(nil)
-                    showToast(errorInfo.message)
+                    showTencentError(errorInfo)
+                    self.reportRoomUnavailable(id: roomId, code: errorInfo.code, reason: errorInfo.message)
                 }
             }
         }
@@ -101,7 +114,7 @@ extension LNRoomManager {
                     handler(true)
                 case .failure(let errorInfo):
                     handler(false)
-                    showToast(errorInfo.message)
+                    showTencentError(errorInfo)
                 }
             }
         }
@@ -171,6 +184,10 @@ extension LNRoomManager {
             }
         }
     }
+    
+    func reportRoomUnavailable(id: String, code: Int, reason: String) {
+        LNHttpManager.shared.reportRoomUnavailable(id: id, code: "\(code)", reason: reason) { _ in }
+    }
 }
 
 // MARK: 用户

+ 11 - 0
Lanu/Manager/Room/Network/LNHttpManager+Room.swift

@@ -31,6 +31,8 @@ private let kNetPath_Room_Cur = "/live/view/watching"
 private let kNetPath_Room_Search = "/live/room/search"
 private let kNetPath_Room_List = "/live/room/list"
 
+private let kNetPath_Room_Unavailable = "/live/room/unavailable"
+
 
 // MARK: 房间管理
 extension LNHttpManager {
@@ -195,4 +197,13 @@ extension LNHttpManager {
             ]
         ], completion: completion)
     }
+    
+    func reportRoomUnavailable(id: String, code: String, reason: String, completion: @escaping (LNHttpError?) -> Void) {
+        post(path: kNetPath_Room_Unavailable, params: [
+            "id": id,
+            "supplier": 1,
+            "errorCode": code,
+            "errorReson": reason
+        ], completion: completion)
+    }
 }

+ 0 - 25
Lanu/Manager/Room/Network/LNRoomResponse.swift

@@ -64,31 +64,6 @@ class LNRoomMicApplyPageVO: Decodable {
     var user: LNRoomUserVO = LNRoomUserVO()
     
     var hasAccept = false
-    
-    var relativeTimeText: String {
-        let time = applyTime / 1_000 - Int64(curTime)
-        guard time > 0 else { return .init(key: "A00352") }
-        
-        let now = Int64(Date().timeIntervalSince1970 * 1_000)
-        let diff = max(0, now - time) / 1_000
-        
-        if diff < 60 {
-            return .init(key: "A00352")
-        }
-        
-        let minute = diff / 60
-        if minute < 60 {
-            return minute == 1 ? .init(key: "A00353", minute) : .init(key: "A00354", minute)
-        }
-        
-        let hour = minute / 60
-        if hour < 24 {
-            return hour == 1 ? .init(key: "A00355", hour) : .init(key: "A00356", hour)
-        }
-        
-        let day = hour / 24
-        return day == 1 ? .init(key: "A00357", day) : .init(key: "A00358", day)
-    }
 }
 
 @AutoCodable

+ 4 - 4
Lanu/Views/Game/Category/LNGameCategoryListView.swift

@@ -10,7 +10,7 @@ import UIKit
 import SnapKit
 
 
-protocol LNGameCategoryListViewDelegate: NSObject {
+protocol LNGameCategoryListViewDelegate: AnyObject {
     func onCategoryListView(view: LNGameCategoryListView, didScrollTo category: LNGameTypeItemVO)
     func onCategoryListView(view: LNGameCategoryListView, didSelect topCategory: LNGameTypeItemVO, category: LNGameCategoryItemVO)
 }
@@ -40,7 +40,7 @@ class LNGameCategoryListView: UIView {
         self.categories = categories
         collectionView.reloadData()
         
-        DispatchQueue.main.async { [weak self] in
+        runOnMain { [weak self] in
             guard let self else { return }
             fixBottomSpace()
         }
@@ -50,7 +50,7 @@ class LNGameCategoryListView: UIView {
         guard let index = categories.firstIndex(where: { $0.code == category.code }) else {
             return
         }
-        DispatchQueue.main.async { [weak self] in
+        runOnMain { [weak self] in
             guard let self else { return }
             if let headerAttributes = collectionView.layoutAttributesForSupplementaryElement(
                 ofKind: UICollectionView.elementKindSectionHeader,
@@ -69,7 +69,7 @@ class LNGameCategoryListView: UIView {
         let width = (bounds.width - collectionViewLayout.minimumInteritemSpacing) / CGFloat(columns) - collectionViewLayout.minimumInteritemSpacing
         collectionViewLayout.itemSize = .init(width: width, height: 68)
         
-        DispatchQueue.main.async { [weak self] in
+        runOnMain { [weak self] in
             guard let self else { return }
             fixBottomSpace()
         }

+ 1 - 1
Lanu/Views/Game/Category/LNGameCategoryTabView.swift

@@ -10,7 +10,7 @@ import UIKit
 import SnapKit
 
 
-protocol LNGameCategoryTabViewDelegate: NSObject {
+protocol LNGameCategoryTabViewDelegate: AnyObject {
     func onGameCategoryTabView(view: LNGameCategoryTabView, didSelect category: LNGameTypeItemVO)
 }
 

+ 1 - 1
Lanu/Views/Game/Join/Input/BaseInfo/LNJoinUsInputInfoView.swift

@@ -11,7 +11,7 @@ import SnapKit
 import Combine
 
 
-protocol LNJoinUsInputInfoViewDelegate: NSObject {
+protocol LNJoinUsInputInfoViewDelegate: AnyObject {
     func joinUsInputInfoViewDidFinish(view: LNJoinUsInputInfoView)
 }
 

+ 1 - 1
Lanu/Views/Game/Join/Input/BindPhone/LNJoinUsInputCaptchaView.swift

@@ -10,7 +10,7 @@ import UIKit
 import SnapKit
 
 
-protocol LNJoinUsInputCaptchaViewDelegate: NSObject {
+protocol LNJoinUsInputCaptchaViewDelegate: AnyObject {
     func joinUsInputCaptchaViewDidFinish(view: LNJoinUsInputCaptchaView)
 }
 

+ 1 - 1
Lanu/Views/Game/Join/Input/BindPhone/LNJoinUsInputPhoneView.swift

@@ -10,7 +10,7 @@ import UIKit
 import SnapKit
 
 
-protocol LNJoinUsInputPhoneViewDelegate: NSObject {
+protocol LNJoinUsInputPhoneViewDelegate: AnyObject {
     func joinUsInputPhoneView(view: LNJoinUsInputPhoneView, didFinished code: String, phone: String)
 }
 

+ 1 - 1
Lanu/Views/Game/Join/Input/SkillInfo/LNJoinUsSelectSkillView.swift

@@ -10,7 +10,7 @@ import UIKit
 import SnapKit
 
 
-protocol LNJoinUsSelectSkillViewDelegate: NSObject {
+protocol LNJoinUsSelectSkillViewDelegate: AnyObject {
     func joinUsSelectSkillView(view: LNJoinUsSelectSkillView, didSelect skill: LNGameCategoryItemVO)
 }
 

+ 2 - 2
Lanu/Views/Game/MateFilter/LNGameCategoryFilterPanel.swift

@@ -10,7 +10,7 @@ import UIKit
 import SnapKit
 
 
-protocol LNGameCategoryFilterPanelDelegate: NSObject {
+protocol LNGameCategoryFilterPanelDelegate: AnyObject {
     func onGameCategoryFilterPanel(panel: LNGameCategoryFilterPanel, didSelect game: LNGameCategoryItemVO, gameType: LNGameTypeItemVO)
 }
 
@@ -128,7 +128,7 @@ extension LNGameCategoryFilterPanel {
             }), for: .touchUpInside)
             
             if index == 0 {
-                DispatchQueue.main.async { [weak self, weak button] in
+                runOnMain { [weak self, weak button] in
                     guard let self, let button else { return }
                     handleClickTab(view: button, typeItem: title)
                 }

+ 1 - 1
Lanu/Views/Game/MateFilter/LNGameFilterPanel.swift

@@ -11,7 +11,7 @@ import SnapKit
 import Combine
 
 
-protocol LNGameFilterPanelDelegate: NSObject {
+protocol LNGameFilterPanelDelegate: AnyObject {
     func onGameFilterPanel(panel: LNGameFilterPanel, didSelectFilter filter: LNGameMateFilter, topType: LNGameTypeItemVO, category: LNGameCategoryItemVO)
 }
 

+ 1 - 1
Lanu/Views/Game/MateFilter/LNGameMateFilterPanel.swift

@@ -10,7 +10,7 @@ import UIKit
 import SnapKit
 
 
-protocol LNGameMateFilterPanelDelegate: NSObject {
+protocol LNGameMateFilterPanelDelegate: AnyObject {
     func onGameMateFilterPanel(panel: LNGameMateFilterPanel, filterGame filter: LNGameMateFilter)
 }
 

+ 1 - 1
Lanu/Views/Game/MateList/LNGameMateListMenuView.swift

@@ -10,7 +10,7 @@ import UIKit
 import SnapKit
 
 
-protocol LNGameMateListMenuViewDelegate: NSObject {
+protocol LNGameMateListMenuViewDelegate: AnyObject {
     func menuView(view: LNGameMateListMenuView, scoreTypeChanged newType: LNSortedType)
     func menuView(view: LNGameMateListMenuView, priceTypeChanged newType: LNSortedType)
     func menuViewDidClickFind(view: LNGameMateListMenuView)

+ 1 - 1
Lanu/Views/Game/Skill/Edit/LNSkillFieldBaseEditView.swift

@@ -10,7 +10,7 @@ import UIKit
 import SnapKit
 
 
-protocol LNSkillFieldBaseEditViewDelegate: NSObject {
+protocol LNSkillFieldBaseEditViewDelegate: AnyObject {
     func onSkillFieldBaseEditViewInputChanged(view: LNSkillFieldBaseEditView)
 }
 

+ 1 - 1
Lanu/Views/Home/GameTab/LNHomeActivityTabView.swift

@@ -10,7 +10,7 @@ import UIKit
 import SnapKit
 
 
-protocol LNHomeActivityTabViewDelegate: NSObject {
+protocol LNHomeActivityTabViewDelegate: AnyObject {
     func homeActivityTabView(view: LNHomeActivityTabView, didSelect category: LNGameCategoryItemVO?)
     func homeActivityTabViewClickMore(view: LNHomeActivityTabView)
 }

+ 1 - 1
Lanu/Views/Home/GameTab/LNMainGameTabView.swift

@@ -10,7 +10,7 @@ import UIKit
 import SnapKit
 
 
-protocol LNHomeGameTabViewDelegate: NSObject {
+protocol LNHomeGameTabViewDelegate: AnyObject {
     func homeGameTabView(view: LNHomeGameTabView, didSelect category: LNGameCategoryItemVO?)
     func homeGameTabViewClickMore(view: LNHomeGameTabView)
 }

+ 1 - 1
Lanu/Views/Home/LNHomeTopTabView.swift

@@ -9,7 +9,7 @@ import Foundation
 import UIKit
 import SnapKit
 
-protocol LNHomeTopTabViewDelegate: NSObject {
+protocol LNHomeTopTabViewDelegate: AnyObject {
     func homeTopTabView(view: LNHomeTopTabView, didSelectAt index: Int, type: LNGameTypeItemVO)
 }
 

+ 1 - 1
Lanu/Views/IM/Chat/Emoji/LNIMChatEmojiPanel.swift

@@ -10,7 +10,7 @@ import UIKit
 import SnapKit
 
 
-protocol LNIMChatEmojiPanelDelegate: NSObject {
+protocol LNIMChatEmojiPanelDelegate: AnyObject {
     func onIMChatEmojiPanelDidClickDelete(view: LNIMChatEmojiPanel)
     func onIMChatEmojiPanel(view: LNIMChatEmojiPanel, didSelectEmoji emoji: LNEmojiData)
 }

+ 1 - 1
Lanu/Views/IM/Chat/GameMate/LNIMChatGameMateSkillCell.swift

@@ -10,7 +10,7 @@ import UIKit
 import SnapKit
 
 
-protocol LNIMChatGameMateSkillCellDelegate: NSObject {
+protocol LNIMChatGameMateSkillCellDelegate: AnyObject {
     func onIMChatGameMateSkillCell(cell: LNIMChatGameMateSkillCell, didClickOrder skill: LNGameMateSkillVO)
 }
 

+ 1 - 1
Lanu/Views/IM/Chat/InputMenu/LNIMChatTextInputView.swift

@@ -10,7 +10,7 @@ import UIKit
 import SnapKit
 
 
-protocol LNIMChatTextInputViewDelegate: NSObject {
+protocol LNIMChatTextInputViewDelegate: AnyObject {
     func onVoiceInputClick()
 }
 

+ 1 - 1
Lanu/Views/IM/Chat/InputMenu/LNIMChatVoiceInputView.swift

@@ -10,7 +10,7 @@ import UIKit
 import SnapKit
 
 
-protocol LNIMChatVoiceInputViewDelegate: NSObject {
+protocol LNIMChatVoiceInputViewDelegate: AnyObject {
     func onVoiceFinishInput()
 }
 

+ 1 - 1
Lanu/Views/Order/OrderQR/LNOrderGenerateQRCodePanel.swift

@@ -33,7 +33,7 @@ class LNOrderGenerateQRCodePanel: LNPopupView {
         
         setupViews()
         
-        DispatchQueue.main.async { [weak self] in
+        runOnMain { [weak self] in
             guard let self else { return }
             tabView.curType = .normal
             curSkill = myUserInfo.skills.first

+ 1 - 1
Lanu/Views/Order/OrderQR/LNOrderQRTabView.swift

@@ -10,7 +10,7 @@ import UIKit
 import SnapKit
 
 
-protocol LNOrderQRTabViewDelegate: NSObject {
+protocol LNOrderQRTabViewDelegate: AnyObject {
     func onOrderQRTabView(view: LNOrderQRTabView, didChangedType newType: LNOrderSource)
 }
 

+ 1 - 1
Lanu/Views/Profile/Edit/LNEditProfilePhotoWallView.swift

@@ -10,7 +10,7 @@ import UIKit
 import SnapKit
 
 
-protocol LNEditProfilePhotoWallViewDelegate: NSObject {
+protocol LNEditProfilePhotoWallViewDelegate: AnyObject {
     func onEditProfilePhotoWallViewDidChanged(view: LNEditProfilePhotoWallView)
 }
 

+ 1 - 1
Lanu/Views/Profile/Feed/LNFeedCommentCell.swift

@@ -26,7 +26,7 @@ class LNFeedCommentCell: UITableViewCell {
         avatar.sd_setImage(with: URL(string: comment.avatar))
         nameLabel.text = comment.nickname
         timeLabel.text = TimeInterval(comment.createdAt / 1_000).tencentIMTimeDesc
-        contentLabel.text = comment.textContent
+        contentLabel.attributedText = comment.textContent.getEmojiString(with: .body_m)
     }
     
     required init?(coder: NSCoder) {

+ 1 - 1
Lanu/Views/Profile/Feed/LNProfileFeedItemCell.swift

@@ -47,7 +47,7 @@ class LNProfileFeedItemCell: UITableViewCell {
         avatar.sd_setImage(with: URL(string: item.avatar))
         nameLabel.text = item.nickname
         timeLabel.text = TimeInterval(item.createdAt / 1_000).tencentIMTimeDesc
-        contentLabel.text = item.textContent
+        contentLabel.attributedText = item.textContent.getEmojiString(with: .body_m)
         likeView.update(id: item.id, liked: item.liked, count: item.likeCount)
         commentView.update(id: item.id, count: item.commentCount)
         videoView.stop()

+ 1 - 1
Lanu/Views/Profile/Profile/Detail/LNProfileUserDetailView.swift

@@ -11,7 +11,7 @@ import SnapKit
 import Combine
 
 
-protocol LNProfileUserDetailViewDelegate: NSObject {
+protocol LNProfileUserDetailViewDelegate: AnyObject {
     func onProfileUserDetailView(view: LNProfileUserDetailView, contentHeightChanged height: CGFloat)
 }
 

+ 1 - 1
Lanu/Views/Profile/Profile/LNProfileInRoomView.swift

@@ -110,7 +110,7 @@ private extension LNProfileInRoomView {
         addSubview(roomCover)
         roomCover.snp.makeConstraints { make in
             make.center.equalTo(waveView)
-            make.width.height.equalTo(36)
+            make.width.height.equalTo(37)
         }
         
         let bottom = buildBottom()

+ 1 - 1
Lanu/Views/Profile/Profile/LNProfilePhotoWall.swift

@@ -16,7 +16,7 @@ private class LNProfilePhotoWallMode {
 }
 
 
-protocol LNProfilePhotoWallDelegate: NSObject {
+protocol LNProfilePhotoWallDelegate: AnyObject {
     func onProfilePhotoWall(view: LNProfilePhotoWall, contentHeightChanged height: CGFloat)
 }
 

+ 1 - 1
Lanu/Views/Profile/Profile/LNProfileTabView.swift

@@ -16,7 +16,7 @@ enum LNProfileTabType {
 }
 
 
-protocol LNProfileTabViewDelegate: NSObject {
+protocol LNProfileTabViewDelegate: AnyObject {
     func onProfileTabView(view: LNProfileTabView, didSelect at: Int, type: LNProfileTabType)
 }
 

+ 1 - 1
Lanu/Views/Profile/Relation/LNUserRelationListView.swift

@@ -11,7 +11,7 @@ import SnapKit
 import MJRefresh
 
 
-protocol LNUserRelationListViewDelegate: NSObject {
+protocol LNUserRelationListViewDelegate: AnyObject {
     func onUserRelationListViewTotalChanged(view: LNUserRelationListView, total: Int)
 }
 

+ 2 - 2
Lanu/Views/Room/Bottom/Input/LNRoomMessageInputView.swift

@@ -49,9 +49,9 @@ extension LNRoomMessageInputView {
         }
         
         let titleLabel = UILabel()
-        titleLabel.text = .init(key: "A00325")
+        titleLabel.text = .init(key: "A00325") + "..."
         titleLabel.font = .body_s
-        titleLabel.textColor = .text_1
+        titleLabel.textColor = .text_1.withAlphaComponent(0.8)
         addSubview(titleLabel)
         titleLabel.snp.makeConstraints { make in
             make.horizontalEdges.equalToSuperview().inset(10)

+ 9 - 3
Lanu/Views/Room/Bottom/LNRoomBottomMenuView.swift

@@ -90,15 +90,21 @@ extension LNRoomBottomMenuView {
             make.width.height.equalTo(32)
         }
         
-        giftButton.isHidden = true
-        giftButton.setImage(.icGift, for: .normal)
+        giftButton.isHidden = false
+        giftButton.setBackgroundImage(.icGift, for: .normal)
+        giftButton.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            let panel = LNRoomGiftPanel()
+            panel.update(roomSession)
+            panel.popup(self)
+        }), for: .touchUpInside)
         stackView.addArrangedSubview(giftButton)
         giftButton.snp.makeConstraints { make in
             make.width.height.equalTo(32)
         }
         
         menuButton.isHidden = true
-        menuButton.setImage(.icMoreWithBg, for: .normal)
+        menuButton.setBackgroundImage(.icMoreWithBg, for: .normal)
         menuButton.addAction(UIAction(handler: { [weak self] _ in
             guard let self else { return }
             let panel = LNRoomSettingMenuPanel()

+ 4 - 4
Lanu/Views/Room/Bottom/Mic/LNRoomBottomMicView.swift

@@ -37,14 +37,14 @@ extension LNRoomBottomMicView: LNRoomViewModelNotify {
         if let seat = roomSession?.mySeatInfo {
             isHidden = false
             if seat.isMute {
-                setImage(.icMicOffWhite15, for: .normal)
+                setBackgroundImage(.icMicOffWhite15, for: .normal)
                 isEnabled = false
             } else {
                 isEnabled = true
                 if seat.isLocalMute {
-                    setImage(.icMicOffWhite15, for: .normal)
+                    setBackgroundImage(.icMicOffWhite15, for: .normal)
                 } else {
-                    setImage(.icMicOn, for: .normal)
+                    setBackgroundImage(.icMicOn, for: .normal)
                 }
             }
         } else {
@@ -55,7 +55,7 @@ extension LNRoomBottomMicView: LNRoomViewModelNotify {
 
 extension LNRoomBottomMicView {
     private func setupViews() {
-        setImage(.icMicOn, for: .normal)
+        setBackgroundImage(.icMicOn, for: .normal)
         addAction(UIAction(handler: { [weak self] _ in
             guard let self else { return }
             if roomSession?.mySeatInfo?.isLocalMute == true {

+ 198 - 0
Lanu/Views/Room/Gift/LNRoomGiftBottomView.swift

@@ -0,0 +1,198 @@
+//
+//  LNRoomGiftBottomView.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/3/23.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+protocol LNRoomGiftBottomViewDelegate: AnyObject {
+    func onRoomGiftBottomViewDidTapBalance(_ view: LNRoomGiftBottomView)
+    func onRoomGiftBottomViewDidTapSend(_ view: LNRoomGiftBottomView)
+}
+
+
+class LNRoomGiftBottomView: UIView {
+    private let balanceLabel = UILabel()
+    private let sendButton = UIButton(type: .system)
+    private let countView = UIView()
+    private let minusButton = UIButton()
+    private let countLabel = UILabel()
+    private var countLabelWidth: Constraint?
+    private(set) var curCount: Int = 0 {
+        didSet {
+            countLabel.text = "\(curCount)"
+            minusButton.isEnabled = curCount > 1
+            minusButton.alpha = curCount > 1 ? 1.0 : 0.4
+            if curCount > 99 {
+                countLabelWidth?.update(offset: 35)
+            } else if curCount > 9 {
+                countLabelWidth?.update(offset: 25)
+            } else {
+                countLabelWidth?.update(offset: 15)
+            }
+        }
+    }
+    var enable: Bool = false {
+        didSet {
+            sendButton.isEnabled = enable
+            sendButton.alpha = enable ? 1.0 : 0.4
+            countView.isHidden = !enable
+            if !enable {
+                curCount = 1
+            }
+        }
+    }
+    
+    weak var delegate: LNRoomGiftBottomViewDelegate?
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        setupViews()
+        LNEventDeliver.addObserver(self)
+        
+        runOnMain { [weak self] in
+            guard let self else { return }
+            curCount = 1
+            enable = false
+        }
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+extension LNRoomGiftBottomView: LNPurchaseManagerNotify {
+    func onUserWalletInfoChanged(info: LNUserWalletInfo) {
+        balanceLabel.text = myWalletInfo.diamond.toDisplay
+    }
+}
+
+private extension LNRoomGiftBottomView {
+    func setupViews() {
+        snp.makeConstraints { make in
+            make.height.equalTo(54)
+        }
+        
+        let balanceView = UIView()
+        balanceView.onTap { [weak self] in
+            guard let self else { return }
+            delegate?.onRoomGiftBottomViewDidTapBalance(self)
+        }
+        addSubview(balanceView)
+        balanceView.snp.makeConstraints { make in
+            make.leading.equalToSuperview().offset(10)
+            make.verticalEdges.equalToSuperview()
+        }
+        
+        let diamond = UIImageView.diamondImageView(true)
+        balanceView.addSubview(diamond)
+        diamond.snp.makeConstraints { make in
+            make.leading.equalToSuperview()
+            make.centerY.equalToSuperview().offset(-1)
+            make.width.height.equalTo(18)
+        }
+        
+        balanceLabel.font = .heading_h3
+        balanceLabel.textColor = .text_1
+        balanceLabel.text = myWalletInfo.diamond.toDisplay
+        balanceView.addSubview(balanceLabel)
+        balanceLabel.snp.makeConstraints { make in
+            make.leading.equalTo(diamond.snp.trailing).offset(2)
+            make.centerY.equalToSuperview()
+        }
+        
+        let arrow = UIImageView.arrowImageView(size: 12, weight: .semibold)
+        arrow.tintColor = .text_1
+        balanceView.addSubview(arrow)
+        arrow.snp.makeConstraints { make in
+            make.leading.equalTo(balanceLabel.snp.trailing).offset(4)
+            make.trailing.equalToSuperview()
+            make.centerY.equalToSuperview()
+        }
+        
+        countView.layer.cornerRadius = 15
+        countView.backgroundColor = .fill.withAlphaComponent(0.1)
+        addSubview(countView)
+        countView.snp.makeConstraints { make in
+            make.top.equalToSuperview().offset(10)
+            make.trailing.equalToSuperview().offset(-10)
+            make.height.equalTo(30)
+        }
+        
+        sendButton.setTitle(.init(key: "A00305"), for: .normal)
+        sendButton.setTitleColor(.text_1, for: .normal)
+        sendButton.titleLabel?.font = .heading_h5
+        sendButton.layer.cornerRadius = 15
+        sendButton.clipsToBounds = true
+        sendButton.setBackgroundImage(.primary_8, for: .normal)
+        sendButton.contentEdgeInsets = .init(top: 0, left: 12, bottom: 0, right: 12)
+        sendButton.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            delegate?.onRoomGiftBottomViewDidTapSend(self)
+        }), for: .touchUpInside)
+        addSubview(sendButton)
+        sendButton.snp.makeConstraints { make in
+            make.trailing.equalTo(countView)
+            make.centerY.equalTo(countView)
+            make.height.equalTo(countView)
+        }
+        
+        let config = UIImage.SymbolConfiguration(pointSize: 11, weight: .semibold)
+        minusButton.setImage(.init(systemName: "minus", withConfiguration: config), for: .normal)
+        minusButton.backgroundColor = .fill.withAlphaComponent(0.6)
+        minusButton.layer.cornerRadius = 12
+        minusButton.tintColor = .fill_7
+        minusButton.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            curCount -= 1
+        }), for: .touchUpInside)
+        countView.addSubview(minusButton)
+        minusButton.snp.makeConstraints { make in
+            make.width.height.equalTo(24)
+            make.centerY.equalToSuperview()
+            make.leading.equalToSuperview().offset(3)
+        }
+        
+        countLabel.font = .body_m
+        countLabel.textColor = .text_1
+        countLabel.textAlignment = .center
+        countView.addSubview(countLabel)
+        countLabel.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.leading.equalTo(minusButton.snp.trailing).offset(6)
+            countLabelWidth = make.width.equalTo(15).constraint
+        }
+        
+        let fakeView = UIView()
+        countView.addSubview(fakeView)
+        fakeView.snp.makeConstraints { make in
+            make.verticalEdges.equalToSuperview()
+            make.trailing.equalToSuperview()
+            make.width.equalTo(sendButton)
+        }
+        
+        let addButton = UIButton()
+        addButton.setImage(.init(systemName: "plus", withConfiguration: config), for: .normal)
+        addButton.backgroundColor = .fill.withAlphaComponent(0.6)
+        addButton.layer.cornerRadius = 12
+        addButton.tintColor = .fill_7
+        addButton.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            curCount += 1
+        }), for: .touchUpInside)
+        countView.addSubview(addButton)
+        addButton.snp.makeConstraints { make in
+            make.width.height.equalTo(24)
+            make.centerY.equalToSuperview()
+            make.leading.equalTo(countLabel.snp.trailing).offset(6)
+            make.trailing.equalTo(fakeView.snp.leading).offset(-12)
+        }
+    }
+}

+ 296 - 0
Lanu/Views/Room/Gift/LNRoomGiftHeaderView.swift

@@ -0,0 +1,296 @@
+//
+//  LNRoomGiftHeaderView.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/3/23.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+class LNRoomGiftHeaderView: UIView {
+    private let roomSeatsView = UIView()
+    private let stackView = UIStackView()
+    
+    private let specifiedUserView = LNRoomGiftSpecifiedUserView()
+    
+    private weak var roomSession: LNRoomViewModel?
+    private var headers: [LNRoomSeatNum: LNRoomGiftAvatarView] = [:]
+    var selection: [String] {
+        if !roomSeatsView.isHidden {
+            headers.map { $1 }.filter { !$0.isHidden && $0.isSelected }.compactMap { $0.curSeatItem?.uid }
+        } else if let uid = specifiedUserView.curUid {
+            [uid]
+        } else {
+            []
+        }
+    }
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        setupViews()
+        LNEventDeliver.addObserver(self)
+    }
+    
+    func update(_ room: LNRoomViewModel?, selectedUid: String?) {
+        roomSession = room
+        if selectedUid == nil
+            || room?.seatsInfo.contains(where: { $0.uid == selectedUid }) == true {
+            onRoomSeatsChanged()
+            
+            roomSeatsView.isHidden = false
+            specifiedUserView.isHidden = true
+        } else {
+            roomSeatsView.isHidden = true
+            specifiedUserView.isHidden = false
+        }
+        if let selectedUid {
+            headers.first { $1.curSeatItem?.uid == selectedUid }?.value.isSelected = true
+            specifiedUserView.update(selectedUid)
+        }
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+extension LNRoomGiftHeaderView: LNRoomViewModelNotify {
+    func onRoomSeatsChanged() {
+        guard let seats = roomSession?.seatsInfo.sorted(by: { $0.index.rawValue < $1.index.rawValue }) else { return }
+        if headers.count != seats.count {
+            stackView.arrangedSubviews.forEach {
+                stackView.removeArrangedSubview($0)
+                $0.removeFromSuperview()
+            }
+            
+            for seat in seats {
+                let avatar = LNRoomGiftAvatarView()
+                avatar.isSelected = false
+                avatar.onTap { [weak avatar] in
+                    guard let avatar else { return }
+                    avatar.isSelected.toggle()
+                }
+                avatar.update(seat)
+                stackView.addArrangedSubview(avatar)
+                headers[seat.index] = avatar
+            }
+        } else {
+            for seat in seats {
+                headers[seat.index]?.update(seat)
+            }
+        }
+    }
+}
+
+private extension LNRoomGiftHeaderView {
+    func setupViews() {
+        let titleLabel = UILabel()
+        titleLabel.font = .body_l
+        titleLabel.textColor = .text_2
+        titleLabel.text = .init(key: "B00129")
+        addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.leading.equalToSuperview().offset(10)
+            make.centerY.equalToSuperview()
+        }
+        
+        let roomSeatsView = buildRoomSeatsView()
+        addSubview(roomSeatsView)
+        roomSeatsView.snp.makeConstraints { make in
+            make.leading.equalTo(titleLabel.snp.trailing).offset(6)
+            make.trailing.equalToSuperview().offset(-10)
+            make.verticalEdges.equalToSuperview()
+            make.height.equalTo(40)
+        }
+        
+        let userView = buildSpecifiedUserView()
+        addSubview(userView)
+        userView.snp.makeConstraints { make in
+            make.leading.equalTo(titleLabel.snp.trailing).offset(6)
+            make.trailing.equalToSuperview().offset(-10)
+            make.centerY.equalToSuperview()
+        }
+    }
+    
+    private func buildRoomSeatsView() -> UIView {
+        let scrollView = UIScrollView()
+        scrollView.showsHorizontalScrollIndicator = false
+        scrollView.contentInset = .init(top: 0, left: 0, bottom: 0, right: 32)
+        roomSeatsView.addSubview(scrollView)
+        scrollView.snp.makeConstraints { make in
+            make.edges.equalToSuperview()
+        }
+        
+        stackView.spacing = 6
+        stackView.axis = .horizontal
+        scrollView.addSubview(stackView)
+        stackView.snp.makeConstraints { make in
+            make.edges.equalToSuperview()
+            make.height.equalToSuperview()
+        }
+        
+        let gradientView = UIView.gradientView([
+            .fill_7.withAlphaComponent(0), .fill_7
+        ], .horizontalLTR)
+        roomSeatsView.addSubview(gradientView)
+        gradientView.snp.makeConstraints { make in
+            make.verticalEdges.equalToSuperview()
+            make.trailing.equalToSuperview()
+            make.width.equalTo(32)
+        }
+        
+        return roomSeatsView
+    }
+    
+    private func buildSpecifiedUserView() -> UIView {
+        
+        return specifiedUserView
+    }
+}
+
+private class LNRoomGiftAvatarView: UIView {
+    private let avatarBg = UIView()
+    private let avatarView = UIImageView()
+    private let badgeBg = UIView()
+    private let badge = UILabel()
+    private(set) var curSeatItem: LNRoomSeatItem?
+    
+    var isSelected: Bool = false {
+        didSet {
+            if isSelected {
+                avatarBg.layer.borderColor = UIColor.text_6.cgColor
+                badge.textColor = .text_1
+                badgeBg.backgroundColor = .text_6
+            } else {
+                avatarBg.layer.borderColor = UIColor.clear.cgColor
+                badge.textColor = .text_4
+                badgeBg.backgroundColor = .text_1
+            }
+        }
+    }
+    
+    func update(_ seat: LNRoomSeatItem) {
+        isHidden = seat.uid.isEmpty || seat.uid.isMyUid
+        avatarView.sd_setImage(with: URL(string: seat.avatar))
+        badge.text = seat.index.giftHeaderTitle
+        
+        curSeatItem = seat
+    }
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        snp.makeConstraints { make in
+            make.width.height.equalTo(40)
+        }
+        
+        avatarBg.backgroundColor = .clear
+        avatarBg.layer.cornerRadius = 17
+        avatarBg.layer.borderWidth = 1
+        avatarBg.layer.borderColor = UIColor.primary_4.cgColor
+        addSubview(avatarBg)
+        avatarBg.snp.makeConstraints { make in
+            make.width.height.equalTo(34)
+            make.centerX.equalToSuperview()
+            make.top.equalToSuperview().offset(1)
+            make.bottom.equalToSuperview().offset(-5)
+        }
+        
+        avatarView.layer.cornerRadius = 15
+        avatarView.clipsToBounds = true
+        avatarView.contentMode = .scaleAspectFill
+        avatarBg.addSubview(avatarView)
+        avatarView.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+            make.width.height.equalTo(30)
+        }
+        
+        badgeBg.backgroundColor = .fill
+        badgeBg.layer.cornerRadius = 6
+        addSubview(badgeBg)
+        badgeBg.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.bottom.equalToSuperview().offset(-2)
+            make.height.equalTo(12)
+            make.leading.greaterThanOrEqualToSuperview()
+            make.width.greaterThanOrEqualTo(26)
+        }
+        
+        badge.font = .systemFont(ofSize: 10)
+        badge.textColor = .text_4
+        badge.textAlignment = .center
+        badgeBg.addSubview(badge)
+        badge.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+            make.horizontalEdges.equalToSuperview().inset(4)
+        }
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+private class LNRoomGiftSpecifiedUserView: UIView {
+    private let nameLabel = UILabel()
+    private let avatar = UIImageView()
+    private(set) var curUid: String?
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        let avatarBg = UIView()
+        avatarBg.backgroundColor = .clear
+        avatarBg.layer.cornerRadius = 17
+        avatarBg.layer.borderWidth = 1
+        avatarBg.layer.borderColor = UIColor.primary_4.cgColor
+        addSubview(avatarBg)
+        avatarBg.snp.makeConstraints { make in
+            make.width.height.equalTo(34)
+            make.leading.equalToSuperview()
+            make.verticalEdges.equalToSuperview()
+        }
+        
+        avatar.layer.cornerRadius = 15
+        avatar.clipsToBounds = true
+        avatar.contentMode = .scaleAspectFill
+        avatarBg.addSubview(avatar)
+        avatar.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+            make.width.height.equalTo(30)
+        }
+        
+        nameLabel.font = .body_s
+        nameLabel.textColor = .text_1
+        addSubview(nameLabel)
+        nameLabel.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.leading.equalTo(avatarBg.snp.trailing).offset(5)
+            make.trailing.equalToSuperview()
+        }
+    }
+    
+    func update(_ uid: String?) {
+        curUid = uid
+        guard let uid, !uid.isEmpty else {
+            isHidden = true
+            return
+        }
+        
+        LNProfileManager.shared.getCachedProfileUserInfo(uid: uid, fetchIfNeeded: true)
+        { [weak self] info in
+            guard let self else { return }
+            guard let info, info.uid == curUid else { return }
+            nameLabel.text = info.name
+            avatar.sd_setImage(with: URL(string: info.avatar))
+        }
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}

+ 101 - 0
Lanu/Views/Room/Gift/LNRoomGiftPanel.swift

@@ -0,0 +1,101 @@
+//
+//  LNRoomGiftPanel.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/3/23.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+class LNRoomGiftPanel: LNPopupView {
+    private let headerView = LNRoomGiftHeaderView()
+    private let listView = LNRoomGiftListView()
+    private let bottomView = LNRoomGiftBottomView()
+    
+    private weak var roomSession: LNRoomViewModel?
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        setupViews()
+    }
+    
+    func update(_ room: LNRoomViewModel?, selectedUid: String? = nil) {
+        roomSession = room
+        headerView.update(room, selectedUid: selectedUid)
+        listView.update(room)
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+extension LNRoomGiftPanel: LNRoomGiftListViewDelegate {
+    func onRoomGiftListView(_ view: LNRoomGiftListView, didSelect index: Int) {
+        checkSendEnable()
+    }
+}
+
+extension LNRoomGiftPanel: LNRoomGiftBottomViewDelegate {
+    func onRoomGiftBottomViewDidTapBalance(_ view: LNRoomGiftBottomView) {
+        dismiss()
+        pushToDiamondView()
+    }
+    
+    func onRoomGiftBottomViewDidTapSend(_ view: LNRoomGiftBottomView) {
+        if headerView.selection.isEmpty {
+            showToast(.init(key: "A00391"))
+            return
+        }
+        guard let gift = listView.selectedGift else {
+            return
+        }
+        roomSession?.sendGift(gift: gift, to: headerView.selection, count: bottomView.curCount) { _ in }
+    }
+}
+
+extension LNRoomGiftPanel {
+    private func checkSendEnable() {
+        bottomView.enable = listView.selectedGift != nil
+    }
+    
+    private func setupViews() {
+        touchOutsideToCancel = true
+        container.backgroundColor = .fill_7
+        
+        container.addSubview(headerView)
+        headerView.snp.makeConstraints { make in
+            make.top.equalToSuperview().offset(12)
+            make.horizontalEdges.equalToSuperview()
+        }
+        
+        let separator = UIView()
+        separator.backgroundColor = .text_1.withAlphaComponent(0.12)
+        container.addSubview(separator)
+        separator.snp.makeConstraints { make in
+            make.top.equalTo(headerView.snp.bottom).offset(10)
+            make.horizontalEdges.equalToSuperview().inset(10)
+            make.height.equalTo(0.5)
+        }
+        
+        listView.delegate = self
+        container.addSubview(listView)
+        listView.snp.makeConstraints { make in
+            make.top.equalTo(separator.snp.bottom).offset(10)
+            make.horizontalEdges.equalToSuperview()
+            make.height.equalTo(256)
+        }
+        
+        bottomView.delegate = self
+        container.addSubview(bottomView)
+        bottomView.snp.makeConstraints { make in
+            make.top.equalTo(listView.snp.bottom)
+            make.horizontalEdges.equalToSuperview()
+            make.bottom.equalToSuperview().offset(commonBottomInset + 12)
+        }
+    }
+}

+ 125 - 0
Lanu/Views/Room/Gift/List/LNRoomGiftItemCell.swift

@@ -0,0 +1,125 @@
+//
+//  LNRoomGiftItemCell.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/3/23.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+class LNRoomGiftItemCell: UICollectionViewCell {
+    private let selectionBackground = UIImageView()
+    private let iconView = UIImageView()
+    private let nameLabel = UILabel()
+    private let priceLabel = UILabel()
+    override var isSelected: Bool {
+        didSet {
+            selectionBackground.isHidden = !isSelected
+        }
+    }
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        setupViews()
+    }
+    
+    func update(_ item: LNGiftItemVO) {
+        guard let res = LNGiftManager.shared.resource(for: item.resId) else {
+            isHidden = true
+            return
+        }
+        isHidden = false
+        nameLabel.text = res.curName
+        priceLabel.text = item.value.currencyDisplay
+        iconView.sd_setImage(with: URL(string: res.icon))
+    }
+    
+    func showJumpAnimate() {
+        iconView.layer.removeAllAnimations()
+        iconView.transform = .identity
+        
+        let totalDuration: TimeInterval = 1.0
+        let firstJumpHeight: CGFloat = 8.0
+        let secondJumpHeight: CGFloat = 6.0
+        
+        let perTime = totalDuration / 4.0
+        UIView.animateKeyframes(withDuration: totalDuration, delay: 0, options: [.calculationModeCubic], animations: {
+            UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: perTime) {
+                self.iconView.transform = CGAffineTransform(translationX: 0, y: -firstJumpHeight)
+            }
+            UIView.addKeyframe(withRelativeStartTime: perTime, relativeDuration: perTime) {
+                self.iconView.transform = .identity
+            }
+            UIView.addKeyframe(withRelativeStartTime: 2 * perTime, relativeDuration: perTime) {
+                self.iconView.transform = CGAffineTransform(translationX: 0, y: -secondJumpHeight)
+            }
+            UIView.addKeyframe(withRelativeStartTime: 3 * perTime, relativeDuration: perTime) {
+                self.iconView.transform = .identity
+            }
+        })
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+private extension LNRoomGiftItemCell {
+    func setupViews() {
+        backgroundColor = .clear
+        
+        selectionBackground.image = .icGiftSelectedBg
+        selectionBackground.isHidden = true
+        contentView.addSubview(selectionBackground)
+        selectionBackground.snp.makeConstraints { make in
+            make.edges.equalToSuperview()
+        }
+        
+        iconView.contentMode = .scaleAspectFit
+        contentView.addSubview(iconView)
+        iconView.snp.makeConstraints { make in
+            make.top.equalToSuperview()
+            make.centerX.equalToSuperview()
+            make.width.height.equalTo(58)
+        }
+        
+        nameLabel.font = .body_xs
+        nameLabel.textColor = .text_1
+        nameLabel.textAlignment = .center
+        contentView.addSubview(nameLabel)
+        nameLabel.snp.makeConstraints { make in
+            make.top.equalTo(iconView.snp.bottom).offset(2)
+            make.centerX.equalToSuperview()
+            make.horizontalEdges.equalToSuperview().inset(4)
+        }
+        
+        let priceView = UIView()
+        contentView.addSubview(priceView)
+        priceView.snp.makeConstraints { make in
+            make.top.equalTo(nameLabel.snp.bottom).offset(2)
+            make.centerX.equalToSuperview()
+            make.bottom.lessThanOrEqualToSuperview()
+        }
+        
+        let diamond = UIImageView.diamondImageView()
+        diamond.contentMode = .scaleAspectFit
+        priceView.addSubview(diamond)
+        diamond.snp.makeConstraints { make in
+            make.leading.equalToSuperview()
+            make.centerY.equalToSuperview()
+            make.width.height.equalTo(10)
+        }
+        
+        priceLabel.font = .body_xs
+        priceLabel.textColor = .text_1.withAlphaComponent(0.5)
+        priceView.addSubview(priceLabel)
+        priceLabel.snp.makeConstraints { make in
+            make.leading.equalTo(diamond.snp.trailing).offset(2)
+            make.top.bottom.trailing.equalToSuperview()
+        }
+    }
+}

+ 115 - 0
Lanu/Views/Room/Gift/List/LNRoomGiftListView.swift

@@ -0,0 +1,115 @@
+//
+//  LNRoomGiftListView.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/3/23.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+import Combine
+
+
+protocol LNRoomGiftListViewDelegate: AnyObject {
+    func onRoomGiftListView(_ view: LNRoomGiftListView, didSelect index: Int)
+}
+
+
+class LNRoomGiftListView: UIView {
+    private let collectionView: UICollectionView
+    private var selectedIndex: Int?
+    var selectedGift: LNGiftItemVO? {
+        if let selectedIndex {
+            giftList[selectedIndex]
+        } else {
+            nil
+        }
+    }
+    
+    private var roomSession: LNRoomViewModel?
+    private var giftList: [LNGiftItemVO] = []
+    
+    weak var delegate: LNRoomGiftListViewDelegate?
+    
+    override init(frame: CGRect) {
+        let layout = UICollectionViewFlowLayout()
+        layout.scrollDirection = .vertical
+        layout.minimumLineSpacing = 10
+        layout.minimumInteritemSpacing = 0
+        collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
+        super.init(frame: frame)
+        
+        setupViews()
+    }
+    
+    func update(_ room: LNRoomViewModel?) {
+        roomSession = room
+        giftList = roomSession?.giftList ?? [] // 拷贝一份,避免房间数据更新后导致界面异常 
+        selectedIndex = nil
+        collectionView.reloadData()
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+extension LNRoomGiftListView: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
+    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
+        giftList.count
+    }
+    
+    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
+        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: LNRoomGiftItemCell.className, for: indexPath) as! LNRoomGiftItemCell
+        cell.update(giftList[indexPath.item])
+        cell.isSelected = selectedIndex == indexPath.item
+        return cell
+    }
+    
+    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
+        if selectedIndex == indexPath.item { return }
+        
+        if let selectedIndex {
+            collectionView.cellForItem(at: .init(row: selectedIndex, section: 0))?.isSelected = false
+        }
+        selectedIndex = indexPath.item
+        collectionView.cellForItem(at: indexPath)?.isSelected = true
+        (collectionView.cellForItem(at: indexPath) as? LNRoomGiftItemCell)?.showJumpAnimate()
+        
+        delegate?.onRoomGiftListView(self, didSelect: indexPath.item)
+    }
+    
+    func collectionView(_ collectionView: UICollectionView,
+                        layout collectionViewLayout: UICollectionViewLayout,
+                        sizeForItemAt indexPath: IndexPath) -> CGSize
+    {
+        .init(width: 90, height: 96)
+    }
+}
+
+private extension LNRoomGiftListView {
+    func setupViews() {
+        collectionView.delegate = self
+        collectionView.dataSource = self
+        collectionView.backgroundColor = .clear
+        collectionView.allowsMultipleSelection = false
+        collectionView.showsVerticalScrollIndicator = false
+        collectionView.contentInset = .init(top: 0, left: 7.5, bottom: 32, right: 7.5)
+        collectionView.register(LNRoomGiftItemCell.self, forCellWithReuseIdentifier: LNRoomGiftItemCell.className)
+        addSubview(collectionView)
+        collectionView.snp.makeConstraints { make in
+            make.edges.equalToSuperview()
+        }
+        
+        let bottomGradientView = UIView.gradientView([
+            .init(hex: "#24213800"), .init(hex: "#242138")
+        ], .verticalUTD)
+        addSubview(bottomGradientView)
+        bottomGradientView.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.bottom.equalToSuperview()
+            make.height.equalTo(44)
+        }
+    }
+}

+ 1 - 1
Lanu/Views/Room/Join/Apply/LNRoomApplySeatCell.swift

@@ -28,7 +28,7 @@ class LNRoomApplySeatCell: UITableViewCell {
         indexLabel.text = "\(index)"
         avatarView.showAvatar(item.user.avatar)
         nameLabel.text = item.user.nickname
-        timeLabel.text = item.relativeTimeText
+        timeLabel.text = TimeInterval(item.applyTime / 1_000).relativeTimeText
         
         genderView.image = switch item.user.gender {
         case .unknow: nil

+ 15 - 18
Lanu/Views/Room/Join/Apply/LNRoomApplySeatListPanel.swift

@@ -70,9 +70,7 @@ private extension LNRoomApplySeatListPanel {
             } else {
                 tableView.mj_footer?.endRefreshing()
             }
-            if let total = res?.total {
-                countLabel.text = .init(key: "A00334", total)
-            }
+            updateCountView()
         }
     }
 }
@@ -93,6 +91,16 @@ extension LNRoomApplySeatListPanel: UITableViewDataSource {
 }
 
 private extension LNRoomApplySeatListPanel {
+    private func updateCountView() {
+        let text = String(key: "A00333", items.count)
+        let range = (text as NSString).range(of: .init(key: "A00334", items.count))
+        let attrString = NSMutableAttributedString(string: text)
+        attrString.addAttributes([
+            .foregroundColor: UIColor.text_6
+        ], range: range)
+        countLabel.attributedText = attrString
+    }
+    
     func setupViews() {
         container.backgroundColor = .fill_7
         
@@ -167,24 +175,13 @@ private extension LNRoomApplySeatListPanel {
     func buildCountView() -> UIView {
         let container = UIView()
         
-        let stackView = UIStackView()
-        stackView.axis = .horizontal
-        stackView.spacing = 5
-        stackView.alignment = .center
-        container.addSubview(stackView)
-        stackView.snp.makeConstraints { make in
+        countLabel.font = .body_m
+        countLabel.textColor = .text_1
+        container.addSubview(countLabel)
+        countLabel.snp.makeConstraints { make in
             make.leading.centerY.equalToSuperview()
         }
         
-        countLabel.font = .body_m
-        countLabel.textColor = .text_6
-        stackView.addArrangedSubview(countLabel)
-        
-        countDescLabel.font = .body_m
-        countDescLabel.textColor = .text_1
-        countDescLabel.text = .init(key: "A00333")
-        stackView.addArrangedSubview(countDescLabel)
-        
         return container
     }
 }

+ 1 - 1
Lanu/Views/Room/Join/Manage/LNRoomManageSeatCell.swift

@@ -31,7 +31,7 @@ class LNRoomManageSeatCell: UITableViewCell {
         indexLabel.text = "\(index)"
         avatarView.showAvatar(item.user.avatar)
         nameLabel.text = item.user.nickname
-        timeLabel.text = item.relativeTimeText
+        timeLabel.text = TimeInterval(item.applyTime / 1_000).relativeTimeText
         
         genderView.image = switch item.user.gender {
         case .unknow: nil

+ 51 - 49
Lanu/Views/Room/Join/Manage/LNRoomManageSeatListView.swift

@@ -114,7 +114,13 @@ extension LNRoomManageSeatListView: UITableViewDataSource, UITableViewDelegate {
 
 extension LNRoomManageSeatListView {
     private func updateCountView() {
-        countLabel.text = .init(key: "A00334", items.count)
+        let text = String(key: "A00333", items.count)
+        let range = (text as NSString).range(of: .init(key: "A00334", items.count))
+        let attrString = NSMutableAttributedString(string: text)
+        attrString.addAttributes([
+            .foregroundColor: UIColor.text_6
+        ], range: range)
+        countLabel.attributedText = attrString
     }
     
     private func updateFilterView() {
@@ -199,58 +205,54 @@ extension LNRoomManageSeatListView {
     private func buildCountView() -> UIView {
         let container = UIView()
         
-        let stackView = UIStackView()
-        stackView.axis = .horizontal
-        stackView.spacing = 5
-        stackView.alignment = .center
-        container.addSubview(stackView)
-        stackView.snp.makeConstraints { make in
-            make.leading.centerY.equalToSuperview()
-        }
-        
         countLabel.font = .body_m
-        countLabel.textColor = .text_6
-        stackView.addArrangedSubview(countLabel)
-        
-        let countDescLabel = UILabel()
-        countDescLabel.font = .body_m
-        countDescLabel.textColor = .text_1
-        countDescLabel.text = .init(key: "A00333")
-        stackView.addArrangedSubview(countDescLabel)
+        countLabel.textColor = .text_1
+        countLabel.numberOfLines = 0
+        container.addSubview(countLabel)
+        countLabel.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.leading.equalToSuperview()
+            make.trailing.lessThanOrEqualToSuperview().priority(.medium)
+        }
         
-        let config = UIImage.SymbolConfiguration(pointSize: 12)
-        filterButton.isHidden = tabType != .playmate
-        filterButton.backgroundColor = .fill.withAlphaComponent(0.15)
-        filterButton.layer.cornerRadius = 15
-        filterButton.clipsToBounds = true
-        filterButton.titleLabel?.font = .body_s
-        filterButton.setTitleColor(.text_1, for: .normal)
-        filterButton.setImage(.init(systemName: "chevron.backward", withConfiguration: config), for: .normal)
-        filterButton.tintColor = .text_1
-        filterButton.semanticContentAttribute = .forceRightToLeft
-        filterButton.imageEdgeInsets = .init(top: 0, left: 4, bottom: 0, right: -4)
-        filterButton.contentEdgeInsets = .init(top: 0, left: 12, bottom: 0, right: 12)
-        filterButton.addAction(UIAction(handler: { [weak self] _ in
-            guard let self, tabType == .playmate else { return }
-            
-            let panel = LNRoomGameCategoryFilterPanel()
-            let options = buildPlaymateFilterOptions()
-            panel.update(options: options, curSelectedCode: curCode)
-            panel.handler = { [weak self] option in
-                guard let self else { return }
-                curCode = option.code
-                curTitle = option.code.isEmpty ? .init(key: "A00361") : option.name
-                updateFilterView()
+        if tabType == .playmate {
+            let config = UIImage.SymbolConfiguration(pointSize: 12)
+            filterButton.backgroundColor = .fill.withAlphaComponent(0.15)
+            filterButton.layer.cornerRadius = 15
+            filterButton.clipsToBounds = true
+            filterButton.titleLabel?.font = .body_s
+            filterButton.setTitleColor(.text_1, for: .normal)
+            filterButton.setImage(.init(systemName: "chevron.backward", withConfiguration: config), for: .normal)
+            filterButton.tintColor = .text_1
+            filterButton.semanticContentAttribute = .forceRightToLeft
+            filterButton.imageEdgeInsets = .init(top: 0, left: 4, bottom: 0, right: -4)
+            filterButton.contentEdgeInsets = .init(top: 0, left: 12, bottom: 0, right: 12)
+            filterButton.addAction(UIAction(handler: { [weak self] _ in
+                guard let self, tabType == .playmate else { return }
                 
-                nextTag = nil
-                tableView.mj_header?.beginRefreshing()
+                let panel = LNRoomGameCategoryFilterPanel()
+                let options = buildPlaymateFilterOptions()
+                panel.update(options: options, curSelectedCode: curCode)
+                panel.handler = { [weak self] option in
+                    guard let self else { return }
+                    curCode = option.code
+                    curTitle = option.code.isEmpty ? .init(key: "A00361") : option.name
+                    updateFilterView()
+                    
+                    nextTag = nil
+                    tableView.mj_header?.beginRefreshing()
+                }
+                panel.popup(self)
+            }), for: .touchUpInside)
+            container.addSubview(filterButton)
+            filterButton.snp.makeConstraints { make in
+                make.centerY.trailing.equalToSuperview()
+                make.height.equalTo(30)
+                make.leading.greaterThanOrEqualTo(countLabel.snp.trailing).offset(16)
+                make.width.lessThanOrEqualTo(131)
             }
-            panel.popup(self)
-        }), for: .touchUpInside)
-        container.addSubview(filterButton)
-        filterButton.snp.makeConstraints { make in
-            make.centerY.trailing.equalToSuperview()
-            make.height.equalTo(30)
+            filterButton.setContentHuggingPriority(.required, for: .horizontal)
+            filterButton.setContentCompressionResistancePriority(.required, for: .horizontal)
         }
         
         return container

+ 1 - 1
Lanu/Views/Room/LNRoomOrderGuideView.swift

@@ -132,7 +132,7 @@ private extension LNRoomOrderGuideView {
     func setupViews() {
         backgroundColor = .clear
 
-        dimLayer.fillColor = UIColor.black.withAlphaComponent(0.8).cgColor
+        dimLayer.fillColor = UIColor.black.withAlphaComponent(0.6).cgColor
         dimLayer.fillRule = .evenOdd
         layer.addSublayer(dimLayer)
 

+ 4 - 4
Lanu/Views/Room/Message/LNRoomChatMessageCell.swift → Lanu/Views/Room/Message/Cells/LNRoomChatMessageCell.swift

@@ -24,9 +24,9 @@ class LNRoomChatMessageCell: UITableViewCell {
     }
     
     func update(_ message: LNRoomChatMessageItem) {
-        contentLabel.text = message.text
+        contentLabel.attributedText = message.content.getEmojiString(with: .heading_h5)
         avatarView.sd_setImage(with: URL(string: message.avatar))
-        nameLabel.text = message.name
+        nameLabel.text = message.nickname
         
         curItem = message
     }
@@ -74,7 +74,7 @@ extension LNRoomChatMessageCell {
             make.trailing.lessThanOrEqualToSuperview()
         }
         
-        nameLabel.font = .heading_h5
+        nameLabel.font = .body_s
         nameLabel.textColor = .text_2
         nameLabel.setContentHuggingPriority(.required, for: .vertical)
         nameLabel.onTap { [weak self] in
@@ -98,7 +98,7 @@ extension LNRoomChatMessageCell {
             make.bottom.equalToSuperview()
         }
         
-        contentLabel.font = .body_s
+        contentLabel.font = .heading_h5
         contentLabel.textColor = .text_1
         contentLabel.numberOfLines = 0
         bubble.addSubview(contentLabel)

+ 124 - 0
Lanu/Views/Room/Message/Cells/LNRoomGiftMessageCell.swift

@@ -0,0 +1,124 @@
+//
+//  LNRoomGiftMessageCell.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/3/26.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+class LNRoomGiftMessageCell: UITableViewCell {
+    private let contentLabel = UILabel()
+    private let attachment = NSTextAttachment(image: .icGift)
+    private var curItem: LNRoomGiftMessageItem?
+    
+    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+        super.init(style: style, reuseIdentifier: reuseIdentifier)
+        
+        setupViews()
+        
+        attachment.bounds = .init(x: 0, y: -6.5, width: 22, height: 22)
+    }
+    
+    func update(_ message: LNRoomGiftMessageItem) {
+        curItem = message
+        contentLabel.attributedText = buildContent(message)
+        
+        guard let iconURL = URL(string: message.giftIcon), !message.giftIcon.isEmpty else { return }
+        SDWebImageManager.shared.loadImage(with: iconURL, progress: nil) { [weak self] image, _, error, _, _, _ in
+            guard let self else { return }
+            guard error == nil, let image else { return }
+            guard self.curItem === message else { return }
+            attachment.image = image
+            contentLabel.attributedText = contentLabel.attributedText
+        }
+    }
+    
+    override func prepareForReuse() {
+        super.prepareForReuse()
+        
+        contentLabel.attributedText = nil
+        curItem = nil
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+extension LNRoomGiftMessageCell {
+    private func buildContent(_ message: LNRoomGiftMessageItem, giftImage: UIImage = .icGift) -> NSAttributedString {
+        let text = String(key: "B00130", message.senderName, message.receiverName, message.giftCount)
+        let attr = NSMutableAttributedString(string: text, attributes: [
+            .font: UIFont.heading_h5,
+            .foregroundColor: UIColor.text_1
+        ])
+        
+        let senderRange = (text as NSString).range(of: message.senderName)
+        attr.addAttributes([
+            .font: UIFont.heading_h5,
+            .foregroundColor: UIColor.text_7
+        ], range: senderRange)
+        
+        let receiverRange = (text as NSString).range(of: message.receiverName)
+        attr.addAttributes([
+            .font: UIFont.heading_h5,
+            .foregroundColor: UIColor.text_7
+        ], range: receiverRange)
+        
+        let countRange: NSRange? = if text.contains("x\(message.giftCount)") {
+            (text as NSString).range(of: "x\(message.giftCount)")
+        } else if text.contains("\(message.giftCount)x") {
+            (text as NSString).range(of: "\(message.giftCount)x")
+        } else {
+            nil
+        }
+        if let countRange {
+            attr.addAttributes([
+                .font: UIFont.heading_h5,
+                .foregroundColor: UIColor.text_7
+            ], range: countRange)
+        }
+        
+        let giftIconRange = (text as NSString).range(of: "{icon}")
+        attr.replaceCharacters(in: giftIconRange, with: .init(attachment: attachment))
+        
+        return attr
+    }
+}
+
+extension LNRoomGiftMessageCell {
+    private func setupViews() {
+        backgroundColor = .clear
+        
+        let container = UIView()
+        container.backgroundColor = .fill.withAlphaComponent(0.12)
+        container.layer.cornerRadius = 12
+        container.onTap { [weak self] in
+            guard let self else { return }
+            guard let curItem else { return }
+            let panel = LNRoomProfileCardPanel()
+            panel.load(curItem.sender)
+            panel.popup(self)
+        }
+        contentView.addSubview(container)
+        container.snp.makeConstraints { make in
+            make.verticalEdges.equalToSuperview().inset(8)
+            make.leading.equalToSuperview()
+            make.trailing.lessThanOrEqualToSuperview()
+        }
+        
+        contentLabel.text = " "
+        contentLabel.font = .heading_h5
+        contentLabel.textColor = .text_1
+        contentLabel.numberOfLines = 0
+        container.addSubview(contentLabel)
+        contentLabel.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(10)
+            make.verticalEdges.equalToSuperview().inset(8)
+        }
+    }
+}

+ 0 - 0
Lanu/Views/Room/Message/LNRoomSystemMessageCell.swift → Lanu/Views/Room/Message/Cells/LNRoomSystemMessageCell.swift


+ 0 - 0
Lanu/Views/Room/Message/LNRoomUnknownMessageCell.swift → Lanu/Views/Room/Message/Cells/LNRoomUnknownMessageCell.swift


+ 6 - 5
Lanu/Views/Room/Message/LNRoomWelcomeMessageCell.swift → Lanu/Views/Room/Message/Cells/LNRoomWelcomeMessageCell.swift

@@ -12,7 +12,7 @@ import SnapKit
 
 class LNRoomWelcomeMessageCell: UITableViewCell {
     private let contentLabel = UILabel()
-    private var curItem: LNRoomChatMessageItem?
+    private var curItem: LNRoomWelcomeMessageItem?
     
     override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
         super.init(style: style, reuseIdentifier: reuseIdentifier)
@@ -20,10 +20,10 @@ class LNRoomWelcomeMessageCell: UITableViewCell {
         setupViews()
     }
     
-    func update(_ message: LNRoomChatMessageItem) {
-        let text = String(key: "A00371", message.name)
+    func update(_ message: LNRoomWelcomeMessageItem) {
+        let text = String(key: "A00371", message.nickname)
         let attr = NSMutableAttributedString(string: text)
-        let range = (text as NSString).range(of: message.name)
+        let range = (text as NSString).range(of: message.nickname)
         attr.addAttributes([
             .foregroundColor: UIColor.text_7,
             .font: UIFont.heading_h5
@@ -49,7 +49,7 @@ extension LNRoomWelcomeMessageCell {
             guard let self else { return }
             guard let curItem else { return }
             let panel = LNRoomProfileCardPanel()
-            panel.load(curItem.sender)
+            panel.load(curItem.userNo)
             panel.popup(self)
         }
         contentView.addSubview(container)
@@ -62,6 +62,7 @@ extension LNRoomWelcomeMessageCell {
         contentLabel.text = " "
         contentLabel.font = .heading_h5
         contentLabel.textColor = .text_1
+        contentLabel.numberOfLines = 0
         container.addSubview(contentLabel)
         contentLabel.snp.makeConstraints { make in
             make.horizontalEdges.equalToSuperview().inset(10)

+ 30 - 12
Lanu/Views/Room/Message/LNRoomMessageView.swift

@@ -16,7 +16,7 @@ class LNRoomMessageView: UIView {
     private let maxMessageCount = 300
     private weak var roomSession: LNRoomViewModel?
     
-    private var items: [LNRoomChatMessageItem] = []
+    private var items: [LNRoomMessageItem] = []
     
     override init(frame: CGRect) {
         super.init(frame: frame)
@@ -35,7 +35,7 @@ class LNRoomMessageView: UIView {
 }
 
 extension LNRoomMessageView: LNRoomViewModelNotify {
-    func onRoomMessageChanged(messages: [LNRoomChatMessageItem]) {
+    func onRoomMessageChanged(messages: [LNRoomMessageItem]) {
         items.append(contentsOf: messages)
         if items.count > maxMessageCount {
             items.removeFirst(Int(Double(maxMessageCount) * 0.3))
@@ -53,35 +53,53 @@ extension LNRoomMessageView: UITableViewDataSource {
     func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
         let message = items[indexPath.row]
         
-        switch message.type {
-        case .chat:
+        if let userMessage = message as? LNRoomChatMessageItem {
             let cell = tableView.dequeueReusableCell(withIdentifier: LNRoomChatMessageCell.className, for: indexPath) as! LNRoomChatMessageCell
-            cell.update(message)
+            cell.update(userMessage)
             return cell
-        case .welcome:
+        } else if let welcomeMessage = message as? LNRoomWelcomeMessageItem {
             let cell = tableView.dequeueReusableCell(withIdentifier: LNRoomWelcomeMessageCell.className, for: indexPath) as! LNRoomWelcomeMessageCell
-            cell.update(message)
+            cell.update(welcomeMessage)
+            return cell
+        } else if let giftMessage = message as? LNRoomGiftMessageItem {
+            let cell = tableView.dequeueReusableCell(withIdentifier: LNRoomGiftMessageCell.className, for: indexPath) as! LNRoomGiftMessageCell
+            cell.update(giftMessage)
             return cell
         }
+        
+        return tableView.dequeueReusableCell(withIdentifier: LNRoomUnknownMessageCell.className, for: indexPath)
     }
 }
 
 extension LNRoomMessageView {
     private func setupViews() {
-        tableView.backgroundColor = .clear
+        tableView.dataSource = self
+        tableView.allowsSelection = false
         tableView.separatorStyle = .none
-        tableView.showsVerticalScrollIndicator = false
+        tableView.backgroundColor = .clear
         tableView.alwaysBounceVertical = true
-        tableView.allowsSelection = false
+        tableView.showsVerticalScrollIndicator = false
         tableView.contentInsetAdjustmentBehavior = .never
-        tableView.dataSource = self
-        tableView.register(LNRoomWelcomeMessageCell.self, forCellReuseIdentifier: LNRoomWelcomeMessageCell.className)
+        tableView.contentInset = .init(top: 0, left: 0, bottom: 16, right: 0)
         tableView.register(LNRoomChatMessageCell.self, forCellReuseIdentifier: LNRoomChatMessageCell.className)
+        tableView.register(LNRoomGiftMessageCell.self, forCellReuseIdentifier: LNRoomGiftMessageCell.className)
+        tableView.register(LNRoomSystemMessageCell.self, forCellReuseIdentifier: LNRoomSystemMessageCell.className)
+        tableView.register(LNRoomWelcomeMessageCell.self, forCellReuseIdentifier: LNRoomWelcomeMessageCell.className)
         tableView.register(LNRoomUnknownMessageCell.self, forCellReuseIdentifier: LNRoomUnknownMessageCell.className)
         addSubview(tableView)
         tableView.snp.makeConstraints { make in
             make.edges.equalToSuperview()
         }
+        
+        let bottomGradientView = UIView.gradientView([
+            .init(hex: "#010B2300"), .init(hex: "#010B23")
+        ], .verticalUTD)
+        addSubview(bottomGradientView)
+        bottomGradientView.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.bottom.equalToSuperview()
+            make.height.equalTo(44)
+        }
     }
 }
 

+ 16 - 10
Lanu/Views/Room/Profile/LNRoomProfileBottomMenu.swift

@@ -21,11 +21,9 @@ class LNRoomProfileBottomMenu: UIView {
             if isFollow {
                 follow.setTitle(.init(key: "A00026"), for: .normal)
                 follow.setTitleColor(.text_1.withAlphaComponent(0.2), for: .normal)
-                follow.titleLabel?.font = .body_l
             } else {
                 follow.setTitle(.init(key: "A00225"), for: .normal)
                 follow.setTitleColor(.text_1, for: .normal)
-                follow.titleLabel?.font = .body_l
             }
         }
     }
@@ -48,7 +46,7 @@ class LNRoomProfileBottomMenu: UIView {
         
         follow.setTitle(.init(key: "A00225"), for: .normal)
         follow.setTitleColor(.text_1, for: .normal)
-        follow.titleLabel?.font = .body_l
+        follow.titleLabel?.font = .heading_h3
         follow.addAction(UIAction(handler: { [weak self] _ in
             guard let self else { return }
             guard let curUid else { return }
@@ -63,7 +61,7 @@ class LNRoomProfileBottomMenu: UIView {
         
         chat.setTitle(.init(key: "A00042"), for: .normal)
         chat.setTitleColor(.text_1, for: .normal)
-        chat.titleLabel?.font = .body_l
+        chat.titleLabel?.font = .heading_h3
         chat.addAction(UIAction(handler: { [weak self] _ in
             guard let self else { return }
             guard let curUid else { return }
@@ -71,13 +69,21 @@ class LNRoomProfileBottomMenu: UIView {
             pushToChat(uid: curUid)
         }), for: .touchUpInside)
         stackView.addArrangedSubview(chat)
+        addSeperator(chat)
         
-//        addSeperator(chat)
-//        
-//        gift.setTitle(.init(key: "A00336"), for: .normal)
-//        gift.setTitleColor(.text_6, for: .normal)
-//        gift.titleLabel?.font = .body_l
-//        stackView.addArrangedSubview(gift)
+        gift.setTitle(.init(key: "A00336"), for: .normal)
+        gift.setTitleColor(.text_6, for: .normal)
+        gift.titleLabel?.font = .heading_h3
+        gift.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            guard let curUid else { return }
+            panel?.dismiss()
+            
+            let panel = LNRoomGiftPanel()
+            panel.update(LNRoomManager.shared.curRoom, selectedUid: curUid)
+            panel.popup(self)
+        }), for: .touchUpInside)
+        stackView.addArrangedSubview(gift)
         
         LNEventDeliver.addObserver(self)
     }

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików