修改bug
This commit is contained in:
parent
7e10b77d10
commit
5501345a7e
@ -16,7 +16,7 @@ class SCVariant1Config implements AppConfig {
|
||||
@override
|
||||
String get apiHost => const String.fromEnvironment(
|
||||
'API_HOST',
|
||||
defaultValue: 'https://jvapi.haiyihy.com/',
|
||||
defaultValue: 'http://192.168.110.64:1100/',
|
||||
); // 默认连本地接口,本地调试可通过 --dart-define=API_HOST 覆盖,线上“https://jvapi.haiyihy.com/”
|
||||
|
||||
@override
|
||||
|
||||
@ -168,8 +168,7 @@ class SCGlobalConfig {
|
||||
}
|
||||
|
||||
static String clampFloatingBroadcastScope(String value) {
|
||||
final scope = normalizeFloatingBroadcastScope(value);
|
||||
return _isLowPerformanceDevice ? floatingBroadcastScopeOff : scope;
|
||||
return normalizeFloatingBroadcastScope(value);
|
||||
}
|
||||
|
||||
static String floatingBroadcastScopeStorageKey(String? account) {
|
||||
@ -188,8 +187,7 @@ class SCGlobalConfig {
|
||||
|
||||
static bool get isLowPerformanceDevice => _isLowPerformanceDevice;
|
||||
|
||||
static bool get allowsHighCostAnimations =>
|
||||
!_isLowPerformanceDevice && sdkInt > maxSdkNoAnim;
|
||||
static bool get allowsHighCostAnimations => true;
|
||||
|
||||
static int get recommendedImageCacheBytes =>
|
||||
_isLowPerformanceDevice ? 40 * 1024 * 1024 : 100 * 1024 * 1024;
|
||||
@ -211,19 +209,17 @@ class SCGlobalConfig {
|
||||
isLowRamDevice ||
|
||||
processorCount <= 6 ||
|
||||
matchesKnownLowPerformanceModel;
|
||||
resetVisualEffectSwitchesToRecommendedDefaults();
|
||||
}
|
||||
|
||||
static bool clampVisualEffectPreference(bool enabled) {
|
||||
return !_isLowPerformanceDevice && enabled;
|
||||
return enabled;
|
||||
}
|
||||
|
||||
static void resetVisualEffectSwitchesToRecommendedDefaults() {
|
||||
final enabled = !_isLowPerformanceDevice;
|
||||
isEntryVehicleAnimation = enabled;
|
||||
isGiftSpecialEffects = enabled;
|
||||
isFloatingAnimationInGlobal = enabled;
|
||||
isLuckGiftSpecialEffects = enabled;
|
||||
isEntryVehicleAnimation = true;
|
||||
isGiftSpecialEffects = true;
|
||||
isFloatingAnimationInGlobal = true;
|
||||
isLuckGiftSpecialEffects = true;
|
||||
}
|
||||
|
||||
/// 获取当前业务逻辑策略
|
||||
|
||||
@ -23,7 +23,6 @@ import 'package:yumi/shared/tools/sc_message_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_user_id_utils.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_vip_res.dart';
|
||||
import 'package:yumi/services/audio/rtm_manager.dart';
|
||||
import 'package:marquee/marquee.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@ -59,7 +58,6 @@ import 'package:yumi/shared/tools/sc_message_notifier.dart';
|
||||
import 'package:yumi/shared/tools/sc_path_utils.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_vip_repository_imp.dart';
|
||||
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||||
import 'package:yumi/modules/index/main_route.dart';
|
||||
import 'package:yumi/ui_kit/widgets/sc_nine_patch_image.dart';
|
||||
@ -81,29 +79,6 @@ const EdgeInsets _vipBubbleCenterSliceRatio = EdgeInsets.fromLTRB(
|
||||
);
|
||||
const EdgeInsets _vipBubbleSourceEdgeInset = EdgeInsets.all(2);
|
||||
|
||||
String _resolveVipChatBubbleImageUrl(SCVipResourceRes? resource) {
|
||||
final candidates = <String?>[
|
||||
resource?.sourceResourceUrl,
|
||||
resource?.previewUrl,
|
||||
resource?.coverUrl,
|
||||
resource?.cover,
|
||||
];
|
||||
return _firstNonBlankChatBubbleUrl([
|
||||
...candidates.where((value) => SCNetworkSvgaWidget.isSvga(value)),
|
||||
...candidates,
|
||||
]);
|
||||
}
|
||||
|
||||
String _firstNonBlankChatBubbleUrl(Iterable<String?> values) {
|
||||
for (final value in values) {
|
||||
final text = value?.trim();
|
||||
if (text != null && text.isNotEmpty) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
class SCMessageChatPage extends StatefulWidget {
|
||||
final V2TimConversation? conversation;
|
||||
final bool shrinkWrap;
|
||||
@ -144,8 +119,6 @@ class _SCMessageChatPageState extends State<SCMessageChatPage> {
|
||||
///互相关注才能互相发送消息
|
||||
bool canSendMsg = false;
|
||||
SocialChatUserProfile? friend;
|
||||
SCVipResourceRes? _selfVipChatBubble;
|
||||
bool _isSelfVipChatBubbleLoading = false;
|
||||
final Map<String, _CpInviteMessageState> _localCpInviteStates = {};
|
||||
|
||||
///是否显示发送按钮
|
||||
@ -196,7 +169,6 @@ class _SCMessageChatPageState extends State<SCMessageChatPage> {
|
||||
rtmProvider?.onRevokeMessageListener = _onRevokeMessage;
|
||||
rtmProvider?.onNewMessageCurrentConversationListener = _onNewMessage;
|
||||
loadMsg();
|
||||
_loadSelfVipChatBubble();
|
||||
|
||||
if (_isC2CConversation && friend == null) {
|
||||
loadFriend();
|
||||
@ -385,171 +357,6 @@ class _SCMessageChatPageState extends State<SCMessageChatPage> {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _loadSelfVipChatBubble() async {
|
||||
if (_isSelfVipChatBubbleLoading) return;
|
||||
final currentUserId = AccountStorage().getCurrentUser()?.userProfile?.id;
|
||||
if (currentUserId == null || currentUserId.isEmpty) return;
|
||||
|
||||
_isSelfVipChatBubbleLoading = true;
|
||||
final repository = SCVipRepositoryImp();
|
||||
SCVipHomeRes? vipHome;
|
||||
SCVipResourceRes? chatBubble;
|
||||
|
||||
try {
|
||||
final home = await repository.vipHome();
|
||||
vipHome = home;
|
||||
chatBubble = _resolveSelfVipChatBubbleFromHome(home);
|
||||
} catch (error) {}
|
||||
|
||||
if (chatBubble == null) {
|
||||
try {
|
||||
final status = await repository.vipStatus();
|
||||
chatBubble = _resolveSelfVipChatBubbleFromStatus(status);
|
||||
if (chatBubble == null &&
|
||||
vipHome != null &&
|
||||
_isUsableVipStatus(status)) {
|
||||
chatBubble = _firstUsableVipChatBubble([
|
||||
_currentVipLevelConfig(
|
||||
vipHome,
|
||||
targetLevel: status.levelInt,
|
||||
targetLevelCode: status.levelCode,
|
||||
)?.chatBubble,
|
||||
]);
|
||||
}
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_selfVipChatBubble = chatBubble;
|
||||
_isSelfVipChatBubbleLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
SCVipResourceRes? _resolveSelfVipChatBubbleFromHome(SCVipHomeRes home) {
|
||||
final status = home.state;
|
||||
if (status?.active == false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final config = _currentVipLevelConfig(home);
|
||||
if (_isUsableVipStatus(status)) {
|
||||
return _firstUsableVipChatBubble([
|
||||
status?.chatBubble,
|
||||
config?.chatBubble,
|
||||
]);
|
||||
}
|
||||
|
||||
final profileLevel =
|
||||
AccountStorage().getCurrentUser()?.userProfile?.vipLevelForColoredId ??
|
||||
0;
|
||||
if (profileLevel > 0) {
|
||||
return _firstUsableVipChatBubble([config?.chatBubble]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
SCVipResourceRes? _resolveSelfVipChatBubbleFromStatus(SCVipStatusRes status) {
|
||||
if (!_isUsableVipStatus(status)) {
|
||||
return null;
|
||||
}
|
||||
return _firstUsableVipChatBubble([status.chatBubble]);
|
||||
}
|
||||
|
||||
SCVipLevelConfigRes? _currentVipLevelConfig(
|
||||
SCVipHomeRes home, {
|
||||
int? targetLevel,
|
||||
String? targetLevelCode,
|
||||
}) {
|
||||
final status = home.state;
|
||||
final statusLevel = _vipLevelIntFromStatus(status);
|
||||
final profileLevel =
|
||||
AccountStorage().getCurrentUser()?.userProfile?.vipLevelForColoredId ??
|
||||
0;
|
||||
final resolvedTargetLevel =
|
||||
(targetLevel ?? 0) > 0
|
||||
? targetLevel!
|
||||
: (statusLevel > 0 ? statusLevel : profileLevel);
|
||||
final statusLevelCode =
|
||||
targetLevelCode?.trim() ?? status?.levelCode?.trim();
|
||||
|
||||
for (final level in home.levels) {
|
||||
if (resolvedTargetLevel > 0 && level.levelInt == resolvedTargetLevel) {
|
||||
return level;
|
||||
}
|
||||
}
|
||||
if (statusLevelCode != null && statusLevelCode.isNotEmpty) {
|
||||
for (final level in home.levels) {
|
||||
if (_sameVipLevelCode(level.levelCode, statusLevelCode)) {
|
||||
return level;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
bool _isUsableVipStatus(SCVipStatusRes? status) {
|
||||
return status != null &&
|
||||
status.active != false &&
|
||||
(_vipLevelIntFromStatus(status) > 0 ||
|
||||
_hasNonBlankText(status.levelCode) ||
|
||||
_hasNonBlankText(status.displayName) ||
|
||||
_resolveVipChatBubbleImageUrl(status.chatBubble).isNotEmpty);
|
||||
}
|
||||
|
||||
int _vipLevelIntFromStatus(SCVipStatusRes? status) {
|
||||
final explicitLevel = status?.levelInt ?? 0;
|
||||
if (explicitLevel > 0) {
|
||||
return explicitLevel;
|
||||
}
|
||||
return _parseVipLevelText(status?.levelCode) ??
|
||||
_parseVipLevelText(status?.displayName) ??
|
||||
0;
|
||||
}
|
||||
|
||||
int? _parseVipLevelText(String? value) {
|
||||
final text = value?.trim();
|
||||
if (text == null || text.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final match = RegExp(
|
||||
r'(?:S?VIP)?\s*([1-9]\d*)',
|
||||
caseSensitive: false,
|
||||
).firstMatch(text);
|
||||
return int.tryParse(match?.group(1) ?? '');
|
||||
}
|
||||
|
||||
bool _sameVipLevelCode(String? left, String? right) {
|
||||
final normalizedLeft = left?.replaceAll(RegExp(r'\s+'), '').toLowerCase();
|
||||
final normalizedRight = right?.replaceAll(RegExp(r'\s+'), '').toLowerCase();
|
||||
return normalizedLeft != null &&
|
||||
normalizedLeft.isNotEmpty &&
|
||||
normalizedLeft == normalizedRight;
|
||||
}
|
||||
|
||||
bool _hasNonBlankText(String? value) {
|
||||
return value?.trim().isNotEmpty ?? false;
|
||||
}
|
||||
|
||||
SCVipResourceRes? _firstUsableVipChatBubble(
|
||||
Iterable<SCVipResourceRes?> resources,
|
||||
) {
|
||||
for (final resource in resources) {
|
||||
if (_resolveVipChatBubbleImageUrl(resource).isNotEmpty) {
|
||||
return resource;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String _vipChatBubbleLogValue(SCVipResourceRes? resource) {
|
||||
if (resource == null) return 'null';
|
||||
return '{id:${resource.resourceId},name:${resource.name},'
|
||||
'previewUrl:${resource.previewUrl},'
|
||||
'sourceUrl:${resource.sourceResourceUrl},'
|
||||
'selectedUrl:${_resolveVipChatBubbleImageUrl(resource)}}';
|
||||
}
|
||||
|
||||
///消息列表
|
||||
Widget _msgList() {
|
||||
return SmartRefresher(
|
||||
@ -595,7 +402,6 @@ class _SCMessageChatPageState extends State<SCMessageChatPage> {
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(c, i) => _MessageItem(
|
||||
message: currentConversationMessageList[i],
|
||||
selfVipChatBubble: _selfVipChatBubble,
|
||||
currentConversation: currentConversation,
|
||||
preMessage:
|
||||
i > 0 ? currentConversationMessageList[i - 1] : null,
|
||||
@ -1182,7 +988,6 @@ class _MessageItem extends StatelessWidget {
|
||||
|
||||
final V2TimMessage message;
|
||||
final V2TimMessage? preMessage;
|
||||
final SCVipResourceRes? selfVipChatBubble;
|
||||
final V2TimConversation? currentConversation;
|
||||
|
||||
BusinessLogicStrategy get _strategy => SCGlobalConfig.businessLogicStrategy;
|
||||
@ -1201,7 +1006,6 @@ class _MessageItem extends StatelessWidget {
|
||||
|
||||
_MessageItem({
|
||||
required this.message,
|
||||
this.selfVipChatBubble,
|
||||
this.currentConversation,
|
||||
this.preMessage,
|
||||
// this.userProfile,
|
||||
@ -2827,24 +2631,18 @@ class _MessageItem extends StatelessWidget {
|
||||
if (chatBubbleUrl.isNotEmpty) {
|
||||
return _buildVipTextBubble(content: content, imageUrl: chatBubbleUrl);
|
||||
}
|
||||
if (message.isSelf ?? false) {
|
||||
final vipChatBubbleUrl = _resolveVipChatBubbleImageUrl(selfVipChatBubble);
|
||||
if (vipChatBubbleUrl.isNotEmpty) {
|
||||
return _buildVipTextBubble(
|
||||
content: content,
|
||||
imageUrl: vipChatBubbleUrl,
|
||||
);
|
||||
}
|
||||
}
|
||||
return _buildDefaultTextBubble(content);
|
||||
}
|
||||
|
||||
PropsResources? _currentUserChatBubble() {
|
||||
final memoryBubble =
|
||||
AccountStorage().getCurrentUser()?.userProfile?.getChatBox();
|
||||
final currentUser = AccountStorage().getCurrentUser();
|
||||
final memoryBubble = currentUser?.userProfile?.getChatBox();
|
||||
if (_resolveChatBubbleImageUrl(memoryBubble).isNotEmpty) {
|
||||
return memoryBubble;
|
||||
}
|
||||
if (currentUser?.userProfile != null) {
|
||||
return memoryBubble;
|
||||
}
|
||||
|
||||
final userJson = DataPersistence.getCurrentUser();
|
||||
if (userJson.isEmpty) {
|
||||
|
||||
@ -1900,16 +1900,6 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
'animationCount=$animationCount',
|
||||
);
|
||||
SCGiftVapSvgaManager().play(gift.giftSourceUrl!);
|
||||
} else if (!_isCpGift(gift) &&
|
||||
SCGlobalConfig.isLowPerformanceDevice &&
|
||||
SCGlobalConfig.isGiftSpecialEffects &&
|
||||
(rtcProvider?.shouldShowRoomVisualEffects ?? false)) {
|
||||
_enqueueLowPerformanceGiftCompensation(
|
||||
gift: gift,
|
||||
receiver: u.user,
|
||||
quantity: quantity,
|
||||
coins: coins,
|
||||
);
|
||||
} else {
|
||||
_giftFxLog(
|
||||
'skip local play because visual effects disabled '
|
||||
@ -1936,52 +1926,10 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
}
|
||||
}
|
||||
|
||||
void _enqueueLowPerformanceGiftCompensation({
|
||||
required SocialChatGiftRes gift,
|
||||
required SocialChatUserProfile? receiver,
|
||||
required int quantity,
|
||||
required num coins,
|
||||
}) {
|
||||
final sender = AccountStorage().getCurrentUser()?.userProfile;
|
||||
final roomId = rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? "";
|
||||
_enqueueGiftFloatingMessage(
|
||||
SCFloatingMessage(
|
||||
type: 1,
|
||||
priority: 900,
|
||||
userId: sender?.id ?? "",
|
||||
toUserId: receiver?.id ?? "",
|
||||
userAvatarUrl: sender?.userAvatar ?? "",
|
||||
toUserAvatarUrl: receiver?.userAvatar ?? "",
|
||||
userName: sender?.userNickname ?? "",
|
||||
toUserName: receiver?.userNickname ?? "",
|
||||
giftUrl: gift.giftPhoto ?? "",
|
||||
giftId: gift.id,
|
||||
giftTab: _floatingGiftTabFor(gift),
|
||||
number: quantity,
|
||||
coins: coins,
|
||||
roomId: roomId,
|
||||
),
|
||||
dedupKey: _giftFloatingDedupKey(
|
||||
roomId: roomId,
|
||||
senderId: sender?.id,
|
||||
receiverId: receiver?.id,
|
||||
giftId: gift.id,
|
||||
quantity: quantity,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _enqueueGiftFloatingMessage(
|
||||
SCFloatingMessage message, {
|
||||
String? dedupKey,
|
||||
}) {
|
||||
if (SCGlobalConfig.isLowPerformanceDevice) {
|
||||
OverlayManager().addLowPerformanceCompensationMessage(
|
||||
message,
|
||||
dedupKey: dedupKey,
|
||||
);
|
||||
return;
|
||||
}
|
||||
OverlayManager().addMessage(message);
|
||||
}
|
||||
|
||||
|
||||
@ -6,24 +6,24 @@ class BaishunLoadingView extends StatelessWidget {
|
||||
const BaishunLoadingView({
|
||||
super.key,
|
||||
this.message = 'Loading game...',
|
||||
this.backgroundColor = const Color(0xFF081915),
|
||||
});
|
||||
|
||||
final String message;
|
||||
final Color backgroundColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Positioned.fill(
|
||||
child: Container(
|
||||
color: Colors.black.withValues(alpha: 0.42),
|
||||
color: backgroundColor,
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 22.w, vertical: 18.w),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF102D28).withValues(alpha: 0.92),
|
||||
borderRadius: BorderRadius.circular(16.w),
|
||||
border: Border.all(
|
||||
color: Colors.white.withValues(alpha: 0.1),
|
||||
),
|
||||
border: Border.all(color: Colors.white.withValues(alpha: 0.1)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
||||
@ -295,6 +295,9 @@ class _BagsChatboxPageState
|
||||
res.propsResources?.id ?? "",
|
||||
unload,
|
||||
);
|
||||
try {
|
||||
await profileManager.fetchUserProfileData(loadGuardCount: false);
|
||||
} catch (_) {}
|
||||
if (!mounted) {
|
||||
return false;
|
||||
}
|
||||
@ -305,8 +308,11 @@ class _BagsChatboxPageState
|
||||
myChatbox = null;
|
||||
SCTts.show(localizations.successfullyUnloaded);
|
||||
}
|
||||
profileManager.syncCurrentUserChatBubble(
|
||||
unload ? null : res.propsResources,
|
||||
expireTime: res.expireTime?.toString(),
|
||||
);
|
||||
setState(() {});
|
||||
profileManager.fetchUserProfileData(loadGuardCount: false);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
|
||||
@ -13,8 +13,6 @@ import 'package:yumi/shared/tools/sc_room_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_top_layer_guard.dart';
|
||||
import 'package:yumi/shared/tools/sc_cp_relation_notice_utils.dart';
|
||||
import 'package:yumi/shared/data_sources/models/message/room_rocket_launch_broadcast_message.dart';
|
||||
import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_vip_repository_imp.dart';
|
||||
@ -3864,46 +3862,6 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
return "";
|
||||
}
|
||||
|
||||
String _resolveEntryCompensationCover(PropsResources? resource) {
|
||||
return _firstNonBlankEntryResource([
|
||||
resource?.roomSendCoverUrl,
|
||||
resource?.roomOpenedUrl,
|
||||
resource?.cover,
|
||||
resource?.roomNotOpenedUrl,
|
||||
]);
|
||||
}
|
||||
|
||||
void _enqueueLowPerformanceEntryCompensation({
|
||||
required Msg joinMsg,
|
||||
required PropsResources resource,
|
||||
required String roomId,
|
||||
}) {
|
||||
if (!SCGlobalConfig.isLowPerformanceDevice ||
|
||||
!shouldShowRoomVisualEffects ||
|
||||
roomId.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final coverUrl = _resolveEntryCompensationCover(resource);
|
||||
OverlayManager().addLowPerformanceCompensationMessage(
|
||||
SCFloatingMessage(
|
||||
type: 5,
|
||||
priority: 950,
|
||||
userId: joinMsg.user?.id ?? "",
|
||||
roomId: roomId,
|
||||
userAvatarUrl: joinMsg.user?.userAvatar ?? "",
|
||||
userName: joinMsg.user?.userNickname ?? "",
|
||||
giftUrl: coverUrl,
|
||||
fallbackUrl: coverUrl,
|
||||
),
|
||||
dedupKey: [
|
||||
"entry",
|
||||
roomId,
|
||||
joinMsg.user?.id ?? "",
|
||||
resource.id ?? "",
|
||||
].join("|"),
|
||||
);
|
||||
}
|
||||
|
||||
void _playRoomEntryEffects(
|
||||
Msg joinMsg, {
|
||||
required int entryRequestSerial,
|
||||
@ -3931,24 +3889,6 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
type: SCGiftVapSvgaManager.entryEffectType,
|
||||
);
|
||||
});
|
||||
} else if (sourceUrl.isNotEmpty &&
|
||||
SCGlobalConfig.isLowPerformanceDevice &&
|
||||
SCGlobalConfig.isEntryVehicleAnimation &&
|
||||
shouldShowRoomVisualEffects &&
|
||||
_isCurrentRoomEntrySession(entryRequestSerial, roomId)) {
|
||||
_roomEntryEffectTimer?.cancel();
|
||||
_roomEntryEffectTimer = Timer(const Duration(milliseconds: 550), () {
|
||||
_roomEntryEffectTimer = null;
|
||||
if (!shouldShowRoomVisualEffects ||
|
||||
!_isCurrentRoomEntrySession(entryRequestSerial, roomId)) {
|
||||
return;
|
||||
}
|
||||
_enqueueLowPerformanceEntryCompensation(
|
||||
joinMsg: joinMsg,
|
||||
resource: entryResource,
|
||||
roomId: roomId,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (rtmProvider?.msgUserJoinListener != null) {
|
||||
@ -5296,11 +5236,10 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
);
|
||||
|
||||
final targetIndex = micGoUpRes.micIndex ?? index;
|
||||
final selfMicMuted = micGoUpRes.micMute ?? false;
|
||||
final switched = await _switchRoomRtcToBroadcaster(
|
||||
muted: (micGoUpRes.micMute ?? false) || isMic,
|
||||
keepAudioPublishing:
|
||||
((micGoUpRes.micMute ?? false) || isMic) &&
|
||||
_currentUserPublishingRoomMusic,
|
||||
muted: selfMicMuted,
|
||||
keepAudioPublishing: selfMicMuted && _currentUserPublishingRoomMusic,
|
||||
);
|
||||
if (!switched) {
|
||||
await _switchRoomRtcToAudience();
|
||||
@ -5356,6 +5295,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
roomToken: micGoUpRes.roomToken,
|
||||
);
|
||||
}
|
||||
isMic = selfMicMuted;
|
||||
_syncSelfMicRuntimeState();
|
||||
if (roomWheatMap[targetIndex]?.micMute ?? false) {
|
||||
///房主上麦自动解禁麦位
|
||||
|
||||
@ -256,13 +256,6 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
SCFloatingMessage message, {
|
||||
String? dedupKey,
|
||||
}) {
|
||||
if (SCGlobalConfig.isLowPerformanceDevice) {
|
||||
OverlayManager().addLowPerformanceCompensationMessage(
|
||||
message,
|
||||
dedupKey: dedupKey,
|
||||
);
|
||||
return;
|
||||
}
|
||||
OverlayManager().addMessage(message);
|
||||
}
|
||||
|
||||
@ -283,23 +276,6 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
].join("|");
|
||||
}
|
||||
|
||||
String _entryFloatingDedupKey({
|
||||
required String? roomId,
|
||||
required String? userId,
|
||||
required String? resourceId,
|
||||
}) {
|
||||
return ["entry", roomId ?? "", userId ?? "", resourceId ?? ""].join("|");
|
||||
}
|
||||
|
||||
String _resolveEntryCompensationCover(PropsResources? resource) {
|
||||
return _firstNonBlank([
|
||||
resource?.roomSendCoverUrl,
|
||||
resource?.roomOpenedUrl,
|
||||
resource?.cover,
|
||||
resource?.roomNotOpenedUrl,
|
||||
]);
|
||||
}
|
||||
|
||||
String _firstNonBlank(Iterable<String?> values) {
|
||||
for (final value in values) {
|
||||
final text = value?.trim();
|
||||
@ -6387,29 +6363,6 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
priority: SCGiftVapSvgaManager.entryEffectPriority,
|
||||
type: SCGiftVapSvgaManager.entryEffectType,
|
||||
);
|
||||
} else if (SCGlobalConfig.isLowPerformanceDevice &&
|
||||
SCGlobalConfig.isEntryVehicleAnimation &&
|
||||
shouldShowRoomVisualEffects) {
|
||||
final roomId =
|
||||
rtcProvider.currenRoom?.roomProfile?.roomProfile?.id ?? "";
|
||||
final coverUrl = _resolveEntryCompensationCover(entryResource);
|
||||
OverlayManager().addLowPerformanceCompensationMessage(
|
||||
SCFloatingMessage(
|
||||
type: 5,
|
||||
priority: 950,
|
||||
userId: msg.user?.id ?? "",
|
||||
roomId: roomId,
|
||||
userAvatarUrl: msg.user?.userAvatar ?? "",
|
||||
userName: msg.user?.userNickname ?? "",
|
||||
giftUrl: coverUrl,
|
||||
fallbackUrl: coverUrl,
|
||||
),
|
||||
dedupKey: _entryFloatingDedupKey(
|
||||
roomId: roomId,
|
||||
userId: msg.user?.id,
|
||||
resourceId: entryResource.id,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (msg.type == SCRoomMsgType.gift) {
|
||||
@ -6460,46 +6413,6 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
'giftId=${gift.id} giftName=${gift.giftName}',
|
||||
);
|
||||
SCGiftVapSvgaManager().play(giftSourceUrl);
|
||||
} else if (!scIsCpGift(gift) &&
|
||||
SCGlobalConfig.isLowPerformanceDevice &&
|
||||
SCGlobalConfig.isGiftSpecialEffects &&
|
||||
rtcProvider.shouldShowRoomVisualEffects) {
|
||||
final roomId =
|
||||
(msg.msg?.trim().isNotEmpty ?? false)
|
||||
? msg.msg ?? ""
|
||||
: rtcProvider
|
||||
.currenRoom
|
||||
?.roomProfile
|
||||
?.roomProfile
|
||||
?.id ??
|
||||
"";
|
||||
final quantity = msg.number ?? 0;
|
||||
final coins = quantity * (gift.giftCandy ?? 0);
|
||||
_enqueueGiftFloatingMessage(
|
||||
SCFloatingMessage(
|
||||
type: 1,
|
||||
priority: 900,
|
||||
userId: msg.user?.id ?? "",
|
||||
toUserId: msg.toUser?.id ?? "",
|
||||
userAvatarUrl: msg.user?.userAvatar ?? "",
|
||||
userName: msg.user?.userNickname ?? "",
|
||||
toUserName: msg.toUser?.userNickname ?? "",
|
||||
toUserAvatarUrl: msg.toUser?.userAvatar ?? "",
|
||||
giftUrl: gift.giftPhoto,
|
||||
giftId: gift.id,
|
||||
giftTab: _floatingGiftTabFor(gift),
|
||||
number: quantity,
|
||||
coins: coins,
|
||||
roomId: roomId,
|
||||
),
|
||||
dedupKey: _giftFloatingDedupKey(
|
||||
roomId: roomId,
|
||||
senderId: msg.user?.id,
|
||||
receiverId: msg.toUser?.id,
|
||||
giftId: gift.id,
|
||||
quantity: quantity,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
_giftFxLog(
|
||||
'skip player play because visual effects disabled '
|
||||
|
||||
@ -14,6 +14,7 @@ import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/room_user_card_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_user_identity_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_vip_res.dart';
|
||||
import 'package:yumi/shared/data_sources/models/enum/sc_props_type.dart';
|
||||
import 'package:yumi/shared/tools/sc_cp_relation_level_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_cp_relation_notice_utils.dart';
|
||||
|
||||
@ -92,6 +93,32 @@ class SocialChatUserProfileManager extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void syncCurrentUserChatBubble(
|
||||
PropsResources? chatBubble, {
|
||||
String? expireTime,
|
||||
}) {
|
||||
final profile = currentUserProfile;
|
||||
if (profile == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final useProps = <UseProps>[
|
||||
...?profile.useProps?.where(
|
||||
(item) => item.propsResources?.type != SCPropsType.CHAT_BUBBLE.name,
|
||||
),
|
||||
];
|
||||
if (chatBubble != null) {
|
||||
useProps.add(
|
||||
UseProps(
|
||||
expireTime: expireTime ?? "4102444800000",
|
||||
propsResources: chatBubble,
|
||||
userId: profile.id,
|
||||
),
|
||||
);
|
||||
}
|
||||
syncCurrentUserProfile(profile.copyWith(useProps: useProps));
|
||||
}
|
||||
|
||||
Future fetchUserProfileData({
|
||||
bool loadGuardCount = true,
|
||||
bool refreshFamilyData = false,
|
||||
|
||||
@ -26,9 +26,7 @@ import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart';
|
||||
typedef FloatingScreenManager = OverlayManager;
|
||||
|
||||
class OverlayManager {
|
||||
static const int _maxLowPerformanceCompensationQueueLength = 8;
|
||||
static const int _maxDeferredRegionRedPacketCount = 12;
|
||||
static const Duration _compensationDedupWindow = Duration(seconds: 3);
|
||||
static const Duration _redPacketFloatingDedupWindow = Duration(minutes: 30);
|
||||
static const Duration _rocketFloatingFallbackDedupWindow = Duration(
|
||||
seconds: 30,
|
||||
@ -43,7 +41,6 @@ class OverlayManager {
|
||||
(a, b) => b.priority.compareTo(a.priority),
|
||||
);
|
||||
final List<SCFloatingMessage> _deferredRegionRedPacketMessages = [];
|
||||
final Map<String, DateTime> _recentCompensationKeys = {};
|
||||
final Map<String, DateTime> _recentFloatingKeys = {};
|
||||
final Map<int, OverlayEntry> _stackedOverlayEntries = {};
|
||||
final Map<int, SCFloatingMessage> _stackedMessages = {};
|
||||
@ -84,28 +81,6 @@ class OverlayManager {
|
||||
}
|
||||
}
|
||||
|
||||
void addLowPerformanceCompensationMessage(
|
||||
SCFloatingMessage message, {
|
||||
String? dedupKey,
|
||||
}) {
|
||||
if (_isDisposed ||
|
||||
_isSuppressed ||
|
||||
!SCGlobalConfig.isLowPerformanceDevice ||
|
||||
SCGlobalConfig.isFloatingBroadcastClosed) {
|
||||
return;
|
||||
}
|
||||
final key = dedupKey ?? _compensationKeyFor(message);
|
||||
if (_isRecentCompensationDuplicate(key)) {
|
||||
return;
|
||||
}
|
||||
_rememberCompensationKey(key);
|
||||
_trimLowPerformanceCompensationQueue();
|
||||
_enqueueMessage(
|
||||
message,
|
||||
debugLabel: 'low_performance_compensation_${message.type ?? -1}',
|
||||
);
|
||||
}
|
||||
|
||||
void _enqueueMessage(
|
||||
SCFloatingMessage message, {
|
||||
required String debugLabel,
|
||||
@ -123,27 +98,6 @@ class OverlayManager {
|
||||
);
|
||||
}
|
||||
|
||||
bool _isRecentCompensationDuplicate(String key) {
|
||||
_pruneRecentCompensationKeys();
|
||||
final recentTime = _recentCompensationKeys[key];
|
||||
if (recentTime == null) {
|
||||
return false;
|
||||
}
|
||||
return DateTime.now().difference(recentTime) < _compensationDedupWindow;
|
||||
}
|
||||
|
||||
void _rememberCompensationKey(String key) {
|
||||
_recentCompensationKeys[key] = DateTime.now();
|
||||
_pruneRecentCompensationKeys();
|
||||
}
|
||||
|
||||
void _pruneRecentCompensationKeys() {
|
||||
final now = DateTime.now();
|
||||
_recentCompensationKeys.removeWhere(
|
||||
(_, time) => now.difference(time) > _compensationDedupWindow,
|
||||
);
|
||||
}
|
||||
|
||||
bool _isRecentFloatingDuplicate(SCFloatingMessage message) {
|
||||
_pruneRecentFloatingKeys();
|
||||
final key = _floatingDedupKeyFor(message);
|
||||
@ -261,33 +215,6 @@ class OverlayManager {
|
||||
return _redPacketFloatingDedupWindow;
|
||||
}
|
||||
|
||||
String _compensationKeyFor(SCFloatingMessage message) {
|
||||
return [
|
||||
message.type,
|
||||
message.roomId,
|
||||
message.userId,
|
||||
message.toUserId,
|
||||
message.giftId,
|
||||
message.number,
|
||||
message.giftUrl,
|
||||
].join('|');
|
||||
}
|
||||
|
||||
void _trimLowPerformanceCompensationQueue() {
|
||||
if (_messageQueue.length < _maxLowPerformanceCompensationQueueLength) {
|
||||
return;
|
||||
}
|
||||
final retained =
|
||||
_messageQueue.unorderedElements.cast<SCFloatingMessage>().toList()
|
||||
..sort((a, b) => b.priority.compareTo(a.priority));
|
||||
_messageQueue.clear();
|
||||
for (final message in retained.take(
|
||||
_maxLowPerformanceCompensationQueueLength - 1,
|
||||
)) {
|
||||
_messageQueue.add(message);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _warmMessageImages(SCFloatingMessage message) async {
|
||||
unawaited(
|
||||
warmImageResource(
|
||||
@ -671,7 +598,7 @@ class OverlayManager {
|
||||
SCGlobalConfig.floatingBroadcastScope = scope;
|
||||
return;
|
||||
}
|
||||
final defaultEffectsEnabled = !SCGlobalConfig.isLowPerformanceDevice;
|
||||
const defaultEffectsEnabled = true;
|
||||
final isLegacyFloatingAnimationInGlobal = DataPersistence.getBool(
|
||||
SCGlobalConfig.legacyFloatingAnimationInGlobalStorageKey(account),
|
||||
defaultValue: defaultEffectsEnabled,
|
||||
@ -752,7 +679,6 @@ class OverlayManager {
|
||||
_removeAllStackedMessages();
|
||||
_messageQueue.clear();
|
||||
_deferredRegionRedPacketMessages.clear();
|
||||
_recentCompensationKeys.clear();
|
||||
_isPlaying = false;
|
||||
_isProcessing = false;
|
||||
}
|
||||
|
||||
@ -1,14 +1,20 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:audioplayers/audioplayers.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
|
||||
|
||||
class SCLuckyGiftWinSoundPlayer {
|
||||
static const String assetPath =
|
||||
"sc_images/room/anim/luck_gift/lucky_gift_win.mp3";
|
||||
static const Duration _sameEventDedupeWindow = Duration(seconds: 3);
|
||||
static const Duration _stopDelay = Duration(milliseconds: 3200);
|
||||
static const double _volume = 0.75;
|
||||
|
||||
static final AudioPlayer _player = AudioPlayer();
|
||||
static String? _lastEventKey;
|
||||
static DateTime? _lastPlayedAt;
|
||||
static bool _loaded = false;
|
||||
static bool _playing = false;
|
||||
static Future<void>? _loadFuture;
|
||||
static Future<void>? _startFuture;
|
||||
static Timer? _stopTimer;
|
||||
|
||||
static String buildEventKey({
|
||||
String? giftId,
|
||||
@ -26,25 +32,75 @@ class SCLuckyGiftWinSoundPlayer {
|
||||
|
||||
static Future<void> play({String? eventKey}) async {
|
||||
if (DataPersistence.getPlayGiftMusic()) {
|
||||
unawaited(stop());
|
||||
return;
|
||||
}
|
||||
|
||||
final normalizedEventKey = (eventKey ?? '').trim();
|
||||
final now = DateTime.now();
|
||||
final lastPlayedAt = _lastPlayedAt;
|
||||
if (normalizedEventKey.isNotEmpty &&
|
||||
normalizedEventKey == _lastEventKey &&
|
||||
lastPlayedAt != null &&
|
||||
now.difference(lastPlayedAt) < _sameEventDedupeWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
_lastEventKey = normalizedEventKey.isEmpty ? null : normalizedEventKey;
|
||||
_lastPlayedAt = now;
|
||||
try {
|
||||
await _player.stop();
|
||||
await _player.play(AssetSource(assetPath));
|
||||
} catch (error) {}
|
||||
await _ensureLoaded();
|
||||
_extendStopWindow();
|
||||
|
||||
if (_playing || _startFuture != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
_startFuture = _startPlayback();
|
||||
await _startFuture;
|
||||
} catch (error) {
|
||||
_startFuture = null;
|
||||
}
|
||||
_startFuture = null;
|
||||
}
|
||||
|
||||
static Future<void> stop() async {
|
||||
_stopTimer?.cancel();
|
||||
_stopTimer = null;
|
||||
try {
|
||||
if (_player.state != PlayerState.stopped) {
|
||||
await _player.stop();
|
||||
}
|
||||
} catch (error) {
|
||||
_playing = false;
|
||||
return;
|
||||
}
|
||||
_playing = false;
|
||||
}
|
||||
|
||||
static Future<void> _ensureLoaded() {
|
||||
if (_loaded) {
|
||||
return Future<void>.value();
|
||||
}
|
||||
final loadFuture = _loadFuture;
|
||||
if (loadFuture != null) {
|
||||
return loadFuture;
|
||||
}
|
||||
_loadFuture = _load();
|
||||
return _loadFuture!;
|
||||
}
|
||||
|
||||
static Future<void> _load() async {
|
||||
try {
|
||||
await _player.setReleaseMode(ReleaseMode.loop);
|
||||
await _player.setVolume(_volume);
|
||||
await _player.setSource(AssetSource(assetPath));
|
||||
_loaded = true;
|
||||
} catch (error) {
|
||||
_loadFuture = null;
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _startPlayback() async {
|
||||
await _player.seek(Duration.zero);
|
||||
await _player.resume();
|
||||
_playing = true;
|
||||
}
|
||||
|
||||
static void _extendStopWindow() {
|
||||
_stopTimer?.cancel();
|
||||
_stopTimer = Timer(_stopDelay, () {
|
||||
unawaited(stop());
|
||||
});
|
||||
}
|
||||
|
||||
static String _formatNum(num? value) {
|
||||
|
||||
@ -288,7 +288,7 @@ class SCChatRoomHelper {
|
||||
}
|
||||
|
||||
static void roomSCGlobalConfig(String roomId) {
|
||||
final defaultEffectsEnabled = !SCGlobalConfig.isLowPerformanceDevice;
|
||||
const defaultEffectsEnabled = true;
|
||||
final account = AccountStorage().getCurrentUser()?.userProfile?.account;
|
||||
SCGlobalConfig
|
||||
.isGiftSpecialEffects = SCGlobalConfig.clampVisualEffectPreference(
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
import 'package:extended_image/extended_image.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_debouncer/flutter_debouncer.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
@ -70,7 +68,11 @@ class _PropsBagChatboxDetailDialogState
|
||||
],
|
||||
),
|
||||
SizedBox(height: 25.w),
|
||||
netImage(url: widget.res.propsResources?.cover??"",width: 80.w,height: 25.w),
|
||||
netImage(
|
||||
url: widget.res.propsResources?.cover ?? "",
|
||||
width: 80.w,
|
||||
height: 25.w,
|
||||
),
|
||||
SizedBox(height: 30.w),
|
||||
GestureDetector(
|
||||
child: Container(
|
||||
@ -136,6 +138,11 @@ class _PropsBagChatboxDetailDialogState
|
||||
}
|
||||
|
||||
void _use(BagsListRes res, bool unload) {
|
||||
final localizations = SCAppLocalizations.of(context)!;
|
||||
final profileManager = Provider.of<SocialChatUserProfileManager>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
SCLoadingManager.show();
|
||||
SCStoreRepositoryImp()
|
||||
.switchPropsUse(
|
||||
@ -144,18 +151,25 @@ class _PropsBagChatboxDetailDialogState
|
||||
unload,
|
||||
)
|
||||
.then((value) async {
|
||||
await Provider.of<SocialChatUserProfileManager>(
|
||||
context,
|
||||
listen: false,
|
||||
).fetchUserProfileData(loadGuardCount: false);
|
||||
try {
|
||||
await profileManager.fetchUserProfileData(loadGuardCount: false);
|
||||
} catch (_) {}
|
||||
if (!mounted) {
|
||||
SCLoadingManager.hide();
|
||||
return;
|
||||
}
|
||||
profileManager.syncCurrentUserChatBubble(
|
||||
unload ? null : res.propsResources,
|
||||
expireTime: res.expireTime?.toString(),
|
||||
);
|
||||
Future.delayed(Duration(milliseconds: 400), () {
|
||||
SCLoadingManager.hide();
|
||||
SmartDialog.dismiss(tag: "showPropsDetail");
|
||||
});
|
||||
if (!unload) {
|
||||
SCTts.show(SCAppLocalizations.of(context)!.successfulWear);
|
||||
SCTts.show(localizations.successfulWear);
|
||||
} else {
|
||||
SCTts.show(SCAppLocalizations.of(context)!.successfullyUnloaded);
|
||||
SCTts.show(localizations.successfullyUnloaded);
|
||||
}
|
||||
})
|
||||
.catchError((e) {
|
||||
|
||||
@ -65,10 +65,6 @@ class _RoomAnimationQueueScreenState extends State<RoomAnimationQueueScreen> {
|
||||
}
|
||||
|
||||
void _enqueueVipEntryFloatingMessage(Msg msg) {
|
||||
if (SCGlobalConfig.isLowPerformanceDevice &&
|
||||
((msg.user?.getMountains()?.sourceUrl ?? "").trim().isNotEmpty)) {
|
||||
return;
|
||||
}
|
||||
final floatPicture = msg.user?.getFloatPicture();
|
||||
final floatPictureUrl = _resolveVipFloatPictureUrl(floatPicture);
|
||||
final floatPictureFallbackUrl = _resolveVipFloatPictureFallbackUrl(
|
||||
@ -86,33 +82,16 @@ class _RoomAnimationQueueScreenState extends State<RoomAnimationQueueScreen> {
|
||||
if (roomId.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final isLowPerformance = SCGlobalConfig.isLowPerformanceDevice;
|
||||
final displayUrl =
|
||||
isLowPerformance && floatPictureFallbackUrl.isNotEmpty
|
||||
? floatPictureFallbackUrl
|
||||
: floatPictureUrl;
|
||||
final floatingMessage = SCFloatingMessage(
|
||||
type: 5,
|
||||
userId: msg.user?.id,
|
||||
roomId: roomId,
|
||||
userAvatarUrl: msg.user?.userAvatar,
|
||||
userName: msg.user?.userNickname,
|
||||
giftUrl: displayUrl,
|
||||
giftUrl: floatPictureUrl,
|
||||
fallbackUrl: floatPictureFallbackUrl,
|
||||
priority: 900,
|
||||
);
|
||||
if (isLowPerformance) {
|
||||
OverlayManager().addLowPerformanceCompensationMessage(
|
||||
floatingMessage,
|
||||
dedupKey: [
|
||||
"entry_float",
|
||||
roomId,
|
||||
msg.user?.id ?? "",
|
||||
floatPicture?.id ?? "",
|
||||
].join("|"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
OverlayManager().addMessage(floatingMessage);
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user