< Summary

Class:ChatHUDView
Assembly:ChatHUD
File(s):/tmp/workspace/unity-renderer/unity-renderer/Assets/Scripts/MainScripts/DCL/Controllers/HUD/ChatWidgetHUD/ChatHUDView.cs
Covered lines:52
Uncovered lines:135
Coverable lines:187
Total lines:396
Line coverage:27.8% (52 of 187)
Covered branches:0
Total branches:0

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity NPath complexity Sequence coverage
ChatHUDView()0%110100%
add_OnShowMenu(...)0%220100%
remove_OnShowMenu(...)0%220100%
add_OnInputFieldSelected(...)0%110100%
remove_OnInputFieldSelected(...)0%2.862040%
add_OnInputFieldDeselected(...)0%110100%
remove_OnInputFieldDeselected(...)0%2.862040%
Create()0%2100%
Awake()0%220100%
OnEnable()0%110100%
OnDisable()0%110100%
Update()0%4.184077.78%
ResetInputField(...)0%6200%
RefreshControl()0%12300%
FocusInputField()0%110100%
UnfocusInputField()0%220100%
SetInputFieldText(...)0%2100%
FadeOutMessages()0%6200%
SetFadeoutMode(...)0%20400%
AddEntry(...)0%20400%
SetEntry(...)0%12300%
RemoveOldestEntry()0%12300%
Hide(...)0%6200%
OnMessageCancelHover()0%2100%
ClearAllEntries()0%2.092071.43%
IsEntryVisible(...)0%2100%
OnInputFieldSubmit(...)0%6200%
WaitThenTriggerSendMessage()0%20400%
OnInputFieldSelect(...)0%2100%
OnInputFieldDeselect(...)0%2100%
OnOpenContextMenu(...)0%2100%
OnMessageTriggerHover(...)0%12300%
OnMessageCoordinatesTriggerHover(...)0%2100%
OnMessageCancelGotoHover()0%2100%
SortEntries()0%2100%
SortEntriesImmediate()0%30500%
HandleNextChatInHistoryInput(...)0%6200%
HandlePreviousChatInHistoryInput(...)0%6200%
UpdateLayout()0%2100%
GetFirstEntry()0%20400%

File(s)

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

#LineLine coverage
 1using System;
 2using System.Collections;
 3using System.Collections.Generic;
 4using System.Linq;
 5using DCL.Helpers;
 6using DCL.Interface;
 7using TMPro;
 8using UnityEngine;
 9using UnityEngine.Events;
 10using UnityEngine.EventSystems;
 11using UnityEngine.UI;
 12using System.Threading;
 13using Cysharp.Threading.Tasks;
 14using DG.Tweening;
 15
 16public class ChatHUDView : BaseComponentView, IChatHUDComponentView
 17{
 18    private const string VIEW_PATH = "SocialBarV1/Chat";
 19
 20    public TMP_InputField inputField;
 21    public RectTransform chatEntriesContainer;
 22    public ScrollRect scrollRect;
 23    public GameObject messageHoverPanel;
 24    public GameObject messageHoverGotoPanel;
 25    public TextMeshProUGUI messageHoverText;
 26    public TextMeshProUGUI messageHoverGotoText;
 27    public UserContextMenu contextMenu;
 28    public UserContextConfirmationDialog confirmationDialog;
 29    [SerializeField] private DefaultChatEntryFactory defaultChatEntryFactory;
 30    [SerializeField] private Model model;
 31    [SerializeField] private InputAction_Trigger nextChatInHistoryInput;
 32    [SerializeField] private InputAction_Trigger previousChatInHistoryInput;
 33
 3134    private readonly Dictionary<string, ChatEntry> entries = new Dictionary<string, ChatEntry>();
 3135    private readonly ChatMessage currentMessage = new ChatMessage();
 36
 3137    private readonly Dictionary<Action, UnityAction<string>> inputFieldSelectedListeners =
 38        new Dictionary<Action, UnityAction<string>>();
 39
 3140    private readonly Dictionary<Action, UnityAction<string>> inputFieldUnselectedListeners =
 41        new Dictionary<Action, UnityAction<string>>();
 42
 43    private int updateLayoutDelayedFrames;
 44    private bool isSortingDirty;
 45
 046    protected bool IsFadeoutModeEnabled => model.enableFadeoutMode;
 47
 48    public event Action<string> OnMessageUpdated;
 49
 50    public event Action OnShowMenu
 51    {
 52        add
 53        {
 354            if (contextMenu != null)
 355                contextMenu.OnShowMenu += value;
 356        }
 57        remove
 58        {
 359            if (contextMenu != null)
 360                contextMenu.OnShowMenu -= value;
 361        }
 62    }
 63
 64    public event Action OnInputFieldSelected
 65    {
 66        add
 67        {
 068            void Action(string s) => value.Invoke();
 369            inputFieldSelectedListeners[value] = Action;
 370            inputField.onSelect.AddListener(Action);
 371        }
 72        remove
 73        {
 374            if (!inputFieldSelectedListeners.ContainsKey(value))
 375                return;
 076            inputField.onSelect.RemoveListener(inputFieldSelectedListeners[value]);
 077            inputFieldSelectedListeners.Remove(value);
 078        }
 79    }
 80
 81    public event Action OnInputFieldDeselected
 82    {
 83        add
 84        {
 085            void Action(string s) => value.Invoke();
 386            inputFieldUnselectedListeners[value] = Action;
 387            inputField.onDeselect.AddListener(Action);
 388        }
 89        remove
 90        {
 391            if (!inputFieldUnselectedListeners.ContainsKey(value))
 392                return;
 093            inputField.onDeselect.RemoveListener(inputFieldUnselectedListeners[value]);
 094            inputFieldUnselectedListeners.Remove(value);
 095        }
 96    }
 97
 98    public event Action OnPreviousChatInHistory;
 99    public event Action OnNextChatInHistory;
 100    public event Action<ChatMessage> OnSendMessage;
 101
 0102    public int EntryCount => entries.Count;
 0103    public IChatEntryFactory ChatEntryFactory { get; set; }
 104
 105    public static ChatHUDView Create()
 106    {
 0107        var view = Instantiate(Resources.Load<GameObject>(VIEW_PATH)).GetComponent<ChatHUDView>();
 0108        return view;
 109    }
 110
 111    public override void Awake()
 112    {
 26113        base.Awake();
 26114        inputField.onSubmit.AddListener(OnInputFieldSubmit);
 26115        inputField.onSelect.AddListener(OnInputFieldSelect);
 26116        inputField.onDeselect.AddListener(OnInputFieldDeselect);
 26117        inputField.onValueChanged.AddListener(str => OnMessageUpdated?.Invoke(str));
 26118        ChatEntryFactory ??= defaultChatEntryFactory;
 26119        model.enableFadeoutMode = true;
 26120    }
 121
 122    public override void OnEnable()
 123    {
 29124        base.OnEnable();
 29125        UpdateLayout();
 29126        nextChatInHistoryInput.OnTriggered += HandleNextChatInHistoryInput;
 29127        previousChatInHistoryInput.OnTriggered += HandlePreviousChatInHistoryInput;
 29128    }
 129
 130    public override void OnDisable()
 131    {
 29132        base.OnDisable();
 29133        nextChatInHistoryInput.OnTriggered -= HandleNextChatInHistoryInput;
 29134        previousChatInHistoryInput.OnTriggered -= HandlePreviousChatInHistoryInput;
 29135    }
 136
 137    public override void Update()
 138    {
 3139        base.Update();
 140
 3141        if (updateLayoutDelayedFrames > 0)
 142        {
 3143            updateLayoutDelayedFrames--;
 144
 3145            if (updateLayoutDelayedFrames <= 0)
 0146                chatEntriesContainer.ForceUpdateLayout(delayed: false);
 147        }
 148
 3149        if (isSortingDirty)
 0150            SortEntriesImmediate();
 3151        isSortingDirty = false;
 3152    }
 153
 154    public void ResetInputField(bool loseFocus = false)
 155    {
 0156        inputField.text = string.Empty;
 0157        inputField.caretColor = Color.white;
 0158        if (loseFocus)
 0159            UnfocusInputField();
 0160    }
 161
 162    public override void RefreshControl()
 163    {
 0164        if (model.isInputFieldFocused)
 0165            FocusInputField();
 0166        SetInputFieldText(model.inputFieldText);
 0167        SetFadeoutMode(model.enableFadeoutMode);
 0168        ClearAllEntries();
 0169        foreach (var entry in model.entries)
 0170            AddEntry(entry);
 0171    }
 172
 173    public void FocusInputField()
 174    {
 2175        inputField.ActivateInputField();
 2176        inputField.Select();
 2177    }
 178
 3179    public void UnfocusInputField() => EventSystem.current?.SetSelectedGameObject(null);
 180
 181    public void SetInputFieldText(string text)
 182    {
 0183        model.inputFieldText = text;
 0184        inputField.text = text;
 0185        inputField.MoveTextEnd(false);
 0186    }
 187
 188    public void FadeOutMessages()
 189    {
 0190        foreach (var entry in entries.Values)
 0191            entry.FadeOut();
 0192    }
 193
 194    private void SetFadeoutMode(bool enabled)
 195    {
 0196        model.enableFadeoutMode = enabled;
 197
 0198        foreach (var entry in entries.Values)
 0199            entry.SetFadeout(enabled && IsEntryVisible(entry));
 200
 0201        if (enabled)
 0202            confirmationDialog.Hide();
 0203    }
 204
 205    public virtual void AddEntry(ChatEntryModel model, bool setScrollPositionToBottom = false)
 206    {
 0207        if (entries.ContainsKey(model.messageId))
 208        {
 0209            var chatEntry = entries[model.messageId];
 0210            chatEntry.SetFadeout(this.model.enableFadeoutMode);
 0211            chatEntry.Populate(model);
 212
 0213            SetEntry(model.messageId, chatEntry, setScrollPositionToBottom);
 0214        }
 215        else
 216        {
 0217            var chatEntry = ChatEntryFactory.Create(model);
 0218            chatEntry.SetFadeout(this.model.enableFadeoutMode);
 0219            chatEntry.Populate(model);
 220
 0221            if (chatEntry.showUserName && model.subType.Equals(ChatEntryModel.SubType.RECEIVED))
 0222                chatEntry.OnUserNameClicked += OnOpenContextMenu;
 223
 0224            chatEntry.OnTriggerHover += OnMessageTriggerHover;
 0225            chatEntry.OnTriggerHoverGoto += OnMessageCoordinatesTriggerHover;
 0226            chatEntry.OnCancelHover += OnMessageCancelHover;
 0227            chatEntry.OnCancelGotoHover += OnMessageCancelGotoHover;
 228
 0229            SetEntry(model.messageId, chatEntry, setScrollPositionToBottom);
 230        }
 0231    }
 232
 233    public virtual void SetEntry(string messageId, ChatEntry chatEntry, bool setScrollPositionToBottom = false)
 234    {
 0235        chatEntry.transform.SetParent(chatEntriesContainer, false);
 0236        entries[messageId] = chatEntry;
 237
 0238        SortEntries();
 0239        UpdateLayout();
 240
 0241        if (setScrollPositionToBottom && scrollRect.verticalNormalizedPosition > 0)
 0242            scrollRect.verticalNormalizedPosition = 0;
 0243    }
 244
 245    public void RemoveOldestEntry()
 246    {
 0247        if (entries.Count <= 0) return;
 0248        var firstEntry = GetFirstEntry();
 0249        if (!firstEntry) return;
 0250        entries.Remove(firstEntry.Model.messageId);
 251        // TODO: entries are not being destroyed if many are added in the same frame. Instead use pooling or DestroyImme
 0252        Destroy(firstEntry.gameObject);
 0253        UpdateLayout();
 0254    }
 255
 256    public override void Hide(bool instant = false)
 257    {
 0258        base.Hide(instant);
 0259        if (contextMenu == null)
 0260            return;
 0261        contextMenu.Hide();
 0262        confirmationDialog.Hide();
 0263    }
 264
 265    public void OnMessageCancelHover()
 266    {
 0267        messageHoverPanel.SetActive(false);
 0268        messageHoverText.text = string.Empty;
 0269    }
 270
 271    public virtual void ClearAllEntries()
 272    {
 2273        foreach (var entry in entries.Values)
 0274            Destroy(entry.gameObject);
 1275        entries.Clear();
 1276        UpdateLayout();
 1277    }
 278
 279    private bool IsEntryVisible(ChatEntry entry)
 280    {
 0281        int visibleCorners =
 282            (entry.transform as RectTransform).CountCornersVisibleFrom(scrollRect.viewport.transform as RectTransform);
 0283        return visibleCorners > 0;
 284    }
 285
 286    private void OnInputFieldSubmit(string message)
 287    {
 0288        currentMessage.body = message;
 0289        currentMessage.sender = string.Empty;
 0290        currentMessage.messageType = ChatMessage.Type.NONE;
 0291        currentMessage.recipient = string.Empty;
 292
 293        // A TMP_InputField is automatically marked as 'wasCanceled' when the ESC key is pressed
 0294        if (inputField.wasCanceled)
 0295            currentMessage.body = string.Empty;
 296
 297        // we have to wait one frame to disengage the flow triggered by OnSendMessage
 298        // otherwise it crashes the application (WebGL only) due a TextMeshPro bug
 0299        StartCoroutine(WaitThenTriggerSendMessage());
 0300    }
 301
 302    private IEnumerator WaitThenTriggerSendMessage()
 303    {
 0304        yield return null;
 0305        OnSendMessage?.Invoke(currentMessage);
 0306    }
 307
 0308    private void OnInputFieldSelect(string message) { AudioScriptableObjects.inputFieldFocus.Play(true); }
 309
 0310    private void OnInputFieldDeselect(string message) { AudioScriptableObjects.inputFieldUnfocus.Play(true); }
 311
 312    private void OnOpenContextMenu(DefaultChatEntry chatEntry)
 313    {
 0314        chatEntry.DockContextMenu((RectTransform) contextMenu.transform);
 0315        contextMenu.transform.parent = transform.parent;
 0316        contextMenu.transform.SetAsLastSibling();
 0317        contextMenu.Show(chatEntry.Model.senderId);
 0318    }
 319
 320    protected virtual void OnMessageTriggerHover(DefaultChatEntry chatEntry)
 321    {
 0322        if (contextMenu == null || contextMenu.isVisible)
 0323            return;
 324
 0325        messageHoverText.text = chatEntry.messageLocalDateTime;
 0326        chatEntry.DockHoverPanel((RectTransform) messageHoverPanel.transform);
 0327        messageHoverPanel.SetActive(true);
 0328    }
 329
 330    private void OnMessageCoordinatesTriggerHover(DefaultChatEntry chatEntry, ParcelCoordinates parcelCoordinates)
 331    {
 0332        messageHoverGotoText.text = $"{parcelCoordinates} INFO";
 0333        chatEntry.DockHoverPanel((RectTransform) messageHoverGotoPanel.transform);
 0334        messageHoverGotoPanel.SetActive(true);
 0335    }
 336
 337    private void OnMessageCancelGotoHover()
 338    {
 0339        messageHoverGotoPanel.SetActive(false);
 0340        messageHoverGotoText.text = string.Empty;
 0341    }
 342
 0343    private void SortEntries() => isSortingDirty = true;
 344
 345    private void SortEntriesImmediate()
 346    {
 0347        if (this.entries.Count <= 0) return;
 348
 0349        var entries = this.entries.Values.OrderBy(x => x.Model.timestamp).ToList();
 350
 0351        for (var i = 0; i < entries.Count; i++)
 352        {
 0353            if (entries[i].transform.GetSiblingIndex() != i)
 0354                entries[i].transform.SetSiblingIndex(i);
 355        }
 0356    }
 357
 0358    private void HandleNextChatInHistoryInput(DCLAction_Trigger action) => OnNextChatInHistory?.Invoke();
 359
 0360    private void HandlePreviousChatInHistoryInput(DCLAction_Trigger action) => OnPreviousChatInHistory?.Invoke();
 361
 362    private void UpdateLayout()
 363    {
 364        // we have to wait several frames before updating the layout
 365        // every message entry waits for 3 frames before updating its own layout
 366        // we gotta force the layout updating after that
 367        // TODO: simplify this change to a bool when we update to a working TextMeshPro version
 0368        updateLayoutDelayedFrames = 4;
 0369    }
 370
 371    private ChatEntry GetFirstEntry()
 372    {
 0373        ChatEntry firstEntry = null;
 374
 0375        for (var i = 0; i < chatEntriesContainer.childCount; i++)
 376        {
 0377            var firstChildTransform = chatEntriesContainer.GetChild(i);
 0378            if (!firstChildTransform) continue;
 0379            var entry = firstChildTransform.GetComponent<ChatEntry>();
 0380            if (!entry) continue;
 0381            firstEntry = entry;
 0382            break;
 383        }
 384
 0385        return firstEntry;
 386    }
 387
 388    [Serializable]
 389    private struct Model
 390    {
 391        public bool isInputFieldFocused;
 392        public string inputFieldText;
 393        public bool enableFadeoutMode;
 394        public ChatEntryModel[] entries;
 395    }
 396}