1635 lines
49 KiB
C#
1635 lines
49 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using DG.Tweening;
|
|
using GameServer.Client;
|
|
using UnityEngine;
|
|
using UnityEngine.EventSystems;
|
|
using UnityEngine.UI;
|
|
|
|
using Bansonic;
|
|
|
|
public class globalChatSystem : MonoBehaviour
|
|
{
|
|
private enum ChatChannelMode
|
|
{
|
|
World,
|
|
Private
|
|
}
|
|
|
|
private sealed class WorldSystemFeedEntry
|
|
{
|
|
public string text;
|
|
public long createdAtUnixMs;
|
|
public long sequence;
|
|
}
|
|
|
|
private sealed class WorldFeedEntry
|
|
{
|
|
public object entry;
|
|
public long timestampMs;
|
|
public int order;
|
|
public string key;
|
|
public long sequence;
|
|
}
|
|
|
|
public GameObject playerMessagePrefab;
|
|
public GameObject selfMessagePrefab;
|
|
public GameObject worldSystemMessagePrefab;
|
|
public Transform playerMessageParent;
|
|
|
|
[Header("extra")]
|
|
public Button emojiButton;
|
|
|
|
[Header("send")]
|
|
public InputField messageInputField;
|
|
public Button sendButton;
|
|
|
|
[Header("button")]
|
|
public Button closeButton;
|
|
|
|
[Header("display")]
|
|
[SerializeField] private int maxMessageHistory = 1000;
|
|
[SerializeField] private int messageDisplayWindow = 20;
|
|
[SerializeField] private Color[] messageTypeColors =
|
|
{
|
|
Color.white,
|
|
new Color(0.23292516f, 0.7924528f, 0.22801706f, 1f)
|
|
};
|
|
|
|
[SerializeField] Text channelNameText;
|
|
[SerializeField] Button back_to_worldwideChatButton;
|
|
[SerializeField] Toggle globalChatToggle;
|
|
[SerializeField] Toggle friendChatToggle;
|
|
[SerializeField] private CanvasGroup chatCanvasGroup;
|
|
|
|
private ArenaRoomService _service;
|
|
private ScrollRect _scrollRect;
|
|
private Coroutine _chatCooldownRoutine;
|
|
private Text _placeholderText;
|
|
private string _defaultPlaceholder = string.Empty;
|
|
private float _nextSendAllowedAt;
|
|
private bool _isSendingMessage;
|
|
private string _pendingSendContent = string.Empty;
|
|
private readonly List<WorldSystemFeedEntry> _systemFeedEntries = new List<WorldSystemFeedEntry>();
|
|
private bool _isViewPinnedToBottom = true;
|
|
private bool _suppressScrollEvents;
|
|
private int _windowStartIndex = -1;
|
|
private int _lastFeedCount;
|
|
private readonly List<string> _renderedKeys = new List<string>();
|
|
private readonly List<GameObject> _renderedObjects = new List<GameObject>();
|
|
private readonly Dictionary<string, long> _feedSequences = new Dictionary<string, long>(StringComparer.Ordinal);
|
|
private long _nextFeedSequence = 1;
|
|
private ChatChannelMode _currentMode = ChatChannelMode.World;
|
|
private string _currentPrivatePartnerId;
|
|
private string _lastPrivatePartnerId = string.Empty;
|
|
private string _defaultChannelName = string.Empty;
|
|
private static string _pendingPrivatePartnerId;
|
|
private Coroutine _layoutRefreshCoroutine;
|
|
private bool _layoutRefreshDirty;
|
|
private bool _scrollToBottomAfterLayout;
|
|
private readonly List<RectTransform> _pendingLayoutDirtyRoots = new List<RectTransform>();
|
|
private Coroutine _refocusInputCoroutine;
|
|
private bool _refocusInputRequested;
|
|
private bool _suppressModeToggleEvents;
|
|
private bool _modeTogglesAutoCreated;
|
|
private ToggleGroup _chatModeToggleGroup;
|
|
private Tween _visibilityTween;
|
|
public static globalChatSystem Instance { get; private set; }
|
|
public static event Action<string> ActivePrivateConversationChanged;
|
|
|
|
private const float WorldChatSendCooldownSeconds = 10f;
|
|
private const float PrivateChatSendCooldownSeconds = 1f;
|
|
private const int InitialHistoryLoadLimit = 20;
|
|
private const int PrivateChatCacheLimit = 50;
|
|
private const int MaxDisplayWindowLimit = 20;
|
|
private const string DefaultWorldChannelName = "世界聊天";
|
|
private const string CooldownPlaceholderFormat = "\u53d1\u9001\u51b7\u5374\u4e2d {0}s";
|
|
private const string PrivateChannelNameFormat = "\u6b63\u5728\u4e0e[{0}]\u79c1\u804a";
|
|
private const string DefaultFriendChannelName = "\u597d\u53cb\u804a\u5929";
|
|
private const string NoRecentPrivateConversationNotice = "\u6682\u65e0\u6700\u8fd1\u79c1\u804a\u5bf9\u8c61";
|
|
private const string LastPrivatePartnerPrefKey = "global_chat_last_private_partner_id";
|
|
|
|
public static void OpenPrivateConversation(string steamId)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(steamId))
|
|
{
|
|
return;
|
|
}
|
|
|
|
_pendingPrivatePartnerId = steamId.Trim();
|
|
globalChatSystem target = Instance;
|
|
if (target == null)
|
|
{
|
|
target = FindAnyInstance();
|
|
}
|
|
|
|
if (target == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!target.gameObject.activeSelf)
|
|
{
|
|
target.gameObject.SetActive(true);
|
|
return;
|
|
}
|
|
|
|
target.StartCoroutine(target.OpenPrivateConversationCoroutine(_pendingPrivatePartnerId));
|
|
_pendingPrivatePartnerId = null;
|
|
}
|
|
|
|
private static globalChatSystem FindAnyInstance()
|
|
{
|
|
globalChatSystem[] all = FindObjectsByType<globalChatSystem>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
|
return all != null && all.Length > 0 ? all[0] : null;
|
|
}
|
|
|
|
private void Awake()
|
|
{
|
|
Instance = this;
|
|
_lastPrivatePartnerId = PlayerPrefs.GetString(LastPrivatePartnerPrefKey, string.Empty);
|
|
EnsureCanvasGroupReference();
|
|
}
|
|
|
|
private void Start()
|
|
{
|
|
_service = ArenaRoomService.Instance;
|
|
_scrollRect = playerMessageParent != null ? playerMessageParent.GetComponentInParent<ScrollRect>() : null;
|
|
if (_scrollRect != null)
|
|
{
|
|
_scrollRect.onValueChanged.RemoveListener(OnScrollValueChanged);
|
|
_scrollRect.onValueChanged.AddListener(OnScrollValueChanged);
|
|
}
|
|
|
|
if (closeButton != null)
|
|
{
|
|
closeButton.onClick.RemoveListener(CloseSelf);
|
|
closeButton.onClick.AddListener(CloseSelf);
|
|
}
|
|
|
|
if (sendButton != null)
|
|
{
|
|
sendButton.onClick.RemoveListener(OnSendClicked);
|
|
sendButton.onClick.AddListener(OnSendClicked);
|
|
}
|
|
|
|
if (messageInputField != null)
|
|
{
|
|
messageInputField.onEndEdit.RemoveListener(OnInputEndEdit);
|
|
messageInputField.onEndEdit.AddListener(OnInputEndEdit);
|
|
_placeholderText = messageInputField.placeholder as Text;
|
|
if (_placeholderText != null)
|
|
{
|
|
_defaultPlaceholder = _placeholderText.text;
|
|
}
|
|
}
|
|
|
|
if (channelNameText != null)
|
|
{
|
|
_defaultChannelName = channelNameText.text ?? string.Empty;
|
|
if (string.IsNullOrWhiteSpace(_defaultChannelName))
|
|
{
|
|
_defaultChannelName = DefaultWorldChannelName;
|
|
}
|
|
}
|
|
|
|
if (back_to_worldwideChatButton != null)
|
|
{
|
|
back_to_worldwideChatButton.onClick.RemoveListener(HandleBackToWorldClicked);
|
|
back_to_worldwideChatButton.onClick.AddListener(HandleBackToWorldClicked);
|
|
}
|
|
|
|
EnsureChatModeToggles();
|
|
|
|
if (_service != null)
|
|
{
|
|
_service.SetWorldChatCacheLimit(GetEffectiveCacheLimit());
|
|
_service.SetPrivateChatCacheLimit(PrivateChatCacheLimit);
|
|
_ = _service.EnsureWorldChatConnected();
|
|
}
|
|
|
|
UpdateSendUiState();
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
EnsureCanvasGroupReference();
|
|
PlayOpenFadeIfNeeded();
|
|
|
|
if (_service == null)
|
|
{
|
|
_service = ArenaRoomService.Instance;
|
|
}
|
|
|
|
if (_service == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_service.OnWorldChatMessagesChanged -= HandleWorldChatMessagesChanged;
|
|
_service.OnWorldChatMessagesChanged += HandleWorldChatMessagesChanged;
|
|
_service.OnPrivateChatMessagesChanged -= HandlePrivateChatMessagesChanged;
|
|
_service.OnPrivateChatMessagesChanged += HandlePrivateChatMessagesChanged;
|
|
ClearDisplayedMessages();
|
|
_isViewPinnedToBottom = true;
|
|
_windowStartIndex = -1;
|
|
EnsureChatModeToggles();
|
|
NotifyActivePrivateConversationChanged();
|
|
|
|
if (!string.IsNullOrWhiteSpace(_pendingPrivatePartnerId))
|
|
{
|
|
string targetSteamId = _pendingPrivatePartnerId;
|
|
_pendingPrivatePartnerId = null;
|
|
_ = SwitchToPrivateChannelAsync(targetSteamId);
|
|
}
|
|
else
|
|
{
|
|
_ = InitializeWorldChatAsync();
|
|
}
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
if (_service != null)
|
|
{
|
|
_service.OnWorldChatMessagesChanged -= HandleWorldChatMessagesChanged;
|
|
_service.OnPrivateChatMessagesChanged -= HandlePrivateChatMessagesChanged;
|
|
}
|
|
|
|
if (_scrollRect != null)
|
|
{
|
|
_scrollRect.onValueChanged.RemoveListener(OnScrollValueChanged);
|
|
}
|
|
|
|
UnbindChatModeToggleListeners();
|
|
|
|
if (_chatCooldownRoutine != null)
|
|
{
|
|
StopCoroutine(_chatCooldownRoutine);
|
|
_chatCooldownRoutine = null;
|
|
}
|
|
|
|
if (_layoutRefreshCoroutine != null)
|
|
{
|
|
StopCoroutine(_layoutRefreshCoroutine);
|
|
_layoutRefreshCoroutine = null;
|
|
}
|
|
|
|
if (_refocusInputCoroutine != null)
|
|
{
|
|
StopCoroutine(_refocusInputCoroutine);
|
|
_refocusInputCoroutine = null;
|
|
}
|
|
|
|
RestoreDefaultPlaceholder();
|
|
NotifyActivePrivateConversationChanged();
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
KillVisibilityTween();
|
|
|
|
if (closeButton != null)
|
|
{
|
|
closeButton.onClick.RemoveListener(CloseSelf);
|
|
}
|
|
|
|
if (back_to_worldwideChatButton != null)
|
|
{
|
|
back_to_worldwideChatButton.onClick.RemoveListener(HandleBackToWorldClicked);
|
|
}
|
|
|
|
UnbindChatModeToggleListeners();
|
|
|
|
if (sendButton != null)
|
|
{
|
|
sendButton.onClick.RemoveListener(OnSendClicked);
|
|
}
|
|
|
|
if (messageInputField != null)
|
|
{
|
|
messageInputField.onEndEdit.RemoveListener(OnInputEndEdit);
|
|
}
|
|
|
|
if (_scrollRect != null)
|
|
{
|
|
_scrollRect.onValueChanged.RemoveListener(OnScrollValueChanged);
|
|
}
|
|
|
|
if (_layoutRefreshCoroutine != null)
|
|
{
|
|
StopCoroutine(_layoutRefreshCoroutine);
|
|
_layoutRefreshCoroutine = null;
|
|
}
|
|
|
|
if (_refocusInputCoroutine != null)
|
|
{
|
|
StopCoroutine(_refocusInputCoroutine);
|
|
_refocusInputCoroutine = null;
|
|
}
|
|
|
|
if (Instance == this)
|
|
{
|
|
Instance = null;
|
|
}
|
|
|
|
RestoreDefaultPlaceholder();
|
|
}
|
|
|
|
private async System.Threading.Tasks.Task InitializeWorldChatAsync()
|
|
{
|
|
if (_service == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
await SwitchToWorldChannelAsync();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.LogWarning($"[globalChatSystem] Init world chat failed: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private void HandleWorldChatMessagesChanged(IReadOnlyList<ArenaRoomChatMessage> messages)
|
|
{
|
|
if (_currentMode != ChatChannelMode.World)
|
|
{
|
|
return;
|
|
}
|
|
|
|
RenderMessages(messages, true);
|
|
}
|
|
|
|
private void HandlePrivateChatMessagesChanged(string partnerId, IReadOnlyList<ArenaRoomChatMessage> messages)
|
|
{
|
|
if (_currentMode != ChatChannelMode.Private
|
|
|| string.IsNullOrWhiteSpace(_currentPrivatePartnerId)
|
|
|| !string.Equals(_currentPrivatePartnerId, partnerId, StringComparison.Ordinal))
|
|
{
|
|
return;
|
|
}
|
|
|
|
ApplyChannelHeader();
|
|
RenderMessages(messages, true);
|
|
}
|
|
|
|
private IEnumerator OpenPrivateConversationCoroutine(string steamId)
|
|
{
|
|
yield return null;
|
|
if (!string.IsNullOrWhiteSpace(steamId))
|
|
{
|
|
_ = SwitchToPrivateChannelAsync(steamId);
|
|
}
|
|
}
|
|
|
|
private async System.Threading.Tasks.Task SwitchToWorldChannelAsync()
|
|
{
|
|
if (_service == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_currentMode = ChatChannelMode.World;
|
|
_currentPrivatePartnerId = null;
|
|
ResetFeedForChannelSwitch();
|
|
ApplyChannelHeader();
|
|
NotifyActivePrivateConversationChanged();
|
|
await _service.EnsureWorldChatConnected();
|
|
await _service.LoadWorldHistory(InitialHistoryLoadLimit);
|
|
RenderMessages(_service.GetWorldChatMessages(), false);
|
|
}
|
|
|
|
private async System.Threading.Tasks.Task SwitchToPrivateChannelAsync(string partnerId)
|
|
{
|
|
if (_service == null || string.IsNullOrWhiteSpace(partnerId))
|
|
{
|
|
return;
|
|
}
|
|
|
|
_currentMode = ChatChannelMode.Private;
|
|
_currentPrivatePartnerId = partnerId.Trim();
|
|
RememberPrivatePartner(_currentPrivatePartnerId);
|
|
ResetFeedForChannelSwitch();
|
|
ApplyChannelHeader();
|
|
NotifyActivePrivateConversationChanged();
|
|
await _service.LoadPrivateHistory(_currentPrivatePartnerId, InitialHistoryLoadLimit);
|
|
RenderMessages(_service.GetPrivateChatMessages(_currentPrivatePartnerId), false);
|
|
}
|
|
|
|
private void HandleBackToWorldClicked()
|
|
{
|
|
_ = SwitchToWorldChannelAsync();
|
|
}
|
|
|
|
private void ApplyChannelHeader()
|
|
{
|
|
if (back_to_worldwideChatButton != null)
|
|
{
|
|
back_to_worldwideChatButton.gameObject.SetActive(_currentMode == ChatChannelMode.Private && !HasChatModeToggles());
|
|
}
|
|
|
|
if (_modeTogglesAutoCreated && channelNameText != null)
|
|
{
|
|
channelNameText.gameObject.SetActive(false);
|
|
}
|
|
|
|
UpdateChatModeToggleVisuals();
|
|
|
|
if (channelNameText == null || _modeTogglesAutoCreated)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_currentMode == ChatChannelMode.Private && !string.IsNullOrWhiteSpace(_currentPrivatePartnerId))
|
|
{
|
|
string partnerName = ResolvePrivatePartnerName(_currentPrivatePartnerId);
|
|
channelNameText.text = string.Format(PrivateChannelNameFormat, partnerName);
|
|
return;
|
|
}
|
|
|
|
channelNameText.text = string.IsNullOrWhiteSpace(_defaultChannelName)
|
|
? DefaultWorldChannelName
|
|
: _defaultChannelName;
|
|
}
|
|
|
|
private string ResolvePrivatePartnerName(string partnerId)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(partnerId))
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
if (_service != null)
|
|
{
|
|
SocialFriendEntry friend = _service.GetFriendsSnapshot().FirstOrDefault(item =>
|
|
item != null && string.Equals(item.friend_steam_id, partnerId, StringComparison.Ordinal));
|
|
if (friend != null)
|
|
{
|
|
return NetworkManager.ResolveDisplayName(friend.friend_name, partnerId, false);
|
|
}
|
|
}
|
|
|
|
return NetworkManager.ResolveDisplayName(string.Empty, partnerId, false);
|
|
}
|
|
|
|
private void ResetFeedForChannelSwitch()
|
|
{
|
|
_systemFeedEntries.Clear();
|
|
ClearDisplayedMessages();
|
|
_isViewPinnedToBottom = true;
|
|
_windowStartIndex = -1;
|
|
_lastFeedCount = 0;
|
|
}
|
|
|
|
private async void OnSendClicked()
|
|
{
|
|
if (_service == null || messageInputField == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (Time.unscaledTime < _nextSendAllowedAt)
|
|
{
|
|
AddSystemMessage(string.Format("\u4f60\u8fd8\u6709{0}\u79d2\u624d\u53ef\u4ee5\u53d1\u9001\u6d88\u606f", Mathf.CeilToInt(_nextSendAllowedAt - Time.unscaledTime)));
|
|
RefocusInputNextFrame();
|
|
UpdateSendUiState();
|
|
return;
|
|
}
|
|
|
|
string content = messageInputField.text != null ? messageInputField.text.Trim() : string.Empty;
|
|
if (string.IsNullOrWhiteSpace(content))
|
|
{
|
|
RefocusInputNextFrame();
|
|
return;
|
|
}
|
|
|
|
if (_isSendingMessage && string.Equals(content, _pendingSendContent, StringComparison.Ordinal))
|
|
{
|
|
RefocusInputNextFrame();
|
|
return;
|
|
}
|
|
|
|
BeginSendCooldown(GetCurrentSendCooldownSeconds());
|
|
_isSendingMessage = true;
|
|
_pendingSendContent = content;
|
|
|
|
try
|
|
{
|
|
messageInputField.text = string.Empty;
|
|
RefocusInputNextFrame();
|
|
if (_currentMode == ChatChannelMode.Private)
|
|
{
|
|
await _service.SendPrivateMessage(_currentPrivatePartnerId, content);
|
|
}
|
|
else
|
|
{
|
|
await _service.SendWorldMessage(content);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (messageInputField != null && string.IsNullOrEmpty(messageInputField.text))
|
|
{
|
|
messageInputField.text = content;
|
|
RefocusInputNextFrame();
|
|
}
|
|
|
|
AddSystemMessage(MapWorldChatErrorToNotice(ex));
|
|
Debug.LogWarning($"[globalChatSystem] Send world message failed: {ex.Message}");
|
|
}
|
|
finally
|
|
{
|
|
_isSendingMessage = false;
|
|
_pendingSendContent = string.Empty;
|
|
}
|
|
}
|
|
|
|
private void BeginSendCooldown(float cooldownSeconds)
|
|
{
|
|
_nextSendAllowedAt = Time.unscaledTime + Mathf.Max(0f, cooldownSeconds);
|
|
if (_chatCooldownRoutine != null)
|
|
{
|
|
StopCoroutine(_chatCooldownRoutine);
|
|
}
|
|
|
|
_chatCooldownRoutine = StartCoroutine(ChatCooldownCoroutine());
|
|
UpdateSendUiState();
|
|
}
|
|
|
|
private IEnumerator ChatCooldownCoroutine()
|
|
{
|
|
while (Time.unscaledTime < _nextSendAllowedAt)
|
|
{
|
|
UpdateSendUiState();
|
|
yield return null;
|
|
}
|
|
|
|
_chatCooldownRoutine = null;
|
|
UpdateSendUiState();
|
|
}
|
|
|
|
private void UpdateSendUiState()
|
|
{
|
|
bool coolingDown = Time.unscaledTime < _nextSendAllowedAt;
|
|
if (sendButton != null)
|
|
{
|
|
sendButton.interactable = !coolingDown;
|
|
}
|
|
|
|
if (_placeholderText != null)
|
|
{
|
|
if (coolingDown)
|
|
{
|
|
float remain = Mathf.Max(0f, _nextSendAllowedAt - Time.unscaledTime);
|
|
_placeholderText.text = string.Format(CooldownPlaceholderFormat, Mathf.CeilToInt(remain));
|
|
}
|
|
else
|
|
{
|
|
_placeholderText.text = _defaultPlaceholder;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void RestoreDefaultPlaceholder()
|
|
{
|
|
if (_placeholderText != null)
|
|
{
|
|
_placeholderText.text = _defaultPlaceholder;
|
|
}
|
|
}
|
|
|
|
private void OnInputEndEdit(string value)
|
|
{
|
|
if (!isActiveAndEnabled || messageInputField == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (Input.GetKeyDown(KeyCode.Return) || Input.GetKeyDown(KeyCode.KeypadEnter))
|
|
{
|
|
OnSendClicked();
|
|
}
|
|
}
|
|
|
|
private void RenderMessages(IReadOnlyList<ArenaRoomChatMessage> messages, bool preserveWindow)
|
|
{
|
|
if (playerMessageParent == null || playerMessagePrefab == null || messages == null)
|
|
{
|
|
ClearDisplayedMessages();
|
|
return;
|
|
}
|
|
|
|
List<WorldFeedEntry> feed = BuildFeed(messages);
|
|
UpdateWindowState(feed.Count, preserveWindow);
|
|
|
|
int startIndex = Mathf.Clamp(_windowStartIndex, 0, Mathf.Max(0, feed.Count - 1));
|
|
int endIndex = Mathf.Min(feed.Count, startIndex + GetRenderedWindowCapacity());
|
|
var visibleEntries = new List<WorldFeedEntry>(Mathf.Max(0, endIndex - startIndex));
|
|
for (int i = startIndex; i < endIndex; i++)
|
|
{
|
|
visibleEntries.Add(feed[i]);
|
|
}
|
|
|
|
bool changed = ApplyRenderedEntries(visibleEntries);
|
|
if (changed)
|
|
{
|
|
ScheduleLayoutRefresh(_isViewPinnedToBottom);
|
|
}
|
|
}
|
|
|
|
private float GetCurrentSendCooldownSeconds()
|
|
{
|
|
return _currentMode == ChatChannelMode.Private
|
|
? PrivateChatSendCooldownSeconds
|
|
: WorldChatSendCooldownSeconds;
|
|
}
|
|
|
|
private void ClearDisplayedMessages()
|
|
{
|
|
if (playerMessageParent == null)
|
|
{
|
|
_renderedKeys.Clear();
|
|
_renderedObjects.Clear();
|
|
return;
|
|
}
|
|
|
|
for (int i = playerMessageParent.childCount - 1; i >= 0; i--)
|
|
{
|
|
Transform child = playerMessageParent.GetChild(i);
|
|
child.SetParent(null, false);
|
|
child.gameObject.SetActive(false);
|
|
Destroy(child.gameObject);
|
|
}
|
|
|
|
_renderedKeys.Clear();
|
|
_renderedObjects.Clear();
|
|
}
|
|
|
|
private Transform CreateSelfMessageWrapper()
|
|
{
|
|
GameObject wrapper = new GameObject("SelfMessageWrapper", typeof(RectTransform), typeof(ContentSizeFitter), typeof(VerticalLayoutGroup));
|
|
RectTransform rect = wrapper.GetComponent<RectTransform>();
|
|
rect.SetParent(playerMessageParent, false);
|
|
rect.anchorMin = new Vector2(0f, 1f);
|
|
rect.anchorMax = new Vector2(0f, 1f);
|
|
rect.pivot = new Vector2(0.5f, 0.5f);
|
|
rect.anchoredPosition = Vector2.zero;
|
|
rect.sizeDelta = new Vector2(530f, 66f);
|
|
|
|
ContentSizeFitter fitter = wrapper.GetComponent<ContentSizeFitter>();
|
|
fitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained;
|
|
fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
|
|
|
VerticalLayoutGroup layout = wrapper.GetComponent<VerticalLayoutGroup>();
|
|
layout.padding = new RectOffset(0, 0, 0, 0);
|
|
layout.spacing = 0f;
|
|
layout.childAlignment = TextAnchor.UpperRight;
|
|
layout.reverseArrangement = false;
|
|
layout.childControlWidth = false;
|
|
layout.childControlHeight = false;
|
|
layout.childScaleWidth = false;
|
|
layout.childScaleHeight = false;
|
|
layout.childForceExpandWidth = false;
|
|
layout.childForceExpandHeight = true;
|
|
|
|
return rect;
|
|
}
|
|
|
|
private void OnScrollValueChanged(Vector2 normalizedPosition)
|
|
{
|
|
if (_suppressScrollEvents || _service == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
IReadOnlyList<ArenaRoomChatMessage> messages = GetActiveMessages();
|
|
List<WorldFeedEntry> feed = BuildFeed(messages);
|
|
if (feed.Count <= GetRenderedWindowCapacity())
|
|
{
|
|
_isViewPinnedToBottom = true;
|
|
return;
|
|
}
|
|
|
|
if (normalizedPosition.y <= 0.05f)
|
|
{
|
|
int maxStart = Mathf.Max(0, feed.Count - GetRenderedWindowCapacity());
|
|
if (_windowStartIndex < maxStart)
|
|
{
|
|
_windowStartIndex = Mathf.Min(maxStart, _windowStartIndex + GetEffectiveDisplayWindow());
|
|
_isViewPinnedToBottom = _windowStartIndex >= maxStart;
|
|
RenderMessages(messages, true);
|
|
SetScrollNormalizedPosition(0.15f);
|
|
return;
|
|
}
|
|
|
|
_isViewPinnedToBottom = true;
|
|
}
|
|
else if (normalizedPosition.y >= 0.95f && _windowStartIndex > 0)
|
|
{
|
|
_windowStartIndex = Mathf.Max(0, _windowStartIndex - GetEffectiveDisplayWindow());
|
|
_isViewPinnedToBottom = false;
|
|
RenderMessages(messages, true);
|
|
SetScrollNormalizedPosition(0.85f);
|
|
}
|
|
else
|
|
{
|
|
_isViewPinnedToBottom = false;
|
|
}
|
|
}
|
|
|
|
private List<WorldFeedEntry> BuildFeed(IReadOnlyList<ArenaRoomChatMessage> messages)
|
|
{
|
|
List<WorldFeedEntry> feed = new List<WorldFeedEntry>();
|
|
int order = 0;
|
|
int startIndex = Mathf.Max(0, messages.Count - GetEffectiveCacheLimit());
|
|
for (int i = startIndex; i < messages.Count; i++)
|
|
{
|
|
ArenaRoomChatMessage message = messages[i];
|
|
if (message != null)
|
|
{
|
|
feed.Add(new WorldFeedEntry
|
|
{
|
|
entry = message,
|
|
timestampMs = ParseMessageUnixTimestamp(message.created_at),
|
|
order = order++,
|
|
key = BuildMessageKey(message),
|
|
sequence = GetOrAssignFeedSequence(BuildMessageKey(message))
|
|
});
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < _systemFeedEntries.Count; i++)
|
|
{
|
|
WorldSystemFeedEntry systemEntry = _systemFeedEntries[i];
|
|
if (systemEntry == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
feed.Add(new WorldFeedEntry
|
|
{
|
|
entry = systemEntry,
|
|
timestampMs = systemEntry.createdAtUnixMs,
|
|
order = order++,
|
|
key = BuildSystemKey(systemEntry),
|
|
sequence = systemEntry.sequence
|
|
});
|
|
}
|
|
|
|
feed.Sort(CompareFeedEntries);
|
|
return feed;
|
|
}
|
|
|
|
private void UpdateWindowState(int feedCount, bool preserveWindow)
|
|
{
|
|
_lastFeedCount = feedCount;
|
|
int windowCapacity = GetRenderedWindowCapacity();
|
|
int maxStart = Mathf.Max(0, feedCount - windowCapacity);
|
|
if (!preserveWindow || _windowStartIndex < 0 || _isViewPinnedToBottom)
|
|
{
|
|
_windowStartIndex = maxStart;
|
|
_isViewPinnedToBottom = true;
|
|
return;
|
|
}
|
|
|
|
_windowStartIndex = Mathf.Clamp(_windowStartIndex, 0, maxStart);
|
|
}
|
|
|
|
private void AddSystemMessage(string content)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(content))
|
|
{
|
|
return;
|
|
}
|
|
|
|
_systemFeedEntries.Add(new WorldSystemFeedEntry
|
|
{
|
|
text = content,
|
|
createdAtUnixMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
|
sequence = _nextFeedSequence++
|
|
});
|
|
TrimSystemFeedCache();
|
|
|
|
RenderMessages(GetActiveMessages(), true);
|
|
}
|
|
|
|
private bool ApplyRenderedEntries(List<WorldFeedEntry> visibleEntries)
|
|
{
|
|
var desiredKeys = new List<string>(visibleEntries.Count);
|
|
for (int i = 0; i < visibleEntries.Count; i++)
|
|
{
|
|
desiredKeys.Add(visibleEntries[i].key);
|
|
}
|
|
|
|
if (desiredKeys.Count == _renderedKeys.Count)
|
|
{
|
|
bool identical = true;
|
|
for (int i = 0; i < desiredKeys.Count; i++)
|
|
{
|
|
if (!string.Equals(desiredKeys[i], _renderedKeys[i], StringComparison.Ordinal))
|
|
{
|
|
identical = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (identical)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
int prefix = 0;
|
|
int sharedCount = Mathf.Min(_renderedKeys.Count, desiredKeys.Count);
|
|
while (prefix < sharedCount && string.Equals(_renderedKeys[prefix], desiredKeys[prefix], StringComparison.Ordinal))
|
|
{
|
|
prefix++;
|
|
}
|
|
|
|
int suffix = 0;
|
|
while (suffix < (_renderedKeys.Count - prefix)
|
|
&& suffix < (desiredKeys.Count - prefix)
|
|
&& string.Equals(
|
|
_renderedKeys[_renderedKeys.Count - 1 - suffix],
|
|
desiredKeys[desiredKeys.Count - 1 - suffix],
|
|
StringComparison.Ordinal))
|
|
{
|
|
suffix++;
|
|
}
|
|
|
|
for (int i = _renderedObjects.Count - suffix - 1; i >= prefix; i--)
|
|
{
|
|
GameObject instance = _renderedObjects[i];
|
|
if (instance != null)
|
|
{
|
|
Destroy(instance);
|
|
}
|
|
|
|
_renderedObjects.RemoveAt(i);
|
|
_renderedKeys.RemoveAt(i);
|
|
}
|
|
|
|
for (int i = prefix; i < desiredKeys.Count - suffix; i++)
|
|
{
|
|
GameObject instance = CreateFeedObject(visibleEntries[i]);
|
|
_renderedObjects.Insert(i, instance);
|
|
_renderedKeys.Insert(i, desiredKeys[i]);
|
|
RegisterLayoutDirtyRoot(instance);
|
|
}
|
|
|
|
for (int i = 0; i < _renderedObjects.Count; i++)
|
|
{
|
|
if (_renderedObjects[i] != null)
|
|
{
|
|
_renderedObjects[i].transform.SetSiblingIndex(i);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private GameObject CreateFeedObject(WorldFeedEntry feedEntry)
|
|
{
|
|
if (feedEntry.entry is WorldSystemFeedEntry systemEntry)
|
|
{
|
|
if (worldSystemMessagePrefab == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
GameObject systemInstance = Instantiate(worldSystemMessagePrefab, playerMessageParent, false);
|
|
systemMessagePrefab systemController = systemInstance.GetComponent<systemMessagePrefab>();
|
|
if (systemController != null)
|
|
{
|
|
systemController.Bind(systemEntry.text);
|
|
}
|
|
|
|
return systemInstance;
|
|
}
|
|
|
|
ArenaRoomChatMessage message = feedEntry.entry as ArenaRoomChatMessage;
|
|
if (message == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
string localSteamId = NetworkManager.Instance != null ? NetworkManager.Instance.SteamId : string.Empty;
|
|
string senderSteamId = !string.IsNullOrWhiteSpace(message.sender_id) ? message.sender_id : message.steam_id;
|
|
bool isSelf = !string.IsNullOrWhiteSpace(localSteamId)
|
|
&& string.Equals(senderSteamId, localSteamId, StringComparison.Ordinal);
|
|
Color bubbleColor = ResolveMessageBubbleColor(isSelf);
|
|
string displayName = NetworkManager.ResolveDisplayName(message.display_name, senderSteamId, false);
|
|
string avatarUrl = NetworkManager.ResolveAvatarUrl(message.avatar_url, senderSteamId);
|
|
|
|
GameObject instance;
|
|
if (isSelf && selfMessagePrefab != null)
|
|
{
|
|
Transform selfWrapper = CreateSelfMessageWrapper();
|
|
instance = Instantiate(selfMessagePrefab, selfWrapper, false);
|
|
instance = selfWrapper.gameObject;
|
|
}
|
|
else
|
|
{
|
|
instance = Instantiate(playerMessagePrefab, playerMessageParent, false);
|
|
}
|
|
|
|
playerMessagePrefab controller = instance.GetComponentInChildren<playerMessagePrefab>();
|
|
if (controller != null)
|
|
{
|
|
controller.Bind(senderSteamId, avatarUrl, displayName, string.Empty, message.content, bubbleColor);
|
|
}
|
|
|
|
return instance;
|
|
}
|
|
|
|
private static string BuildSystemKey(WorldSystemFeedEntry entry)
|
|
{
|
|
return $"system:{entry.createdAtUnixMs}:{entry.text}";
|
|
}
|
|
|
|
private static string BuildMessageKey(ArenaRoomChatMessage message)
|
|
{
|
|
if (message == null)
|
|
{
|
|
return "message:null";
|
|
}
|
|
|
|
if (message.id > 0)
|
|
{
|
|
return $"message:id:{message.id}";
|
|
}
|
|
|
|
string sender = !string.IsNullOrWhiteSpace(message.sender_id) ? message.sender_id : message.steam_id;
|
|
return $"message:fallback:{message.room_code}:{sender}:{message.receiver_id}:{message.created_at}:{message.content}";
|
|
}
|
|
|
|
private void TrimSystemFeedCache()
|
|
{
|
|
int overflow = _systemFeedEntries.Count - GetEffectiveCacheLimit();
|
|
if (overflow > 0)
|
|
{
|
|
_systemFeedEntries.RemoveRange(0, overflow);
|
|
}
|
|
}
|
|
|
|
private static int CompareFeedEntries(WorldFeedEntry left, WorldFeedEntry right)
|
|
{
|
|
int compare = left.sequence.CompareTo(right.sequence);
|
|
if (compare != 0)
|
|
{
|
|
return compare;
|
|
}
|
|
|
|
compare = left.timestampMs.CompareTo(right.timestampMs);
|
|
if (compare != 0)
|
|
{
|
|
return compare;
|
|
}
|
|
|
|
return left.order.CompareTo(right.order);
|
|
}
|
|
|
|
private long GetOrAssignFeedSequence(string key)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(key))
|
|
{
|
|
return _nextFeedSequence++;
|
|
}
|
|
|
|
if (_feedSequences.TryGetValue(key, out long sequence))
|
|
{
|
|
return sequence;
|
|
}
|
|
|
|
sequence = _nextFeedSequence++;
|
|
_feedSequences[key] = sequence;
|
|
return sequence;
|
|
}
|
|
|
|
private static long ParseMessageUnixTimestamp(string createdAt)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(createdAt))
|
|
{
|
|
return long.MaxValue;
|
|
}
|
|
|
|
if (long.TryParse(createdAt, out long unixValue))
|
|
{
|
|
return unixValue >= 1_000_000_000_000L ? unixValue : unixValue * 1000L;
|
|
}
|
|
|
|
if (DateTimeOffset.TryParse(createdAt, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out DateTimeOffset parsed))
|
|
{
|
|
return parsed.ToUnixTimeMilliseconds();
|
|
}
|
|
|
|
return long.MaxValue;
|
|
}
|
|
|
|
private Color ResolveMessageBubbleColor(bool isSelf)
|
|
{
|
|
if (messageTypeColors == null || messageTypeColors.Length == 0)
|
|
{
|
|
return Color.white;
|
|
}
|
|
|
|
if (isSelf && messageTypeColors.Length > 1)
|
|
{
|
|
return messageTypeColors[1];
|
|
}
|
|
|
|
return messageTypeColors[0];
|
|
}
|
|
|
|
private void SetScrollNormalizedPosition(float value)
|
|
{
|
|
if (_scrollRect == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_suppressScrollEvents = true;
|
|
_scrollRect.verticalNormalizedPosition = Mathf.Clamp01(value);
|
|
_suppressScrollEvents = false;
|
|
}
|
|
|
|
private static string MapWorldChatErrorToNotice(Exception ex)
|
|
{
|
|
string raw = ex != null ? ex.Message ?? string.Empty : string.Empty;
|
|
string code = raw;
|
|
int separator = raw.IndexOf(':');
|
|
if (separator >= 0)
|
|
{
|
|
code = raw.Substring(0, separator).Trim();
|
|
}
|
|
|
|
switch (code)
|
|
{
|
|
case "BLOCKED_CONTENT":
|
|
return "\u6d88\u606f\u5185\u5305\u542b\u8fdd\u7981\u8bcd";
|
|
case "RATE_LIMIT":
|
|
return "\u53d1\u9001\u8fc7\u4e8e\u9891\u7e41\uff0c\u8bf7\u7a0d\u540e\u518d\u8bd5";
|
|
case "MESSAGE_TOO_LONG":
|
|
return "\u6d88\u606f\u8d85\u8fc7\u957f\u5ea6\u9650\u5236";
|
|
case "EMPTY_MESSAGE":
|
|
return "\u6d88\u606f\u5185\u5bb9\u4e3a\u7a7a";
|
|
default:
|
|
return raw;
|
|
}
|
|
}
|
|
|
|
private void ScheduleLayoutRefresh(bool scrollToBottom)
|
|
{
|
|
_layoutRefreshDirty = true;
|
|
_scrollToBottomAfterLayout |= scrollToBottom;
|
|
if (_layoutRefreshCoroutine == null)
|
|
{
|
|
_layoutRefreshCoroutine = StartCoroutine(DeferredLayoutRefreshCoroutine());
|
|
}
|
|
}
|
|
|
|
private IEnumerator DeferredLayoutRefreshCoroutine()
|
|
{
|
|
while (true)
|
|
{
|
|
yield return null;
|
|
|
|
bool shouldRebuild = _layoutRefreshDirty;
|
|
bool shouldScrollToBottom = _scrollToBottomAfterLayout;
|
|
_layoutRefreshDirty = false;
|
|
_scrollToBottomAfterLayout = false;
|
|
|
|
if (shouldRebuild)
|
|
{
|
|
ForceRebuildLayoutImmediate();
|
|
if (shouldScrollToBottom && _scrollRect != null)
|
|
{
|
|
Canvas.ForceUpdateCanvases();
|
|
_scrollRect.verticalNormalizedPosition = 0f;
|
|
}
|
|
}
|
|
|
|
if (!_layoutRefreshDirty)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
_layoutRefreshCoroutine = null;
|
|
}
|
|
|
|
private void ForceRebuildLayoutImmediate()
|
|
{
|
|
if (!(playerMessageParent is RectTransform parentRect))
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (int i = 0; i < _pendingLayoutDirtyRoots.Count; i++)
|
|
{
|
|
RectTransform childRect = _pendingLayoutDirtyRoots[i];
|
|
if (childRect != null)
|
|
{
|
|
LayoutRebuilder.ForceRebuildLayoutImmediate(childRect);
|
|
}
|
|
}
|
|
_pendingLayoutDirtyRoots.Clear();
|
|
|
|
LayoutRebuilder.ForceRebuildLayoutImmediate(parentRect);
|
|
if (parentRect.parent is RectTransform outerRect)
|
|
{
|
|
LayoutRebuilder.ForceRebuildLayoutImmediate(outerRect);
|
|
}
|
|
Canvas.ForceUpdateCanvases();
|
|
}
|
|
|
|
private void RefocusInputNextFrame()
|
|
{
|
|
_refocusInputRequested = true;
|
|
if (_refocusInputCoroutine == null)
|
|
{
|
|
_refocusInputCoroutine = StartCoroutine(RefocusInputCoroutine());
|
|
}
|
|
}
|
|
|
|
private IEnumerator RefocusInputCoroutine()
|
|
{
|
|
while (true)
|
|
{
|
|
yield return null;
|
|
if (!_refocusInputRequested)
|
|
{
|
|
break;
|
|
}
|
|
|
|
_refocusInputRequested = false;
|
|
if (messageInputField == null || !gameObject.activeInHierarchy)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
EventSystem.current?.SetSelectedGameObject(messageInputField.gameObject);
|
|
messageInputField.Select();
|
|
messageInputField.ActivateInputField();
|
|
messageInputField.MoveTextEnd(false);
|
|
}
|
|
|
|
_refocusInputCoroutine = null;
|
|
}
|
|
|
|
private void RegisterLayoutDirtyRoot(GameObject instance)
|
|
{
|
|
if (instance == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
RectTransform rect = instance.transform as RectTransform;
|
|
if (rect == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (int i = 0; i < _pendingLayoutDirtyRoots.Count; i++)
|
|
{
|
|
if (_pendingLayoutDirtyRoots[i] == rect)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
_pendingLayoutDirtyRoots.Add(rect);
|
|
}
|
|
|
|
private void CloseSelf()
|
|
{
|
|
HideWithFade();
|
|
}
|
|
|
|
private void EnsureCanvasGroupReference()
|
|
{
|
|
if (chatCanvasGroup == null)
|
|
{
|
|
chatCanvasGroup = GetComponent<CanvasGroup>();
|
|
}
|
|
}
|
|
|
|
private void KillVisibilityTween()
|
|
{
|
|
if (_visibilityTween != null && _visibilityTween.IsActive())
|
|
{
|
|
_visibilityTween.Kill(false);
|
|
}
|
|
|
|
_visibilityTween = null;
|
|
}
|
|
|
|
private void PlayOpenFadeIfNeeded()
|
|
{
|
|
if (chatCanvasGroup == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
KillVisibilityTween();
|
|
chatCanvasGroup.gameObject.SetActive(true);
|
|
chatCanvasGroup.alpha = 0f;
|
|
chatCanvasGroup.interactable = false;
|
|
chatCanvasGroup.blocksRaycasts = false;
|
|
_visibilityTween = chatCanvasGroup.DOFade(1f, 0.25f)
|
|
.SetEase(Ease.Linear)
|
|
.SetUpdate(true)
|
|
.OnComplete(() =>
|
|
{
|
|
if (chatCanvasGroup == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
chatCanvasGroup.alpha = 1f;
|
|
chatCanvasGroup.interactable = true;
|
|
chatCanvasGroup.blocksRaycasts = true;
|
|
_visibilityTween = null;
|
|
});
|
|
}
|
|
|
|
private void HideWithFade()
|
|
{
|
|
if (chatCanvasGroup == null)
|
|
{
|
|
gameObject.SetActive(false);
|
|
return;
|
|
}
|
|
|
|
KillVisibilityTween();
|
|
chatCanvasGroup.interactable = false;
|
|
chatCanvasGroup.blocksRaycasts = false;
|
|
_visibilityTween = chatCanvasGroup.DOFade(0f, 0.25f)
|
|
.SetEase(Ease.Linear)
|
|
.SetUpdate(true)
|
|
.OnComplete(() =>
|
|
{
|
|
if (chatCanvasGroup != null)
|
|
{
|
|
chatCanvasGroup.alpha = 0f;
|
|
}
|
|
|
|
gameObject.SetActive(false);
|
|
_visibilityTween = null;
|
|
});
|
|
}
|
|
|
|
public static bool IsViewingPrivateConversation(string steamId)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(steamId) || Instance == null || !Instance.isActiveAndEnabled)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return Instance._currentMode == ChatChannelMode.Private
|
|
&& string.Equals(Instance._currentPrivatePartnerId, steamId.Trim(), StringComparison.Ordinal);
|
|
}
|
|
|
|
private void EnsureChatModeToggles()
|
|
{
|
|
if (globalChatToggle == null || friendChatToggle == null)
|
|
{
|
|
CreateFallbackChatModeToggles();
|
|
}
|
|
|
|
BindChatModeToggleListeners();
|
|
UpdateChatModeToggleVisuals();
|
|
}
|
|
|
|
private void BindChatModeToggleListeners()
|
|
{
|
|
if (globalChatToggle != null)
|
|
{
|
|
globalChatToggle.onValueChanged.RemoveListener(HandleGlobalChatToggleChanged);
|
|
globalChatToggle.onValueChanged.AddListener(HandleGlobalChatToggleChanged);
|
|
}
|
|
|
|
if (friendChatToggle != null)
|
|
{
|
|
friendChatToggle.onValueChanged.RemoveListener(HandleFriendChatToggleChanged);
|
|
friendChatToggle.onValueChanged.AddListener(HandleFriendChatToggleChanged);
|
|
}
|
|
}
|
|
|
|
private void UnbindChatModeToggleListeners()
|
|
{
|
|
if (globalChatToggle != null)
|
|
{
|
|
globalChatToggle.onValueChanged.RemoveListener(HandleGlobalChatToggleChanged);
|
|
}
|
|
|
|
if (friendChatToggle != null)
|
|
{
|
|
friendChatToggle.onValueChanged.RemoveListener(HandleFriendChatToggleChanged);
|
|
}
|
|
}
|
|
|
|
private void HandleGlobalChatToggleChanged(bool isOn)
|
|
{
|
|
if (_suppressModeToggleEvents || !isOn)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_ = SwitchToWorldChannelAsync();
|
|
}
|
|
|
|
private void HandleFriendChatToggleChanged(bool isOn)
|
|
{
|
|
if (_suppressModeToggleEvents || !isOn)
|
|
{
|
|
return;
|
|
}
|
|
|
|
OpenRecentPrivateConversationFromToggle();
|
|
}
|
|
|
|
private async void OpenRecentPrivateConversationFromToggle()
|
|
{
|
|
string partnerId = ResolvePreferredPrivatePartnerId();
|
|
if (string.IsNullOrWhiteSpace(partnerId))
|
|
{
|
|
AddSystemMessage(NoRecentPrivateConversationNotice);
|
|
UpdateChatModeToggleVisuals();
|
|
return;
|
|
}
|
|
|
|
await SwitchToPrivateChannelAsync(partnerId);
|
|
}
|
|
|
|
private void UpdateChatModeToggleVisuals()
|
|
{
|
|
if (globalChatToggle != null)
|
|
{
|
|
SetToggleWithoutNotify(globalChatToggle, _currentMode == ChatChannelMode.World);
|
|
}
|
|
|
|
if (friendChatToggle != null)
|
|
{
|
|
SetToggleWithoutNotify(friendChatToggle, _currentMode == ChatChannelMode.Private);
|
|
}
|
|
|
|
}
|
|
|
|
private void SetToggleWithoutNotify(Toggle toggle, bool value)
|
|
{
|
|
if (toggle == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_suppressModeToggleEvents = true;
|
|
toggle.SetIsOnWithoutNotify(value);
|
|
_suppressModeToggleEvents = false;
|
|
}
|
|
|
|
private bool HasChatModeToggles()
|
|
{
|
|
return globalChatToggle != null && friendChatToggle != null;
|
|
}
|
|
|
|
private void CreateFallbackChatModeToggles()
|
|
{
|
|
if (globalChatToggle != null && friendChatToggle != null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
RectTransform headerRect = channelNameText != null ? channelNameText.rectTransform.parent as RectTransform : null;
|
|
if (headerRect == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (channelNameText != null)
|
|
{
|
|
channelNameText.gameObject.SetActive(false);
|
|
}
|
|
|
|
headerRect.sizeDelta = new Vector2(360f, 34f);
|
|
|
|
if (_chatModeToggleGroup == null)
|
|
{
|
|
_chatModeToggleGroup = headerRect.GetComponent<ToggleGroup>();
|
|
if (_chatModeToggleGroup == null)
|
|
{
|
|
_chatModeToggleGroup = headerRect.gameObject.AddComponent<ToggleGroup>();
|
|
}
|
|
_chatModeToggleGroup.allowSwitchOff = false;
|
|
}
|
|
|
|
if (globalChatToggle == null)
|
|
{
|
|
globalChatToggle = CreateRuntimeModeToggle(headerRect, "globalChatToggle", DefaultWorldChannelName, new Vector2(-90f, 0f));
|
|
}
|
|
|
|
if (friendChatToggle == null)
|
|
{
|
|
friendChatToggle = CreateRuntimeModeToggle(headerRect, "friendChatToggle", DefaultFriendChannelName, new Vector2(90f, 0f));
|
|
}
|
|
|
|
if (globalChatToggle != null)
|
|
{
|
|
globalChatToggle.group = _chatModeToggleGroup;
|
|
}
|
|
|
|
if (friendChatToggle != null)
|
|
{
|
|
friendChatToggle.group = _chatModeToggleGroup;
|
|
}
|
|
|
|
_modeTogglesAutoCreated = globalChatToggle != null && friendChatToggle != null;
|
|
}
|
|
|
|
private Toggle CreateRuntimeModeToggle(RectTransform parent, string objectName, string labelText, Vector2 anchoredPosition)
|
|
{
|
|
Sprite defaultSprite = Resources.GetBuiltinResource<Sprite>("UISprite.psd");
|
|
Font defaultFont = channelNameText != null && channelNameText.font != null
|
|
? channelNameText.font
|
|
: Resources.GetBuiltinResource<Font>("Arial.ttf");
|
|
|
|
GameObject toggleObject = new GameObject(objectName, typeof(RectTransform), typeof(Image), typeof(Toggle));
|
|
RectTransform toggleRect = toggleObject.GetComponent<RectTransform>();
|
|
toggleRect.SetParent(parent, false);
|
|
toggleRect.anchorMin = new Vector2(0.5f, 0.5f);
|
|
toggleRect.anchorMax = new Vector2(0.5f, 0.5f);
|
|
toggleRect.pivot = new Vector2(0.5f, 0.5f);
|
|
toggleRect.anchoredPosition = anchoredPosition;
|
|
toggleRect.sizeDelta = new Vector2(164f, 30f);
|
|
|
|
Image background = toggleObject.GetComponent<Image>();
|
|
background.sprite = defaultSprite;
|
|
background.type = Image.Type.Sliced;
|
|
background.color = new Color(1f, 1f, 1f, 0.32f);
|
|
|
|
Toggle toggle = toggleObject.GetComponent<Toggle>();
|
|
toggle.targetGraphic = background;
|
|
toggle.transition = Selectable.Transition.ColorTint;
|
|
|
|
ColorBlock colors = toggle.colors;
|
|
colors.normalColor = new Color(1f, 1f, 1f, 0.45f);
|
|
colors.highlightedColor = new Color(1f, 1f, 1f, 0.55f);
|
|
colors.pressedColor = new Color(0.86f, 0.94f, 1f, 0.85f);
|
|
colors.selectedColor = new Color(0.86f, 0.94f, 1f, 0.95f);
|
|
colors.disabledColor = new Color(1f, 1f, 1f, 0.2f);
|
|
toggle.colors = colors;
|
|
|
|
GameObject checkmarkObject = new GameObject("Checkmark", typeof(RectTransform), typeof(Image));
|
|
RectTransform checkmarkRect = checkmarkObject.GetComponent<RectTransform>();
|
|
checkmarkRect.SetParent(toggleRect, false);
|
|
checkmarkRect.anchorMin = Vector2.zero;
|
|
checkmarkRect.anchorMax = Vector2.one;
|
|
checkmarkRect.offsetMin = new Vector2(3f, 3f);
|
|
checkmarkRect.offsetMax = new Vector2(-3f, -3f);
|
|
|
|
Image checkmark = checkmarkObject.GetComponent<Image>();
|
|
checkmark.sprite = defaultSprite;
|
|
checkmark.type = Image.Type.Sliced;
|
|
checkmark.color = new Color(0.63f, 0.87f, 1f, 0.95f);
|
|
toggle.graphic = checkmark;
|
|
|
|
GameObject labelObject = new GameObject("Label", typeof(RectTransform), typeof(Text));
|
|
RectTransform labelRect = labelObject.GetComponent<RectTransform>();
|
|
labelRect.SetParent(toggleRect, false);
|
|
labelRect.anchorMin = Vector2.zero;
|
|
labelRect.anchorMax = Vector2.one;
|
|
labelRect.offsetMin = new Vector2(10f, 0f);
|
|
labelRect.offsetMax = new Vector2(-10f, 0f);
|
|
|
|
Text label = labelObject.GetComponent<Text>();
|
|
label.font = defaultFont;
|
|
label.fontSize = 16;
|
|
label.alignment = TextAnchor.MiddleCenter;
|
|
label.horizontalOverflow = HorizontalWrapMode.Overflow;
|
|
label.verticalOverflow = VerticalWrapMode.Overflow;
|
|
label.color = new Color(0.19607843f, 0.19607843f, 0.19607843f, 1f);
|
|
label.text = labelText;
|
|
|
|
return toggle;
|
|
}
|
|
|
|
private void RememberPrivatePartner(string partnerId)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(partnerId))
|
|
{
|
|
return;
|
|
}
|
|
|
|
_lastPrivatePartnerId = partnerId.Trim();
|
|
PlayerPrefs.SetString(LastPrivatePartnerPrefKey, _lastPrivatePartnerId);
|
|
PlayerPrefs.Save();
|
|
}
|
|
|
|
private string ResolvePreferredPrivatePartnerId()
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(_lastPrivatePartnerId))
|
|
{
|
|
return _lastPrivatePartnerId;
|
|
}
|
|
|
|
if (TryGetMostRecentPrivatePartnerId(out string recentPartnerId))
|
|
{
|
|
return recentPartnerId;
|
|
}
|
|
|
|
return string.Empty;
|
|
}
|
|
|
|
private bool TryGetMostRecentPrivatePartnerId(out string partnerId)
|
|
{
|
|
partnerId = string.Empty;
|
|
if (_service == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
string bestPartner = string.Empty;
|
|
long bestTimestamp = long.MinValue;
|
|
|
|
foreach (SocialFriendEntry friend in _service.GetFriendsSnapshot())
|
|
{
|
|
string candidateId = friend != null ? friend.friend_steam_id : string.Empty;
|
|
if (string.IsNullOrWhiteSpace(candidateId))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
IReadOnlyList<ArenaRoomChatMessage> messages = _service.GetPrivateChatMessages(candidateId);
|
|
if (messages == null || messages.Count == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
ArenaRoomChatMessage lastMessage = messages[messages.Count - 1];
|
|
long timestamp = ParseMessageUnixTimestamp(lastMessage != null ? lastMessage.created_at : string.Empty);
|
|
if (timestamp > bestTimestamp)
|
|
{
|
|
bestTimestamp = timestamp;
|
|
bestPartner = candidateId;
|
|
}
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(bestPartner))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
partnerId = bestPartner;
|
|
return true;
|
|
}
|
|
|
|
private void NotifyActivePrivateConversationChanged()
|
|
{
|
|
string activePartnerId = isActiveAndEnabled && _currentMode == ChatChannelMode.Private
|
|
? _currentPrivatePartnerId ?? string.Empty
|
|
: string.Empty;
|
|
ActivePrivateConversationChanged?.Invoke(activePartnerId);
|
|
}
|
|
|
|
private int GetEffectiveCacheLimit()
|
|
{
|
|
return Mathf.Max(InitialHistoryLoadLimit, Mathf.Max(1, maxMessageHistory));
|
|
}
|
|
|
|
private int GetEffectiveDisplayWindow()
|
|
{
|
|
return Mathf.Clamp(messageDisplayWindow, 1, MaxDisplayWindowLimit);
|
|
}
|
|
|
|
private int GetRenderedWindowCapacity()
|
|
{
|
|
return Mathf.Max(GetEffectiveDisplayWindow(), Mathf.Min(GetEffectiveCacheLimit(), GetEffectiveDisplayWindow() * 3));
|
|
}
|
|
|
|
private IReadOnlyList<ArenaRoomChatMessage> GetActiveMessages()
|
|
{
|
|
if (_service == null)
|
|
{
|
|
return Array.Empty<ArenaRoomChatMessage>();
|
|
}
|
|
|
|
if (_currentMode == ChatChannelMode.Private && !string.IsNullOrWhiteSpace(_currentPrivatePartnerId))
|
|
{
|
|
return _service.GetPrivateChatMessages(_currentPrivatePartnerId);
|
|
}
|
|
|
|
return _service.GetWorldChatMessages();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|