< Summary

Class:DCL.Social.Chat.ChatHUDController
Assembly:ChatHUD
File(s):/tmp/workspace/unity-renderer/unity-renderer/Assets/Scripts/MainScripts/DCL/Controllers/HUD/ChatWidgetHUD/ChatHUDController.cs
Covered lines:214
Uncovered lines:29
Coverable lines:243
Total lines:533
Line coverage:88% (214 of 243)
Covered branches:0
Total branches:0
Covered methods:29
Total methods:34
Method coverage:85.2% (29 of 34)

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity NPath complexity Sequence coverage
ChatHUDController(...)0%110100%
Initialize(...)0%110100%
Dispose()0%110100%
OpenedContextMenu(...)0%2100%
SetVisibility(...)0%220100%
<SetChatMessage()0%17.239053.33%
SetChatMessage(...)0%14140100%
SetChatMessage()0%31.4418065.38%
ClearAllEntries()0%110100%
ResetInputField(...)0%110100%
FocusInputField()0%110100%
SetInputFieldText(...)0%110100%
UnfocusInputField()0%110100%
ContextMenu_OnShowMenu()0%2100%
IsProfanityFilteringEnabled()0%220100%
HandleMessageUpdated(...)0%110100%
UpdateMentions(...)0%6.146084.21%
<UpdateMentions()0%9.249085.71%
HandleSendMessage(...)0%12.3310071.43%
RegisterMessageHistory(...)0%3.213071.43%
ApplyWhisperAttributes(...)0%4.014091.67%
HandleInputFieldSelected()0%6200%
HandleInputFieldDeselected()0%2100%
IsSpamming(...)0%64050%
UpdateSpam(...)0%5.515072.73%
MessagesSentTooFast(...)0%110100%
FillInputWithNextMessage()0%2.022083.33%
FillInputWithPreviousMessage()0%3.13077.78%
HandleMentionSuggestionSelected(...)0%2100%
HideMentionSuggestions()0%110100%
HandleCopyMessageToClipboard(...)0%110100%

File(s)

/tmp/workspace/unity-renderer/unity-renderer/Assets/Scripts/MainScripts/DCL/Controllers/HUD/ChatWidgetHUD/ChatHUDController.cs

#LineLine coverage
 1using Cysharp.Threading.Tasks;
 2using DCL.Chat;
 3using DCL.Interface;
 4using DCL.ProfanityFiltering;
 5using DCL.Social.Chat.Mentions;
 6using DCL.Tasks;
 7using DCLServices.CopyPaste.Analytics;
 8using SocialFeaturesAnalytics;
 9using System;
 10using System.Collections.Generic;
 11using System.Linq;
 12using System.Text.RegularExpressions;
 13using System.Threading;
 14using UnityEngine;
 15
 16namespace DCL.Social.Chat
 17{
 18    public class ChatHUDController : IHUD
 19    {
 20        public const int MAX_CHAT_ENTRIES = 30;
 21        private const int TEMPORARILY_MUTE_MINUTES = 3;
 22        private const int MAX_CONTINUOUS_MESSAGES = 10;
 23        private const int MIN_MILLISECONDS_BETWEEN_MESSAGES = 1500;
 24        private const int MAX_HISTORY_ITERATION = 10;
 25        private const int MAX_MENTION_SUGGESTIONS = 5;
 26
 27        public delegate UniTask<List<UserProfile>> GetSuggestedUserProfiles(string name, int maxCount, CancellationToken
 28
 29        private readonly DataStore dataStore;
 30        private readonly IUserProfileBridge userProfileBridge;
 31        private readonly bool detectWhisper;
 32        private readonly GetSuggestedUserProfiles getSuggestedUserProfiles;
 33        private readonly InputAction_Trigger closeMentionSuggestionsTrigger;
 34        private readonly IProfanityFilter profanityFilter;
 35        private readonly ISocialAnalytics socialAnalytics;
 36        private readonly IChatController chatController;
 37        private readonly IClipboard clipboard;
 38        private readonly ICopyPasteAnalyticsService copyPasteAnalyticsService;
 9539        private readonly Regex mentionRegex = new (@"(\B@\w+)|(\B@+)");
 9540        private readonly Regex whisperRegex = new (@"(?i)^\/(whisper|w) (\S+)( *)(.*)");
 9541        private readonly Dictionary<string, ulong> temporarilyMutedSenders = new ();
 9542        private readonly List<ChatEntryModel> spamMessages = new ();
 9543        private readonly List<string> lastMessagesSent = new ();
 9544        private readonly CancellationTokenSource profileFetchingCancellationToken = new ();
 9545        private readonly CancellationTokenSource addMessagesCancellationToken = new ();
 46
 47        private int currentHistoryIteration;
 48        private IChatHUDComponentView view;
 9549        private CancellationTokenSource mentionSuggestionCancellationToken = new ();
 50        private int mentionLength;
 51        private int mentionFromIndex;
 52        private Dictionary<string, UserProfile> mentionSuggestedProfiles;
 53
 9554        private bool useLegacySorting => dataStore.featureFlags.flags.Get().IsFeatureEnabled("legacy_chat_sorting_enable
 1755        private bool isMentionsEnabled => dataStore.featureFlags.flags.Get().IsFeatureEnabled("chat_mentions_enabled");
 56
 57        public IComparer<ChatEntryModel> SortingStrategy
 58        {
 59            set
 60            {
 3761                if (view != null)
 3762                    view.SortingStrategy = value;
 3763            }
 64        }
 65
 66        public event Action OnInputFieldSelected;
 67        public event Action<ChatMessage> OnSendMessage;
 68        public event Action<ChatMessage> OnMessageSentBlockedBySpam;
 69
 9570        public ChatHUDController(DataStore dataStore,
 71            IUserProfileBridge userProfileBridge,
 72            bool detectWhisper,
 73            GetSuggestedUserProfiles getSuggestedUserProfiles,
 74            ISocialAnalytics socialAnalytics,
 75            IChatController chatController,
 76            IClipboard clipboard,
 77            ICopyPasteAnalyticsService copyPasteAnalyticsService,
 78            IProfanityFilter profanityFilter = null)
 79        {
 9580            this.dataStore = dataStore;
 9581            this.userProfileBridge = userProfileBridge;
 9582            this.detectWhisper = detectWhisper;
 9583            this.getSuggestedUserProfiles = getSuggestedUserProfiles;
 9584            this.socialAnalytics = socialAnalytics;
 9585            this.chatController = chatController;
 9586            this.clipboard = clipboard;
 9587            this.copyPasteAnalyticsService = copyPasteAnalyticsService;
 9588            this.profanityFilter = profanityFilter;
 9589        }
 90
 91        public void Initialize(IChatHUDComponentView view)
 92        {
 9593            this.view = view;
 9594            this.view.OnPreviousChatInHistory -= FillInputWithPreviousMessage;
 9595            this.view.OnPreviousChatInHistory += FillInputWithPreviousMessage;
 9596            this.view.OnNextChatInHistory -= FillInputWithNextMessage;
 9597            this.view.OnNextChatInHistory += FillInputWithNextMessage;
 9598            this.view.OnShowMenu -= ContextMenu_OnShowMenu;
 9599            this.view.OnShowMenu += ContextMenu_OnShowMenu;
 95100            this.view.OnInputFieldSelected -= HandleInputFieldSelected;
 95101            this.view.OnInputFieldSelected += HandleInputFieldSelected;
 95102            this.view.OnInputFieldDeselected -= HandleInputFieldDeselected;
 95103            this.view.OnInputFieldDeselected += HandleInputFieldDeselected;
 95104            this.view.OnSendMessage -= HandleSendMessage;
 95105            this.view.OnSendMessage += HandleSendMessage;
 95106            this.view.OnMessageUpdated -= HandleMessageUpdated;
 95107            this.view.OnMessageUpdated += HandleMessageUpdated;
 95108            this.view.OnMentionSuggestionSelected -= HandleMentionSuggestionSelected;
 95109            this.view.OnMentionSuggestionSelected += HandleMentionSuggestionSelected;
 95110            this.view.OnOpenedContextMenu -= OpenedContextMenu;
 95111            this.view.OnOpenedContextMenu += OpenedContextMenu;
 95112            this.view.OnCopyMessageRequested += HandleCopyMessageToClipboard;
 95113            this.view.UseLegacySorting = useLegacySorting;
 95114        }
 115
 116        public void Dispose()
 117        {
 94118            view.OnShowMenu -= ContextMenu_OnShowMenu;
 94119            view.OnMessageUpdated -= HandleMessageUpdated;
 94120            view.OnSendMessage -= HandleSendMessage;
 94121            view.OnInputFieldSelected -= HandleInputFieldSelected;
 94122            view.OnInputFieldDeselected -= HandleInputFieldDeselected;
 94123            view.OnPreviousChatInHistory -= FillInputWithPreviousMessage;
 94124            view.OnNextChatInHistory -= FillInputWithNextMessage;
 94125            view.OnMentionSuggestionSelected -= HandleMentionSuggestionSelected;
 94126            view.OnCopyMessageRequested -= HandleCopyMessageToClipboard;
 94127            OnSendMessage = null;
 94128            OnInputFieldSelected = null;
 94129            view.Dispose();
 94130            mentionSuggestionCancellationToken.SafeCancelAndDispose();
 94131            profileFetchingCancellationToken.SafeCancelAndDispose();
 94132            addMessagesCancellationToken.SafeCancelAndDispose();
 94133        }
 134
 135        private void OpenedContextMenu(string userId) =>
 0136            socialAnalytics.SendClickedMention(userId);
 137
 138        public void SetVisibility(bool visible)
 139        {
 32140            if (!visible)
 10141                HideMentionSuggestions();
 32142        }
 143
 144        public void SetChatMessage(ChatMessage message, bool setScrollPositionToBottom = false,
 145            bool spamFiltering = true, bool limitMaxEntries = true)
 146        {
 147            async UniTaskVoid EnsureProfileThenUpdateMessage(string profileId, ChatEntryModel model,
 148                Func<ChatEntryModel, UserProfile, ChatEntryModel> modificationCallback,
 149                bool setScrollPositionToBottom, bool spamFiltering, bool limitMaxEntries,
 150                CancellationToken cancellationToken)
 151            {
 152                try
 153                {
 4154                    UserProfile requestedProfile = await userProfileBridge.RequestFullUserProfileAsync(profileId, cancel
 155
 4156                    model = modificationCallback.Invoke(model, requestedProfile);
 157
 158                    // avoid any possible race condition with the current AddChatMessage operation
 12159                    await UniTask.NextFrame(cancellationToken: cancellationToken);
 160
 4161                    await SetChatMessage(model, setScrollPositionToBottom, spamFiltering, limitMaxEntries, cancellationT
 4162                }
 0163                catch (Exception e) when (e is not OperationCanceledException) { Debug.LogException(e); }
 4164            }
 165
 166            string GetEllipsisFormat(string address) =>
 4167                address.Length <= 8 ? address : $"{address[..4]}...{address[^4..]}";
 168
 18169            var model = new ChatEntryModel();
 18170            var ownProfile = userProfileBridge.GetOwn();
 171
 18172            model.messageId = message.messageId;
 18173            model.messageType = message.messageType;
 18174            model.bodyText = message.body;
 18175            model.timestamp = message.timestamp;
 176
 18177            if (!string.IsNullOrEmpty(message.recipient))
 178            {
 6179                model.isChannelMessage = chatController.GetAllocatedChannel(message.recipient) != null;
 180
 6181                if (!model.isChannelMessage)
 182                {
 4183                    UserProfile recipientProfile = userProfileBridge.Get(message.recipient);
 184
 4185                    if (recipientProfile != null)
 2186                        model.recipientName = recipientProfile.userName;
 187                    else
 188                    {
 2189                        model.recipientName = GetEllipsisFormat(message.recipient);
 2190                        model.isLoadingNames = true;
 191
 192                        // sometimes there is no cached profile, so we request it
 193                        // dont block the operation of showing the message immediately
 194                        // just update the message information after we get the profile
 2195                        EnsureProfileThenUpdateMessage(message.recipient, model,
 196                                (m, p) =>
 197                                {
 2198                                    m.recipientName = p.userName;
 2199                                    return m;
 200                                },
 201                                setScrollPositionToBottom, spamFiltering,
 202                                limitMaxEntries,
 203                                profileFetchingCancellationToken.Token)
 204                           .Forget();
 205                    }
 206                }
 207            }
 208
 18209            if (!string.IsNullOrEmpty(message.sender))
 210            {
 18211                model.senderId = message.sender;
 18212                UserProfile senderProfile = userProfileBridge.Get(message.sender);
 213
 18214                if (senderProfile != null)
 16215                    model.senderName = senderProfile.userName;
 216                else
 217                {
 2218                    model.senderName = GetEllipsisFormat(message.sender);
 2219                    model.isLoadingNames = true;
 220
 221                    // sometimes there is no cached profile, so we request it
 222                    // dont block the operation of showing the message immediately
 223                    // just update the message information after we get the profile
 2224                    EnsureProfileThenUpdateMessage(message.sender, model,
 225                            (m, p) =>
 226                            {
 2227                                m.senderName = p.userName;
 2228                                return m;
 229                            },
 230                            setScrollPositionToBottom, spamFiltering,
 231                            limitMaxEntries,
 232                            profileFetchingCancellationToken.Token)
 233                       .Forget();
 234                }
 235            }
 236
 18237            if (message.messageType == ChatMessage.Type.PRIVATE)
 238            {
 9239                model.subType = message.sender == ownProfile.userId
 240                    ? ChatEntryModel.SubType.SENT
 241                    : ChatEntryModel.SubType.RECEIVED;
 242            }
 9243            else if (message.messageType == ChatMessage.Type.PUBLIC)
 244            {
 9245                model.subType = message.sender == ownProfile.userId
 246                    ? ChatEntryModel.SubType.SENT
 247                    : ChatEntryModel.SubType.RECEIVED;
 248            }
 249
 18250            SetChatMessage(model, setScrollPositionToBottom, spamFiltering, limitMaxEntries,
 251                    addMessagesCancellationToken.Token)
 252               .Forget();
 18253        }
 254
 255        public async UniTask SetChatMessage(ChatEntryModel chatEntryModel, bool setScrollPositionToBottom = false, bool 
 256            CancellationToken cancellationToken = default)
 257        {
 34258            if (IsSpamming(chatEntryModel.senderName) && spamFiltering) return;
 259
 34260            chatEntryModel.bodyText = ChatUtils.AddNoParse(chatEntryModel.bodyText);
 261
 34262            if (IsProfanityFilteringEnabled() && chatEntryModel.messageType != ChatMessage.Type.PRIVATE)
 263            {
 12264                chatEntryModel.bodyText = await profanityFilter.Filter(chatEntryModel.bodyText, cancellationToken);
 265
 12266                if (!string.IsNullOrEmpty(chatEntryModel.senderName))
 12267                    chatEntryModel.senderName = await profanityFilter.Filter(chatEntryModel.senderName, cancellationToke
 268
 12269                if (!string.IsNullOrEmpty(chatEntryModel.recipientName))
 2270                    chatEntryModel.recipientName = await profanityFilter.Filter(chatEntryModel.recipientName, cancellati
 271            }
 272
 34273            await UniTask.SwitchToMainThread(cancellationToken: cancellationToken);
 274
 34275            view.SetEntry(chatEntryModel, setScrollPositionToBottom);
 276
 34277            if (limitMaxEntries && view.EntryCount > MAX_CHAT_ENTRIES)
 1278                view.RemoveOldestEntry();
 279
 48280            if (string.IsNullOrEmpty(chatEntryModel.senderId)) return;
 281
 20282            if (spamFiltering)
 20283                UpdateSpam(chatEntryModel);
 34284        }
 285
 286        public void ClearAllEntries() =>
 50287            view.ClearAllEntries();
 288
 289        public void ResetInputField(bool loseFocus = false) =>
 6290            view.ResetInputField(loseFocus);
 291
 292        public void FocusInputField() =>
 20293            view.FocusInputField();
 294
 295        public void SetInputFieldText(string setInputText) =>
 3296            view.SetInputFieldText(setInputText);
 297
 298        public void UnfocusInputField() =>
 27299            view.UnfocusInputField();
 300
 301        private void ContextMenu_OnShowMenu() =>
 0302            view.OnMessageCancelHover();
 303
 304        private bool IsProfanityFilteringEnabled() =>
 34305            dataStore.settings.profanityChatFilteringEnabled.Get()
 306            && profanityFilter != null;
 307
 308        private void HandleMessageUpdated(string message, int cursorPosition) =>
 17309            UpdateMentions(message, cursorPosition);
 310
 311        private void UpdateMentions(string message, int cursorPosition)
 312        {
 17313            if (!isMentionsEnabled) return;
 314
 17315            if (string.IsNullOrEmpty(message))
 316            {
 0317                HideMentionSuggestions();
 0318                return;
 319            }
 320
 321            async UniTaskVoid ShowMentionSuggestionsAsync(string name, CancellationToken cancellationToken)
 322            {
 323                try
 324                {
 13325                    List<UserProfile> suggestions = await getSuggestedUserProfiles.Invoke(name, MAX_MENTION_SUGGESTIONS,
 326
 24327                    mentionSuggestedProfiles = suggestions.ToDictionary(profile => profile.userId, profile => profile);
 328
 12329                    if (suggestions.Count == 0)
 9330                        HideMentionSuggestions();
 331                    else
 332                    {
 3333                        view.ShowMentionSuggestions();
 3334                        dataStore.mentions.isMentionSuggestionVisible.Set(true);
 335
 9336                        view.SetMentionSuggestions(suggestions.Select(profile => new ChatMentionSuggestionModel
 337                                                               {
 338                                                                   userId = profile.userId,
 339                                                                   userName = profile.userName,
 340                                                                   imageUrl = profile.face256SnapshotURL,
 341                                                               })
 342                                                              .ToList());
 343                    }
 12344                }
 3345                catch (Exception e) when (e is not OperationCanceledException) { HideMentionSuggestions(); }
 13346            }
 347
 17348            int lastWrittenCharacterIndex = Math.Max(0, cursorPosition - 1);
 349
 17350            if (mentionFromIndex >= message.Length || message[lastWrittenCharacterIndex] == ' ')
 2351                mentionFromIndex = cursorPosition;
 352
 17353            Match match = mentionRegex.Match(message, mentionFromIndex);
 354
 17355            if (match.Success)
 356            {
 13357                mentionSuggestionCancellationToken = mentionSuggestionCancellationToken.SafeRestart();
 13358                mentionFromIndex = match.Index;
 13359                mentionLength = match.Length;
 13360                string name = match.Value[1..];
 13361                ShowMentionSuggestionsAsync(name, mentionSuggestionCancellationToken.Token).Forget();
 362            }
 363            else
 364            {
 4365                mentionSuggestionCancellationToken.SafeCancelAndDispose();
 4366                mentionFromIndex = lastWrittenCharacterIndex;
 4367                HideMentionSuggestions();
 368            }
 4369        }
 370
 371        private void HandleSendMessage(ChatMessage message)
 372        {
 16373            mentionFromIndex = 0;
 16374            HideMentionSuggestions();
 375
 16376            var ownProfile = userProfileBridge.GetOwn();
 16377            message.sender = ownProfile.userId;
 378
 16379            RegisterMessageHistory(message);
 16380            currentHistoryIteration = 0;
 381
 16382            if (IsSpamming(message.sender) || (IsSpamming(ownProfile.userName) && !string.IsNullOrEmpty(message.body)))
 383            {
 0384                OnMessageSentBlockedBySpam?.Invoke(message);
 0385                return;
 386            }
 387
 32388            foreach (string mention in MentionsUtils.GetAllMentions(message.body))
 0389                socialAnalytics.SendMessageWithMention(mention);
 390
 16391            ApplyWhisperAttributes(message);
 392
 16393            if (message.body.ToLower().StartsWith("/join"))
 394            {
 1395                if (!ownProfile.isGuest)
 1396                    dataStore.channels.channelJoinedSource.Set(ChannelJoinedSource.Command);
 397                else
 398                {
 0399                    dataStore.HUDs.connectWalletModalVisible.Set(true);
 0400                    return;
 401                }
 402            }
 403
 16404            OnSendMessage?.Invoke(message);
 8405        }
 406
 407        private void RegisterMessageHistory(ChatMessage message)
 408        {
 16409            if (string.IsNullOrEmpty(message.body)) return;
 410
 20411            lastMessagesSent.RemoveAll(s => s.Equals(message.body));
 16412            lastMessagesSent.Insert(0, message.body);
 413
 16414            if (lastMessagesSent.Count > MAX_HISTORY_ITERATION)
 0415                lastMessagesSent.RemoveAt(lastMessagesSent.Count - 1);
 16416        }
 417
 418        private void ApplyWhisperAttributes(ChatMessage message)
 419        {
 18420            if (!detectWhisper) return;
 14421            var body = message.body;
 14422            if (string.IsNullOrWhiteSpace(body)) return;
 423
 14424            var match = whisperRegex.Match(body);
 26425            if (!match.Success) return;
 426
 2427            message.messageType = ChatMessage.Type.PRIVATE;
 2428            message.recipient = match.Groups[2].Value;
 2429            message.body = match.Groups[4].Value;
 2430        }
 431
 432        private void HandleInputFieldSelected()
 433        {
 0434            currentHistoryIteration = 0;
 0435            OnInputFieldSelected?.Invoke();
 0436        }
 437
 438        private void HandleInputFieldDeselected() =>
 0439            currentHistoryIteration = 0;
 440
 441        private bool IsSpamming(string senderName)
 442        {
 80443            if (string.IsNullOrEmpty(senderName)) return false;
 444
 52445            var isSpamming = false;
 446
 104447            if (!temporarilyMutedSenders.ContainsKey(senderName)) return false;
 448
 0449            var muteTimestamp = DateTimeOffset.FromUnixTimeMilliseconds((long)temporarilyMutedSenders[senderName]);
 450
 0451            if ((DateTimeOffset.UtcNow - muteTimestamp).Minutes < TEMPORARILY_MUTE_MINUTES)
 0452                isSpamming = true;
 453            else
 0454                temporarilyMutedSenders.Remove(senderName);
 455
 0456            return isSpamming;
 457        }
 458
 459        private void UpdateSpam(ChatEntryModel model)
 460        {
 20461            if (spamMessages.Count == 0)
 13462                spamMessages.Add(model);
 7463            else if (spamMessages[^1].senderName == model.senderName)
 464            {
 5465                if (MessagesSentTooFast(spamMessages[^1].timestamp, model.timestamp))
 466                {
 5467                    spamMessages.Add(model);
 468
 5469                    if (spamMessages.Count >= MAX_CONTINUOUS_MESSAGES)
 470                    {
 0471                        temporarilyMutedSenders.Add(model.senderName, model.timestamp);
 0472                        spamMessages.Clear();
 473                    }
 474                }
 475                else
 0476                    spamMessages.Clear();
 477            }
 478            else
 2479                spamMessages.Clear();
 7480        }
 481
 482        private bool MessagesSentTooFast(ulong oldMessageTimeStamp, ulong newMessageTimeStamp)
 483        {
 5484            var oldDateTime = DateTimeOffset.FromUnixTimeMilliseconds((long)oldMessageTimeStamp);
 5485            var newDateTime = DateTimeOffset.FromUnixTimeMilliseconds((long)newMessageTimeStamp);
 5486            return (newDateTime - oldDateTime).TotalMilliseconds < MIN_MILLISECONDS_BETWEEN_MESSAGES;
 487        }
 488
 489        private void FillInputWithNextMessage()
 490        {
 5491            if (lastMessagesSent.Count == 0) return;
 5492            view.FocusInputField();
 5493            view.SetInputFieldText(lastMessagesSent[currentHistoryIteration]);
 5494            currentHistoryIteration = (currentHistoryIteration + 1) % lastMessagesSent.Count;
 5495        }
 496
 497        private void FillInputWithPreviousMessage()
 498        {
 2499            if (lastMessagesSent.Count == 0)
 500            {
 0501                view.ResetInputField();
 0502                return;
 503            }
 504
 2505            currentHistoryIteration--;
 506
 2507            if (currentHistoryIteration < 0)
 1508                currentHistoryIteration = lastMessagesSent.Count - 1;
 509
 2510            view.FocusInputField();
 2511            view.SetInputFieldText(lastMessagesSent[currentHistoryIteration]);
 2512        }
 513
 514        private void HandleMentionSuggestionSelected(string userId)
 515        {
 0516            view.AddMentionToInputField(mentionFromIndex, mentionLength, userId, mentionSuggestedProfiles[userId].userNa
 0517            socialAnalytics.SendMentionCreated(MentionCreationSource.SuggestionList, userId);
 0518            HideMentionSuggestions();
 0519        }
 520
 521        private void HideMentionSuggestions()
 522        {
 40523            view.HideMentionSuggestions();
 40524            dataStore.mentions.isMentionSuggestionVisible.Set(false);
 40525        }
 526
 527        private void HandleCopyMessageToClipboard(ChatEntryModel model)
 528        {
 1529            clipboard.WriteText(ChatUtils.RemoveNoParse(model.bodyText));
 1530            copyPasteAnalyticsService.Copy("message");
 1531        }
 532    }
 533}