459 lines
14 KiB
C#
459 lines
14 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using CSNetwork;
|
|
using CSNetwork.GPDataType;
|
|
using UnityEngine;
|
|
using UnityEngine.Pool;
|
|
using UnityEngine.UI;
|
|
|
|
namespace BrewMonster.Scripts.ChatUI
|
|
{
|
|
[Serializable]
|
|
public struct ChatMessageData
|
|
{
|
|
public string message;
|
|
public byte channel;
|
|
}
|
|
|
|
public class ChatSystemlUI : MonoBehaviour
|
|
{
|
|
[Header("MiniChat")]
|
|
public Button onOpenChatPanelButton;
|
|
[Tooltip("Parent cho các dòng tin xem trước (nên có VerticalLayoutGroup).")]
|
|
public RectTransform miniChatContent;
|
|
[Tooltip("Null = dùng messagePrefab.")]
|
|
public ChatMessageView miniMessagePrefab;
|
|
[SerializeField] int miniChatPreviewLines = 5;
|
|
[Tooltip("Tắt raycast trên dòng preview để click xuyên xuống onOpenChatPanelButton (TMP/Image mặc định chặn Button).")]
|
|
[SerializeField] bool miniChatPassThroughClicks = true;
|
|
|
|
[Header("ChatPanelUI")] public ScrollRect scrollRect;
|
|
public GameObject chatPanelUIGO;
|
|
public RectTransform content;
|
|
public ChatMessageView messagePrefab;
|
|
public Button closeChatPanelButton;
|
|
public Button emojiButton;
|
|
public Button sendButton;
|
|
|
|
[Header("EmojiPanelUI")]
|
|
public GameObject EmojiPanelUI;
|
|
public Button closeEmojiPanelButton;
|
|
|
|
[Header("Config")] public int maxVisibleMessages = 30;
|
|
public int maxStoredMessages = 2000;
|
|
|
|
[Header("Chat System Data")]
|
|
public ChatSystemSO chatSystemSO;
|
|
private Dictionary<byte, Sprite> _iconCache;
|
|
|
|
private List<ChatMessageData> _messages = new();
|
|
private List<ChatMessageView> _visibleViews = new();
|
|
private List<ChatMessageData> _filteredMessagesCache = new();
|
|
private readonly List<ChatMessageData> _miniFilterBuffer = new();
|
|
private readonly List<ChatMessageView> _miniChatViews = new();
|
|
|
|
private ObjectPool<ChatMessageView> _pool;
|
|
|
|
private bool _userAtBottom = true;
|
|
private ChatChannel _currentFilterChannel = ChatChannel.GP_CHAT_LOCAL;
|
|
|
|
ChatInputHandler _chatInput;
|
|
|
|
void Awake()
|
|
{
|
|
ClearChat();
|
|
_iconCache = new Dictionary<byte, Sprite>();
|
|
if (chatSystemSO != null && chatSystemSO.channelIcons != null)
|
|
{
|
|
foreach (var mapping in chatSystemSO.channelIcons)
|
|
{
|
|
_iconCache[(byte)mapping.channel] = mapping.icon;
|
|
}
|
|
}
|
|
|
|
EventBus.Subscribe<OnEventClearChat>(OnChatMessageClear);
|
|
EventBus.Subscribe<GameSession.ChatMessageEvent>(OnChatMessageReceived);
|
|
EventBus.Subscribe<ChatChannelFilterChangedEvent>(OnChannelFilterChanged);
|
|
_pool = new ObjectPool<ChatMessageView>(
|
|
CreateItem,
|
|
OnGetItem,
|
|
OnReleaseItem,
|
|
OnDestroyItem,
|
|
false,
|
|
10,
|
|
100
|
|
);
|
|
|
|
scrollRect.onValueChanged.AddListener(OnScrollChanged);
|
|
|
|
if (chatPanelUIGO != null)
|
|
chatPanelUIGO.SetActive(false);
|
|
}
|
|
|
|
void OnEnable()
|
|
{
|
|
RefreshMiniChat();
|
|
if (chatPanelUIGO == null || !chatPanelUIGO.activeSelf || _pool == null || content == null)
|
|
return;
|
|
|
|
RefreshVisible();
|
|
}
|
|
|
|
void Start()
|
|
{
|
|
_chatInput = GetComponent<ChatInputHandler>();
|
|
WireUiButtons();
|
|
}
|
|
|
|
void WireUiButtons()
|
|
{
|
|
if (onOpenChatPanelButton != null)
|
|
onOpenChatPanelButton.onClick.AddListener(OpenChatPanel);
|
|
if (closeChatPanelButton != null)
|
|
closeChatPanelButton.onClick.AddListener(CloseChatPanel);
|
|
if (emojiButton != null)
|
|
emojiButton.onClick.AddListener(ToggleEmojiPanel);
|
|
if (closeEmojiPanelButton != null)
|
|
closeEmojiPanelButton.onClick.AddListener(CloseEmojiPanel);
|
|
if (sendButton != null)
|
|
sendButton.onClick.AddListener(OnSendButtonClicked);
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
if (onOpenChatPanelButton != null)
|
|
onOpenChatPanelButton.onClick.RemoveListener(OpenChatPanel);
|
|
if (closeChatPanelButton != null)
|
|
closeChatPanelButton.onClick.RemoveListener(CloseChatPanel);
|
|
if (emojiButton != null)
|
|
emojiButton.onClick.RemoveListener(ToggleEmojiPanel);
|
|
if (closeEmojiPanelButton != null)
|
|
closeEmojiPanelButton.onClick.RemoveListener(CloseEmojiPanel);
|
|
if (sendButton != null)
|
|
sendButton.onClick.RemoveListener(OnSendButtonClicked);
|
|
|
|
EventBus.Unsubscribe<OnEventClearChat>(OnChatMessageClear);
|
|
EventBus.Unsubscribe<GameSession.ChatMessageEvent>(OnChatMessageReceived);
|
|
EventBus.Unsubscribe<ChatChannelFilterChangedEvent>(OnChannelFilterChanged);
|
|
|
|
if (_pool != null)
|
|
{
|
|
_pool.Clear();
|
|
_pool.Dispose();
|
|
}
|
|
|
|
ClearMiniChatViews();
|
|
}
|
|
|
|
private void OnChannelFilterChanged(ChatChannelFilterChangedEvent e)
|
|
{
|
|
if (this == null) return;
|
|
_currentFilterChannel = e.channel;
|
|
RefreshMiniChat();
|
|
if (chatPanelUIGO != null && chatPanelUIGO.activeSelf)
|
|
RefreshVisible();
|
|
}
|
|
|
|
private bool ShouldShowMessage(ChatMessageData data)
|
|
{
|
|
if (_currentFilterChannel == ChatChannel.GP_CHAT_LOCAL) return true;
|
|
if (data.channel == (byte)ChatChannel.GP_CHAT_MISC) return true;
|
|
return data.channel == (byte)_currentFilterChannel;
|
|
}
|
|
|
|
private void OnChatMessageReceived(GameSession.ChatMessageEvent x)
|
|
{
|
|
ChatThreadDispatcher.Instance.Post(() =>
|
|
{
|
|
if (this == null) return;
|
|
AddMessage(x.context, x.channel);
|
|
});
|
|
}
|
|
|
|
|
|
private void OnChatMessageClear(OnEventClearChat obj)
|
|
{
|
|
ChatThreadDispatcher.Instance.Post(() =>
|
|
{
|
|
if (this == null) return;
|
|
ClearChat();
|
|
});
|
|
}
|
|
|
|
ChatMessageView CreateItem()
|
|
{
|
|
if (messagePrefab == null || content == null) return null;
|
|
var item = Instantiate(messagePrefab);
|
|
item.transform.SetParent(content, false);
|
|
return item;
|
|
}
|
|
|
|
void OnGetItem(ChatMessageView item)
|
|
{
|
|
if (item != null)
|
|
item.gameObject.SetActive(true);
|
|
}
|
|
|
|
void OnReleaseItem(ChatMessageView item)
|
|
{
|
|
if (item != null)
|
|
item.gameObject.SetActive(false);
|
|
}
|
|
|
|
void OnDestroyItem(ChatMessageView item)
|
|
{
|
|
if (item != null)
|
|
Destroy(item.gameObject);
|
|
}
|
|
|
|
void OnScrollChanged(Vector2 pos)
|
|
{
|
|
_userAtBottom = scrollRect.verticalNormalizedPosition <= 0.001f;
|
|
}
|
|
|
|
bool IsAtBottom()
|
|
{
|
|
return scrollRect.verticalNormalizedPosition <= 0.001f;
|
|
}
|
|
|
|
public void AddMessage(string msg, byte channel)
|
|
{
|
|
if (this == null) return;
|
|
|
|
var data = new ChatMessageData { message = msg, channel = channel };
|
|
_messages.Add(data);
|
|
|
|
if (_messages.Count > maxStoredMessages)
|
|
_messages.RemoveAt(0);
|
|
|
|
RefreshMiniChat();
|
|
|
|
if (chatPanelUIGO == null || !chatPanelUIGO.activeSelf)
|
|
return;
|
|
|
|
if (ShouldShowMessage(data))
|
|
{
|
|
AddMessageView(data);
|
|
|
|
if (_userAtBottom)
|
|
ScrollToBottom();
|
|
}
|
|
}
|
|
|
|
void AddMessageView(ChatMessageData data)
|
|
{
|
|
if (this == null) return;
|
|
var view = _pool.Get();
|
|
if (view == null) view = CreateItem();
|
|
if (view == null) return;
|
|
|
|
view.transform.SetParent(content, false);
|
|
view.transform.SetAsLastSibling();
|
|
|
|
Sprite icon = _iconCache.ContainsKey(data.channel) ? _iconCache[data.channel] : null;
|
|
view.Bind(icon, data.message);
|
|
|
|
_visibleViews.Add(view);
|
|
|
|
if (_visibleViews.Count > maxVisibleMessages)
|
|
{
|
|
var old = _visibleViews[0];
|
|
_visibleViews.RemoveAt(0);
|
|
if (old != null)
|
|
{
|
|
_pool.Release(old);
|
|
}
|
|
}
|
|
|
|
Canvas.ForceUpdateCanvases();
|
|
}
|
|
|
|
void RefreshVisible()
|
|
{
|
|
foreach (var view in _visibleViews)
|
|
{
|
|
if (view != null)
|
|
_pool.Release(view);
|
|
}
|
|
|
|
_visibleViews.Clear();
|
|
|
|
_filteredMessagesCache.Clear();
|
|
foreach (var msg in _messages)
|
|
{
|
|
if (ShouldShowMessage(msg))
|
|
{
|
|
_filteredMessagesCache.Add(msg);
|
|
}
|
|
}
|
|
|
|
int start = Mathf.Max(0, _filteredMessagesCache.Count - maxVisibleMessages);
|
|
|
|
for (int i = start; i < _filteredMessagesCache.Count; i++)
|
|
{
|
|
var view = _pool.Get();
|
|
if (view == null) view = CreateItem();
|
|
if (view == null) continue;
|
|
|
|
view.transform.SetParent(content, false);
|
|
view.transform.SetAsLastSibling();
|
|
|
|
var data = _filteredMessagesCache[i];
|
|
Sprite icon = _iconCache.ContainsKey(data.channel) ? _iconCache[data.channel] : null;
|
|
view.Bind(icon, data.message);
|
|
|
|
_visibleViews.Add(view);
|
|
}
|
|
|
|
Canvas.ForceUpdateCanvases();
|
|
ScrollToBottom();
|
|
}
|
|
|
|
void RefreshMiniChat()
|
|
{
|
|
if (miniChatContent == null) return;
|
|
|
|
_miniFilterBuffer.Clear();
|
|
foreach (var msg in _messages)
|
|
{
|
|
if (ShouldShowMessage(msg))
|
|
_miniFilterBuffer.Add(msg);
|
|
}
|
|
|
|
int take = Mathf.Min(Mathf.Max(1, miniChatPreviewLines), _miniFilterBuffer.Count);
|
|
int startIdx = _miniFilterBuffer.Count - take;
|
|
|
|
while (_miniChatViews.Count < take)
|
|
{
|
|
var v = CreateMiniChatItem();
|
|
if (v == null) return;
|
|
_miniChatViews.Add(v);
|
|
}
|
|
|
|
while (_miniChatViews.Count > take)
|
|
{
|
|
var last = _miniChatViews[_miniChatViews.Count - 1];
|
|
_miniChatViews.RemoveAt(_miniChatViews.Count - 1);
|
|
if (last != null)
|
|
Destroy(last.gameObject);
|
|
}
|
|
|
|
for (int i = 0; i < take; i++)
|
|
{
|
|
var data = _miniFilterBuffer[startIdx + i];
|
|
var view = _miniChatViews[i];
|
|
Sprite icon = _iconCache.ContainsKey(data.channel) ? _iconCache[data.channel] : null;
|
|
view.gameObject.SetActive(true);
|
|
view.transform.SetSiblingIndex(i);
|
|
if (miniChatPassThroughClicks)
|
|
DisableGraphicsRaycastUnder(view.transform);
|
|
view.Bind(icon, data.message);
|
|
}
|
|
|
|
Canvas.ForceUpdateCanvases();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Mini chat rows use TMP/Image with raycastTarget on — they sit above the open button and steal clicks.
|
|
/// </summary>
|
|
static void DisableGraphicsRaycastUnder(Transform root)
|
|
{
|
|
if (root == null) return;
|
|
foreach (var g in root.GetComponentsInChildren<Graphic>(true))
|
|
g.raycastTarget = false;
|
|
}
|
|
|
|
ChatMessageView CreateMiniChatItem()
|
|
{
|
|
var prefab = miniMessagePrefab != null ? miniMessagePrefab : messagePrefab;
|
|
if (prefab == null || miniChatContent == null) return null;
|
|
var item = Instantiate(prefab, miniChatContent, false);
|
|
return item;
|
|
}
|
|
|
|
public void ScrollToBottom()
|
|
{
|
|
Canvas.ForceUpdateCanvases();
|
|
scrollRect.verticalNormalizedPosition = 0f;
|
|
}
|
|
|
|
public void ClearChat()
|
|
{
|
|
foreach (var view in _visibleViews)
|
|
{
|
|
if (view != null)
|
|
_pool.Release(view);
|
|
}
|
|
|
|
_visibleViews.Clear();
|
|
_messages.Clear();
|
|
ClearMiniChatViews();
|
|
}
|
|
|
|
void ClearMiniChatViews()
|
|
{
|
|
foreach (var v in _miniChatViews)
|
|
{
|
|
if (v != null)
|
|
Destroy(v.gameObject);
|
|
}
|
|
|
|
_miniChatViews.Clear();
|
|
}
|
|
|
|
public void OnHandlerChatButton()
|
|
{
|
|
bool open = !chatPanelUIGO.activeSelf;
|
|
chatPanelUIGO.SetActive(open);
|
|
|
|
if (open)
|
|
RefreshVisible();
|
|
else
|
|
SetEmojiPanelVisible(false);
|
|
}
|
|
|
|
public void OpenChatPanel()
|
|
{
|
|
if (chatPanelUIGO == null) return;
|
|
chatPanelUIGO.SetActive(true);
|
|
RefreshVisible();
|
|
|
|
_chatInput ??= GetComponent<ChatInputHandler>();
|
|
if (_chatInput != null && _chatInput.inputField != null)
|
|
_chatInput.inputField.ActivateInputField();
|
|
}
|
|
|
|
public void CloseChatPanel()
|
|
{
|
|
if (chatPanelUIGO == null) return;
|
|
chatPanelUIGO.SetActive(false);
|
|
SetEmojiPanelVisible(false);
|
|
}
|
|
|
|
public void ToggleEmojiPanel()
|
|
{
|
|
if (EmojiPanelUI == null) return;
|
|
SetEmojiPanelVisible(!EmojiPanelUI.activeSelf);
|
|
}
|
|
|
|
public void CloseEmojiPanel()
|
|
{
|
|
SetEmojiPanelVisible(false);
|
|
}
|
|
|
|
void SetEmojiPanelVisible(bool visible)
|
|
{
|
|
if (EmojiPanelUI != null)
|
|
EmojiPanelUI.SetActive(visible);
|
|
}
|
|
|
|
void OnSendButtonClicked()
|
|
{
|
|
_chatInput ??= GetComponent<ChatInputHandler>();
|
|
_chatInput?.SubmitFromSendButton();
|
|
}
|
|
}
|
|
|
|
public struct OnEventClearChat{}
|
|
}
|