| | 1 | | using Cysharp.Threading.Tasks; |
| | 2 | | using DCL.Helpers; |
| | 3 | | using DCL.Interface; |
| | 4 | | using DCL.ProfanityFiltering; |
| | 5 | | using DCL.SettingsCommon; |
| | 6 | | using DCL.Social.Chat.Mentions; |
| | 7 | | using DCL.Social.Friends; |
| | 8 | | using System; |
| | 9 | | using System.Collections.Generic; |
| | 10 | | using System.Threading; |
| | 11 | | using UnityEngine; |
| | 12 | | using AudioSettings = DCL.SettingsCommon.AudioSettings; |
| | 13 | | using Channel = DCL.Chat.Channels.Channel; |
| | 14 | |
|
| | 15 | | namespace DCL.Chat.Notifications |
| | 16 | | { |
| | 17 | | public class ChatNotificationController : IHUD |
| | 18 | | { |
| | 19 | | private const int FADEOUT_DELAY = 8000; |
| | 20 | |
|
| | 21 | | private readonly DataStore dataStore; |
| | 22 | | private readonly IChatController chatController; |
| | 23 | | private readonly IFriendsController friendsController; |
| | 24 | | private readonly IMainChatNotificationsComponentView mainChatNotificationView; |
| | 25 | | private readonly ITopNotificationsComponentView topNotificationView; |
| | 26 | | private readonly IUserProfileBridge userProfileBridge; |
| | 27 | | private readonly IProfanityFilter profanityFilter; |
| | 28 | | private readonly ISettingsRepository<AudioSettings> audioSettings; |
| 22 | 29 | | private readonly TimeSpan maxNotificationInterval = new (0, 1, 0); |
| 22 | 30 | | private readonly HashSet<string> notificationEntries = new (); |
| 22 | 31 | | private readonly CancellationTokenSource addMessagesCancellationToken = new (); |
| | 32 | |
|
| 66 | 33 | | private BaseVariable<bool> shouldShowNotificationPanel => dataStore.HUDs.shouldShowNotificationPanel; |
| 22 | 34 | | private BaseVariable<Transform> notificationPanelTransform => dataStore.HUDs.notificationPanelTransform; |
| 82 | 35 | | private BaseVariable<Transform> topNotificationPanelTransform => dataStore.HUDs.topNotificationPanelTransform; |
| 44 | 36 | | private BaseVariable<HashSet<string>> visibleTaskbarPanels => dataStore.HUDs.visibleTaskbarPanels; |
| 14 | 37 | | private BaseVariable<string> openedChat => dataStore.HUDs.openChat; |
| 22 | 38 | | private CancellationTokenSource fadeOutCancellationToken = new (); |
| | 39 | | private UserProfile internalOwnUserProfile; |
| | 40 | |
|
| | 41 | | private UserProfile ownUserProfile |
| | 42 | | { |
| | 43 | | get |
| | 44 | | { |
| 38 | 45 | | internalOwnUserProfile ??= userProfileBridge.GetOwn(); |
| 38 | 46 | | return internalOwnUserProfile; |
| | 47 | | } |
| | 48 | | } |
| | 49 | |
|
| 22 | 50 | | public ChatNotificationController(DataStore dataStore, |
| | 51 | | IMainChatNotificationsComponentView mainChatNotificationView, |
| | 52 | | ITopNotificationsComponentView topNotificationView, |
| | 53 | | IChatController chatController, |
| | 54 | | IFriendsController friendsController, |
| | 55 | | IUserProfileBridge userProfileBridge, |
| | 56 | | IProfanityFilter profanityFilter, |
| | 57 | | ISettingsRepository<AudioSettings> audioSettings) |
| | 58 | | { |
| 22 | 59 | | this.dataStore = dataStore; |
| 22 | 60 | | this.chatController = chatController; |
| 22 | 61 | | this.friendsController = friendsController; |
| 22 | 62 | | this.userProfileBridge = userProfileBridge; |
| 22 | 63 | | this.profanityFilter = profanityFilter; |
| 22 | 64 | | this.audioSettings = audioSettings; |
| 22 | 65 | | this.mainChatNotificationView = mainChatNotificationView; |
| 22 | 66 | | this.topNotificationView = topNotificationView; |
| 22 | 67 | | mainChatNotificationView.OnResetFade += ResetFadeOut; |
| 22 | 68 | | topNotificationView.OnResetFade += ResetFadeOut; |
| 22 | 69 | | mainChatNotificationView.OnPanelFocus += TogglePanelBackground; |
| 22 | 70 | | mainChatNotificationView.OnClickedFriendRequest += HandleClickedFriendRequest; |
| 22 | 71 | | topNotificationView.OnClickedFriendRequest += HandleClickedFriendRequest; |
| 22 | 72 | | mainChatNotificationView.OnClickedChatMessage += OpenChat; |
| 22 | 73 | | topNotificationView.OnClickedChatMessage += OpenChat; |
| 22 | 74 | | chatController.OnAddMessage += HandleMessageAdded; |
| 22 | 75 | | friendsController.OnFriendRequestReceived += HandleFriendRequestReceived; |
| 22 | 76 | | friendsController.OnSentFriendRequestApproved += HandleSentFriendRequestApproved; |
| 22 | 77 | | notificationPanelTransform.Set(mainChatNotificationView.GetPanelTransform()); |
| 22 | 78 | | topNotificationPanelTransform.Set(topNotificationView.GetPanelTransform()); |
| 22 | 79 | | visibleTaskbarPanels.OnChange += VisiblePanelsChanged; |
| 22 | 80 | | shouldShowNotificationPanel.OnChange += ResetVisibility; |
| 22 | 81 | | ResetVisibility(shouldShowNotificationPanel.Get(), false); |
| 22 | 82 | | } |
| | 83 | |
|
| | 84 | | public void SetVisibility(bool visible) |
| | 85 | | { |
| 22 | 86 | | ResetFadeOut(visible); |
| | 87 | |
|
| 22 | 88 | | if (visible) |
| | 89 | | { |
| 22 | 90 | | if (shouldShowNotificationPanel.Get()) |
| 22 | 91 | | mainChatNotificationView.Show(); |
| | 92 | |
|
| 22 | 93 | | topNotificationView.Hide(); |
| 22 | 94 | | mainChatNotificationView.ShowNotifications(); |
| | 95 | | } |
| | 96 | | else |
| | 97 | | { |
| 0 | 98 | | mainChatNotificationView.Hide(); |
| | 99 | |
|
| 0 | 100 | | if (!visibleTaskbarPanels.Get().Contains("WorldChatPanel")) |
| 0 | 101 | | topNotificationView.Show(); |
| | 102 | | } |
| 0 | 103 | | } |
| | 104 | |
|
| | 105 | | public void Dispose() |
| | 106 | | { |
| 22 | 107 | | chatController.OnAddMessage -= HandleMessageAdded; |
| 22 | 108 | | friendsController.OnFriendRequestReceived -= HandleFriendRequestReceived; |
| 22 | 109 | | friendsController.OnSentFriendRequestApproved -= HandleSentFriendRequestApproved; |
| 22 | 110 | | visibleTaskbarPanels.OnChange -= VisiblePanelsChanged; |
| 22 | 111 | | mainChatNotificationView.OnResetFade -= ResetFadeOut; |
| 22 | 112 | | topNotificationView.OnResetFade -= ResetFadeOut; |
| 22 | 113 | | mainChatNotificationView.OnClickedChatMessage -= OpenChat; |
| 22 | 114 | | topNotificationView.OnClickedChatMessage -= OpenChat; |
| 22 | 115 | | addMessagesCancellationToken.Cancel(); |
| 22 | 116 | | addMessagesCancellationToken.Dispose(); |
| 22 | 117 | | } |
| | 118 | |
|
| | 119 | | private void VisiblePanelsChanged(HashSet<string> newList, HashSet<string> oldList) |
| | 120 | | { |
| 0 | 121 | | SetVisibility(newList.Count == 0); |
| 0 | 122 | | } |
| | 123 | |
|
| | 124 | | private void HandleMessageAdded(ChatMessage[] messages) |
| | 125 | | { |
| 59 | 126 | | foreach (var message in messages) |
| | 127 | | { |
| 15 | 128 | | if (message.messageType != ChatMessage.Type.PRIVATE && |
| 0 | 129 | | message.messageType != ChatMessage.Type.PUBLIC) return; |
| | 130 | |
|
| 15 | 131 | | var span = Utils.UnixToDateTimeWithTime((ulong)DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()) - |
| | 132 | | Utils.UnixToDateTimeWithTime(message.timestamp); |
| | 133 | |
|
| 16 | 134 | | if (span >= maxNotificationInterval) return; |
| | 135 | |
|
| 14 | 136 | | var channel = chatController.GetAllocatedChannel( |
| | 137 | | string.IsNullOrEmpty(message.recipient) && message.messageType == ChatMessage.Type.PUBLIC |
| | 138 | | ? "nearby" |
| | 139 | | : message.recipient); |
| | 140 | |
|
| 14 | 141 | | if (channel?.Muted ?? false) return; |
| | 142 | |
|
| | 143 | | // TODO: entries may have an inconsistent state. We should update the entry with new data |
| 14 | 144 | | if (notificationEntries.Contains(message.messageId)) return; |
| 14 | 145 | | notificationEntries.Add(message.messageId); |
| | 146 | |
|
| 14 | 147 | | AddNotificationAsync(message, channel, addMessagesCancellationToken.Token).Forget(); |
| | 148 | | } |
| 14 | 149 | | } |
| | 150 | |
|
| | 151 | | private async UniTaskVoid AddNotificationAsync(ChatMessage message, Channel channel = null, CancellationToken ca |
| | 152 | | { |
| 14 | 153 | | string body = message.body; |
| 14 | 154 | | string openedChatId = openedChat.Get(); |
| 14 | 155 | | bool isOwnPlayerMentioned = MentionsUtils.IsUserMentionedInText(ownUserProfile.userName, body); |
| | 156 | |
|
| 14 | 157 | | if (message.messageType == ChatMessage.Type.PRIVATE) |
| | 158 | | { |
| 4 | 159 | | string peerId = ExtractPeerId(message); |
| | 160 | |
|
| | 161 | | try |
| | 162 | | { |
| | 163 | | // incoming friend request's message is added as a DM. This check filters it |
| 5 | 164 | | if (await friendsController.GetFriendshipStatus(peerId, cancellationToken) != FriendshipStatus.FRIEN |
| 3 | 165 | | } |
| 0 | 166 | | catch (Exception e) when (e is not OperationCanceledException) |
| | 167 | | { |
| 0 | 168 | | Debug.LogException(e); |
| 0 | 169 | | } |
| | 170 | |
|
| 3 | 171 | | UserProfile peerProfile = userProfileBridge.Get(peerId); |
| 3 | 172 | | bool isMyMessage = message.sender == ownUserProfile.userId; |
| 3 | 173 | | UserProfile senderProfile = isMyMessage ? ownUserProfile : userProfileBridge.Get(message.sender); |
| 3 | 174 | | string peerName = peerProfile?.userName ?? peerId; |
| 3 | 175 | | string peerProfilePicture = peerProfile?.face256SnapshotURL; |
| 3 | 176 | | string senderName = senderProfile?.userName ?? message.sender; |
| | 177 | |
|
| 3 | 178 | | var privateModel = new PrivateChatMessageNotificationModel( |
| | 179 | | message.messageId, |
| | 180 | | isMyMessage ? peerId : message.sender, |
| | 181 | | body, |
| | 182 | | message.timestamp, |
| | 183 | | senderName, |
| | 184 | | peerName, |
| | 185 | | isMyMessage, |
| | 186 | | isOwnPlayerMentioned, |
| | 187 | | peerProfilePicture); |
| | 188 | |
|
| 3 | 189 | | mainChatNotificationView.AddNewChatNotification(privateModel); |
| | 190 | |
|
| 3 | 191 | | if (message.sender != openedChatId && message.recipient != openedChatId) |
| 3 | 192 | | if (topNotificationPanelTransform.Get().gameObject.activeInHierarchy) |
| 3 | 193 | | topNotificationView.AddNewChatNotification(privateModel); |
| 3 | 194 | | } |
| 10 | 195 | | else if (message.messageType == ChatMessage.Type.PUBLIC) |
| | 196 | | { |
| 10 | 197 | | bool isMyMessage = message.sender == ownUserProfile.userId; |
| 10 | 198 | | UserProfile senderProfile = isMyMessage ? ownUserProfile : userProfileBridge.Get(message.sender); |
| 10 | 199 | | string senderName = senderProfile?.userName ?? message.sender; |
| 10 | 200 | | bool shouldPlayMentionSfx = isOwnPlayerMentioned; |
| | 201 | |
|
| 10 | 202 | | if (isOwnPlayerMentioned) |
| | 203 | | { |
| 3 | 204 | | AudioSettings.ChatNotificationType chatNotificationSfxType = audioSettings.Data.chatNotificationType |
| | 205 | |
|
| 3 | 206 | | shouldPlayMentionSfx = chatNotificationSfxType is AudioSettings.ChatNotificationType.All |
| | 207 | | or AudioSettings.ChatNotificationType.MentionsOnly; |
| | 208 | | } |
| | 209 | |
|
| 10 | 210 | | if (IsProfanityFilteringEnabled()) |
| | 211 | | { |
| 2 | 212 | | senderName = await profanityFilter.Filter(senderName, cancellationToken); |
| 2 | 213 | | body = await profanityFilter.Filter(message.body, cancellationToken); |
| | 214 | | } |
| | 215 | |
|
| 10 | 216 | | var publicModel = new PublicChannelMessageNotificationModel( |
| | 217 | | message.messageId, |
| | 218 | | body, |
| | 219 | | channel?.Name ?? message.recipient, |
| | 220 | | channel?.ChannelId, |
| | 221 | | message.timestamp, |
| | 222 | | isMyMessage, |
| | 223 | | senderName, |
| | 224 | | isOwnPlayerMentioned, |
| | 225 | | shouldPlayMentionSfx); |
| | 226 | |
|
| 10 | 227 | | mainChatNotificationView.AddNewChatNotification(publicModel); |
| | 228 | |
|
| 10 | 229 | | if ((string.IsNullOrEmpty(message.recipient) && openedChatId != ChatUtils.NEARBY_CHANNEL_ID) |
| | 230 | | || (!string.IsNullOrEmpty(message.recipient) && openedChatId != message.recipient)) |
| 10 | 231 | | if (topNotificationPanelTransform.Get().gameObject.activeInHierarchy) |
| 10 | 232 | | topNotificationView.AddNewChatNotification(publicModel); |
| 10 | 233 | | } |
| 14 | 234 | | } |
| | 235 | |
|
| | 236 | | private void HandleFriendRequestReceived(FriendRequest friendRequest) |
| | 237 | | { |
| 2 | 238 | | if (friendRequest.From == ownUserProfile.userId || |
| | 239 | | friendRequest.To != ownUserProfile.userId) |
| 0 | 240 | | return; |
| | 241 | |
|
| 2 | 242 | | var friendRequestProfile = userProfileBridge.Get(friendRequest.From); |
| 2 | 243 | | var friendRequestName = friendRequestProfile?.userName ?? friendRequest.From; |
| | 244 | |
|
| 2 | 245 | | FriendRequestNotificationModel friendRequestNotificationModel = new FriendRequestNotificationModel( |
| | 246 | | friendRequest.FriendRequestId, |
| | 247 | | friendRequest.From, |
| | 248 | | friendRequestName, |
| | 249 | | "Friend Request received", |
| | 250 | | "wants to be your friend.", |
| | 251 | | friendRequest.Timestamp, |
| | 252 | | false); |
| | 253 | |
|
| 2 | 254 | | mainChatNotificationView.AddNewFriendRequestNotification(friendRequestNotificationModel); |
| | 255 | |
|
| 2 | 256 | | if (topNotificationPanelTransform.Get().gameObject.activeInHierarchy) |
| 2 | 257 | | topNotificationView.AddNewFriendRequestNotification(friendRequestNotificationModel); |
| 2 | 258 | | } |
| | 259 | |
|
| | 260 | | private void HandleSentFriendRequestApproved(FriendRequest friendRequest) |
| | 261 | | { |
| 1 | 262 | | string recipientUserId = friendRequest.To; |
| 1 | 263 | | var friendRequestProfile = userProfileBridge.Get(recipientUserId); |
| | 264 | |
|
| 1 | 265 | | FriendRequestNotificationModel friendRequestNotificationModel = new FriendRequestNotificationModel( |
| | 266 | | friendRequest.FriendRequestId, |
| | 267 | | recipientUserId, |
| | 268 | | friendRequestProfile.userName, |
| | 269 | | "Friend Request accepted", |
| | 270 | | "and you are friends now!", |
| | 271 | | DateTime.UtcNow, |
| | 272 | | true); |
| | 273 | |
|
| 1 | 274 | | mainChatNotificationView.AddNewFriendRequestNotification(friendRequestNotificationModel); |
| | 275 | |
|
| 1 | 276 | | if (topNotificationPanelTransform.Get().gameObject.activeInHierarchy) |
| 1 | 277 | | topNotificationView.AddNewFriendRequestNotification(friendRequestNotificationModel); |
| 1 | 278 | | } |
| | 279 | |
|
| | 280 | | private void ResetFadeOut(bool fadeOutAfterDelay = false) |
| | 281 | | { |
| 22 | 282 | | mainChatNotificationView.ShowNotifications(); |
| | 283 | |
|
| 22 | 284 | | if (topNotificationPanelTransform.Get().gameObject.activeInHierarchy) |
| 22 | 285 | | topNotificationView.ShowNotification(); |
| | 286 | |
|
| 22 | 287 | | fadeOutCancellationToken.Cancel(); |
| 22 | 288 | | fadeOutCancellationToken = new CancellationTokenSource(); |
| | 289 | |
|
| 22 | 290 | | if (fadeOutAfterDelay) |
| 22 | 291 | | WaitThenFadeOutNotifications(fadeOutCancellationToken.Token).Forget(); |
| 22 | 292 | | } |
| | 293 | |
|
| | 294 | | private void TogglePanelBackground(bool isInFocus) |
| | 295 | | { |
| 2 | 296 | | if (mainChatNotificationView.GetNotificationsCount() == 0) |
| 1 | 297 | | return; |
| | 298 | |
|
| 1 | 299 | | if (isInFocus) |
| 1 | 300 | | mainChatNotificationView.ShowPanel(); |
| | 301 | | else |
| 0 | 302 | | mainChatNotificationView.HidePanel(); |
| 0 | 303 | | } |
| | 304 | |
|
| | 305 | | private async UniTaskVoid WaitThenFadeOutNotifications(CancellationToken cancellationToken) |
| | 306 | | { |
| 66 | 307 | | await UniTask.Delay(FADEOUT_DELAY, cancellationToken: cancellationToken); |
| 22 | 308 | | await UniTask.SwitchToMainThread(cancellationToken); |
| | 309 | |
|
| 22 | 310 | | if (cancellationToken.IsCancellationRequested) |
| 0 | 311 | | return; |
| | 312 | |
|
| 22 | 313 | | mainChatNotificationView.HideNotifications(); |
| | 314 | |
|
| 22 | 315 | | if (topNotificationPanelTransform.Get() != null && |
| | 316 | | topNotificationPanelTransform.Get().gameObject.activeInHierarchy) |
| 0 | 317 | | topNotificationView.HideNotification(); |
| 22 | 318 | | } |
| | 319 | |
|
| | 320 | | private string ExtractPeerId(ChatMessage message) => |
| 4 | 321 | | message.sender != ownUserProfile.userId ? message.sender : message.recipient; |
| | 322 | |
|
| | 323 | | private void ResetVisibility(bool current, bool previous) => |
| 22 | 324 | | SetVisibility(current); |
| | 325 | |
|
| | 326 | | private bool IsProfanityFilteringEnabled() => |
| 10 | 327 | | dataStore.settings.profanityChatFilteringEnabled.Get(); |
| | 328 | |
|
| | 329 | | private void HandleClickedFriendRequest(string friendRequestId, string userId, bool isAcceptedFromPeer) |
| | 330 | | { |
| 2 | 331 | | if (string.IsNullOrEmpty(friendRequestId)) return; |
| | 332 | |
|
| 2 | 333 | | bool wasFound = friendsController.TryGetAllocatedFriendRequest(friendRequestId, out FriendRequest _); |
| | 334 | |
|
| 2 | 335 | | bool isFriend = friendsController.IsFriend(userId); |
| | 336 | |
|
| 2 | 337 | | if (wasFound && !isFriend && !isAcceptedFromPeer) |
| 1 | 338 | | dataStore.HUDs.openReceivedFriendRequestDetail.Set(friendRequestId, true); |
| 1 | 339 | | else if (isFriend) |
| 1 | 340 | | OpenChat(userId); |
| 1 | 341 | | } |
| | 342 | |
|
| | 343 | | private void OpenChat(string chatId) => |
| 1 | 344 | | dataStore.HUDs.openChat.Set(chatId, true); |
| | 345 | | } |
| | 346 | | } |