< Summary

Class:DCL.Social.Chat.DefaultChatEntry
Assembly:ChatHUD
File(s):/tmp/workspace/unity-renderer/unity-renderer/Assets/Scripts/MainScripts/DCL/Controllers/HUD/ChatWidgetHUD/DefaultChatEntry.cs
Covered lines:77
Uncovered lines:81
Coverable lines:158
Total lines:390
Line coverage:48.7% (77 of 158)
Covered branches:0
Total branches:0
Covered methods:22
Total methods:29
Method coverage:75.8% (22 of 29)

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity NPath complexity Sequence coverage
GetLocalTime(...)0%2100%
DefaultChatEntry()0%110100%
Awake()0%110100%
OnEnable()0%110100%
OnDisable()0%110100%
OnDestroy()0%220100%
Populate(...)0%110100%
PopulateTask()0%11110100%
GetUserString(...)0%9.029093.75%
GetCoordinatesLink(...)0%220100%
PlaySfx(...)0%64.659011.76%
IsRecentMessage(...)0%6200%
OnPointerClick(...)0%42600%
OnPointerEnter(...)0%6200%
OnPointerExit(...)0%18.695018.18%
SetFadeout(...)0%2.52050%
DockContextMenu(...)0%2100%
DockHoverPanel(...)0%2100%
ConfigureMentionLinkDetector(...)0%2.022083.33%
Update()0%110100%
CheckHoverCoordinates()0%13.35030.77%
ProcessHoverPanelTimer()0%14.115028.57%
ProcessHoverGotoPanelTimer()0%14.115028.57%
RemoveTabs(...)0%2.152066.67%
OnPlayerMentioned()0%6200%

File(s)

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

#LineLine coverage
 1using Cysharp.Threading.Tasks;
 2using DCL.Helpers;
 3using DCL.Interface;
 4using DCL.SettingsCommon;
 5using DCL.Social.Chat.Mentions;
 6using System;
 7using System.Globalization;
 8using System.Threading;
 9using TMPro;
 10using UnityEngine;
 11using UnityEngine.EventSystems;
 12using UnityEngine.UI;
 13using AudioSettings = DCL.SettingsCommon.AudioSettings;
 14
 15namespace DCL.Social.Chat
 16{
 17    public class DefaultChatEntry : ChatEntry, IPointerClickHandler, IPointerEnterHandler, IPointerExitHandler
 18    {
 19        public interface ILocalTimeConverterStrategy
 20        {
 21            DateTime GetLocalTime(ulong timestamp);
 22        }
 23
 24        public class SystemLocalTimeConverterStrategy : ILocalTimeConverterStrategy
 25        {
 26            public DateTime GetLocalTime(ulong timestamp) =>
 027                DateTimeOffset.FromUnixTimeMilliseconds((long)timestamp)
 28                              .ToLocalTime()
 29                              .DateTime;
 30        }
 31
 32        [SerializeField] internal TextMeshProUGUI body;
 4333        [SerializeField] internal float timeToHoverPanel = 1f;
 4334        [SerializeField] internal float timeToHoverGotoPanel = 1f;
 4335        [SerializeField] internal bool showUserName = true;
 36        [SerializeField] private RectTransform hoverPanelPositionReference;
 37        [SerializeField] private RectTransform contextMenuPositionReference;
 38        [SerializeField] private RectTransform copyBodyPositionReference;
 39        [SerializeField] private MentionLinkDetector mentionLinkDetector;
 40        [SerializeField] private Color autoMentionBackgroundColor;
 41        [SerializeField] private Image backgroundImage;
 42        [SerializeField] private Button copyBodyButton;
 43        [SerializeField] private ShowHideAnimator bodyCopiedToast;
 44
 45        private float hoverPanelTimer;
 46        private float hoverGotoPanelTimer;
 47        private bool isOverCoordinates;
 48        private bool isShowingPreview;
 49        private ParcelCoordinates currentCoordinates;
 50        private ChatEntryModel model;
 51        private Color initialEntryColor;
 52
 4353        private readonly CancellationTokenSource populationTaskCancellationTokenSource = new CancellationTokenSource();
 54
 2255        public override ChatEntryModel Model => model;
 56
 6757        public ILocalTimeConverterStrategy LocalTimeConverterStrategy { get; set; } = new SystemLocalTimeConverterStrate
 58
 59        public override string HoverString
 60        {
 61            get
 62            {
 663                var date = LocalTimeConverterStrategy.GetLocalTime(Model.timestamp)
 64                                                     .ToString("MM/dd/yy h:mm:ss tt", CultureInfo.InvariantCulture);
 65
 666                return Model.isLoadingNames ? $"Loading name - {date}" : date;
 67            }
 68        }
 69
 70        public override event Action<ChatEntry> OnUserNameClicked;
 71        public override event Action<ChatEntry> OnTriggerHover;
 72        public override event Action<ChatEntry, ParcelCoordinates> OnTriggerHoverGoto;
 73        public override event Action OnCancelHover;
 74        public override event Action OnCancelGotoHover;
 75        public override event Action<ChatEntry> OnCopyClicked;
 76
 77        public void Awake()
 78        {
 2779            initialEntryColor = backgroundImage.color;
 80
 2781            copyBodyButton.onClick.AddListener(() =>
 82            {
 083                OnCopyClicked?.Invoke(this);
 084                bodyCopiedToast.gameObject.SetActive(true);
 085                bodyCopiedToast.ShowDelayHide(3);
 086            });
 2787        }
 88
 89        private void OnEnable()
 90        {
 3291            copyBodyButton.gameObject.SetActive(false);
 3292        }
 93
 94        private void OnDisable()
 95        {
 3296            OnPointerExit(null);
 3297        }
 98
 99        private void OnDestroy()
 100        {
 27101            populationTaskCancellationTokenSource.Cancel();
 102
 27103            if (mentionLinkDetector != null)
 27104                mentionLinkDetector.OnPlayerMentioned -= OnPlayerMentioned;
 27105        }
 106
 107        public override void Populate(ChatEntryModel chatEntryModel) =>
 23108            PopulateTask(chatEntryModel, populationTaskCancellationTokenSource.Token).Forget();
 109
 110        private async UniTask PopulateTask(ChatEntryModel chatEntryModel, CancellationToken cancellationToken)
 111        {
 23112            model = chatEntryModel;
 113
 23114            if (chatEntryModel is { subType: ChatEntryModel.SubType.RECEIVED, messageType: ChatMessage.Type.PUBLIC })
 2115                backgroundImage.color = initialEntryColor;
 116
 23117            chatEntryModel.bodyText = body.ReplaceUnsupportedCharacters(chatEntryModel.bodyText, '?');
 23118            chatEntryModel.bodyText = RemoveTabs(chatEntryModel.bodyText);
 23119            string userString = GetUserString(chatEntryModel);
 120
 121            // Due to a TMPro bug in Unity 2020 LTS we have to wait several frames before setting the body.text to avoid
 122            // client crash. More info at https://github.com/decentraland/unity-renderer/pull/2345#issuecomment-11557535
 123            // TODO: Remove hack in a newer Unity/TMPro version
 69124            await UniTask.NextFrame(cancellationToken);
 51125            await UniTask.NextFrame(cancellationToken);
 36126            await UniTask.NextFrame(cancellationToken);
 127
 12128            if (!string.IsNullOrEmpty(userString) && showUserName)
 9129                body.text = $"{userString} {chatEntryModel.bodyText}";
 130            else
 3131                body.text = chatEntryModel.bodyText;
 132
 12133            body.text = GetCoordinatesLink(body.text);
 134
 12135            (transform as RectTransform).ForceUpdateLayout();
 136
 12137            PlaySfx(chatEntryModel);
 12138        }
 139
 140        private string GetUserString(ChatEntryModel chatEntryModel)
 141        {
 23142            if (string.IsNullOrEmpty(model.senderName)) return "";
 143
 23144            var baseName = model.senderName;
 145
 23146            switch (chatEntryModel.subType)
 147            {
 148                case ChatEntryModel.SubType.SENT:
 16149                    switch (chatEntryModel.messageType)
 150                    {
 151                        case ChatMessage.Type.PUBLIC:
 7152                        case ChatMessage.Type.PRIVATE when chatEntryModel.isChannelMessage:
 9153                            baseName = "You";
 9154                            break;
 155                        case ChatMessage.Type.PRIVATE:
 7156                            baseName = $"To {chatEntryModel.recipientName}";
 7157                            break;
 158                    }
 159
 160                    break;
 161                case ChatEntryModel.SubType.RECEIVED:
 7162                    switch (chatEntryModel.messageType)
 163                    {
 164                        case ChatMessage.Type.PRIVATE:
 4165                            baseName = $"<color=#5EBD3D>From <link=username://{baseName}>{baseName}</link></color>";
 4166                            break;
 167                        case ChatMessage.Type.PUBLIC:
 2168                            baseName = $"<link=username://{baseName}>{baseName}</link>";
 169                            break;
 170                    }
 171
 172                    break;
 173            }
 174
 23175            baseName = $"<b>{baseName}:</b>";
 176
 23177            return baseName;
 178        }
 179
 180        private string GetCoordinatesLink(string body)
 181        {
 12182            return CoordinateUtils.ReplaceTextCoordinates(body, (text, coordinates) =>
 5183                $"</noparse><link={text}><color=#4886E3><u>{text}</u></color></link><noparse>");
 184        }
 185
 186        private void PlaySfx(ChatEntryModel chatEntryModel)
 187        {
 12188            if (HUDAudioHandler.i == null)
 12189                return;
 190
 0191            bool areSoundsEnabled = Settings.i.audioSettings.Data.chatNotificationType == AudioSettings.ChatNotification
 192
 0193            if (IsRecentMessage(chatEntryModel) && areSoundsEnabled)
 194            {
 0195                switch (chatEntryModel.messageType)
 196                {
 197                    case ChatMessage.Type.PUBLIC:
 198                        // Check whether or not the message was sent by the local player
 0199                        if (chatEntryModel.senderId == UserProfile.GetOwnUserProfile().userId)
 0200                            AudioScriptableObjects.chatSend.Play(true);
 201                        else
 0202                            AudioScriptableObjects.chatReceiveGlobal.Play(true);
 203
 0204                        break;
 205                    case ChatMessage.Type.PRIVATE:
 0206                        switch (chatEntryModel.subType)
 207                        {
 208                            case ChatEntryModel.SubType.RECEIVED:
 0209                                AudioScriptableObjects.chatReceivePrivate.Play(true);
 0210                                break;
 211                            case ChatEntryModel.SubType.SENT:
 0212                                AudioScriptableObjects.chatSend.Play(true);
 0213                                break;
 214                        }
 215
 216                        break;
 217                    case ChatMessage.Type.SYSTEM:
 0218                        AudioScriptableObjects.chatReceiveGlobal.Play(true);
 219                        break;
 220                }
 221            }
 222
 0223            HUDAudioHandler.i.RefreshChatLastCheckedTimestamp();
 0224        }
 225
 226        private bool IsRecentMessage(ChatEntryModel chatEntryModel)
 227        {
 0228            return chatEntryModel.timestamp > HUDAudioHandler.i.chatLastCheckedTimestamp
 229                   && (DateTimeOffset.UtcNow - DateTimeOffset.FromUnixTimeMilliseconds((long)chatEntryModel.timestamp))
 230                  .TotalSeconds < 30;
 231        }
 232
 233        public void OnPointerClick(PointerEventData pointerEventData)
 234        {
 0235            if (pointerEventData.button != PointerEventData.InputButton.Left) return;
 236
 0237            int linkIndex =
 238                TMP_TextUtilities.FindIntersectingLink(body, pointerEventData.position, body.canvas.worldCamera);
 239
 0240            if (linkIndex == -1) return;
 241
 0242            string link = body.textInfo.linkInfo[linkIndex].GetLinkID();
 243
 0244            if (CoordinateUtils.HasValidTextCoordinates(link))
 245            {
 0246                DataStore.i.HUDs.gotoPanelVisible.Set(true, true);
 0247                var parcelCoordinate = CoordinateUtils.ParseCoordinatesString(link);
 0248                DataStore.i.HUDs.gotoPanelCoordinates.Set((parcelCoordinate, null, null));
 249            }
 0250            else if (link.StartsWith("username://"))
 0251                OnUserNameClicked?.Invoke(this);
 0252        }
 253
 254        public void OnPointerEnter(PointerEventData pointerEventData)
 255        {
 0256            if (pointerEventData == null)
 0257                return;
 258
 0259            hoverPanelTimer = timeToHoverPanel;
 0260            copyBodyButton.gameObject.SetActive(true);
 0261            var copyBodyButtonTransform = (RectTransform)copyBodyButton.transform;
 0262            copyBodyButtonTransform.pivot = copyBodyPositionReference.pivot;
 0263            copyBodyButtonTransform.position = copyBodyPositionReference.position;
 0264        }
 265
 266        public void OnPointerExit(PointerEventData pointerEventData)
 267        {
 32268            if (pointerEventData == null)
 32269                return;
 270
 0271            hoverPanelTimer = 0f;
 0272            copyBodyButton.gameObject.SetActive(false);
 273
 0274            var linkIndex =
 275                TMP_TextUtilities.FindIntersectingLink(body, pointerEventData.position,
 276                    DataStore.i.camera.hudsCamera.Get());
 277
 0278            if (linkIndex == -1)
 279            {
 0280                isOverCoordinates = false;
 0281                hoverGotoPanelTimer = 0;
 0282                OnCancelGotoHover?.Invoke();
 283            }
 284
 0285            OnCancelHover?.Invoke();
 0286        }
 287
 288        public override void SetFadeout(bool enabled)
 289        {
 10290            if (enabled) return;
 0291            group.alpha = 1;
 0292        }
 293
 294        public override void DockContextMenu(RectTransform panel)
 295        {
 0296            panel.pivot = new Vector2(0, 0);
 0297            panel.position = contextMenuPositionReference.position;
 0298        }
 299
 300        public override void DockHoverPanel(RectTransform panel)
 301        {
 0302            panel.pivot = hoverPanelPositionReference.pivot;
 0303            panel.position = hoverPanelPositionReference.position;
 0304        }
 305
 306        public override void ConfigureMentionLinkDetector(UserContextMenu userContextMenu)
 307        {
 5308            if (mentionLinkDetector == null)
 0309                return;
 310
 5311            mentionLinkDetector.SetContextMenu(userContextMenu);
 5312            mentionLinkDetector.OnPlayerMentioned -= OnPlayerMentioned;
 5313            mentionLinkDetector.OnPlayerMentioned += OnPlayerMentioned;
 5314        }
 315
 316        private void Update()
 317        {
 318            // TODO: why it needs to be in an update? what about OnPointerEnter/OnPointerExit?
 53319            CheckHoverCoordinates();
 53320            ProcessHoverPanelTimer();
 53321            ProcessHoverGotoPanelTimer();
 53322        }
 323
 324        private void CheckHoverCoordinates()
 325        {
 53326            if (isOverCoordinates)
 0327                return;
 328
 53329            var linkIndex =
 330                TMP_TextUtilities.FindIntersectingLink(body, Input.mousePosition, DataStore.i.camera.hudsCamera.Get());
 331
 53332            if (linkIndex == -1)
 53333                return;
 334
 0335            var link = body.textInfo.linkInfo[linkIndex].GetLinkID();
 0336            if (!CoordinateUtils.HasValidTextCoordinates(link)) return;
 337
 0338            isOverCoordinates = true;
 0339            currentCoordinates = CoordinateUtils.ParseCoordinatesString(link);
 0340            hoverGotoPanelTimer = timeToHoverGotoPanel;
 0341            OnCancelHover?.Invoke();
 0342        }
 343
 344        private void ProcessHoverPanelTimer()
 345        {
 53346            if (hoverPanelTimer <= 0f || isOverCoordinates)
 53347                return;
 348
 0349            hoverPanelTimer -= Time.deltaTime;
 350
 0351            if (hoverPanelTimer <= 0f)
 352            {
 0353                hoverPanelTimer = 0f;
 0354                OnTriggerHover?.Invoke(this);
 355            }
 0356        }
 357
 358        private void ProcessHoverGotoPanelTimer()
 359        {
 53360            if (hoverGotoPanelTimer <= 0f || !isOverCoordinates)
 53361                return;
 362
 0363            hoverGotoPanelTimer -= Time.deltaTime;
 364
 0365            if (hoverGotoPanelTimer <= 0f)
 366            {
 0367                hoverGotoPanelTimer = 0f;
 0368                OnTriggerHoverGoto?.Invoke(this, currentCoordinates);
 369            }
 0370        }
 371
 372        private string RemoveTabs(string text)
 373        {
 23374            if (string.IsNullOrEmpty(text))
 0375                return "";
 376
 377            //NOTE(Brian): ContentSizeFitter doesn't fare well with tabs, so i'm replacing these
 378            //             with spaces.
 23379            return text.Replace("\t", "    ");
 380        }
 381
 382        private void OnPlayerMentioned()
 383        {
 0384            if (!MentionsUtils.IsUserMentionedInText(UserProfile.GetOwnUserProfile().userName, model.bodyText))
 0385                return;
 386
 0387            backgroundImage.color = autoMentionBackgroundColor;
 0388        }
 389    }
 390}