push notifications

This commit is contained in:
2026-11-03 20:26:24 +05:30
parent a1b2b41912
commit 3ee348c09f
221 changed files with 9440 additions and 4899 deletions
+293 -9
View File
@@ -1,6 +1,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using DG.Tweening;
using TMPro;
using UnityEngine;
@@ -10,6 +11,8 @@ public class CustomerSupportCanvas : MonoBehaviour
{
const float ScreenFadeSeconds = 0.5f;
const float PollIntervalSeconds = 5f;
const float ChatLayoutRebuildDelay = 0.1f;
const int ChatWrapEveryChars = 25;
static readonly (string slug, string label)[] Categories =
{
@@ -42,6 +45,10 @@ public class CustomerSupportCanvas : MonoBehaviour
public GameObject noChatSelectedTxt;
public RectTransform ticketChatParent;
/// <summary>
/// ScrollRect that wraps <see cref="ticketChatParent"/>. If unset, resolved from the chat parent.
/// </summary>
public ScrollRect ticketChatScroll;
/// <summary>
/// Two children, first is left, second is right, only 1 TMP_Text inside each, which should be the message. enable left for customer support agent, enable right for player.
/// </summary>
public GameObject ticketChatItemPrefab;
@@ -63,15 +70,31 @@ public class CustomerSupportCanvas : MonoBehaviour
readonly List<Toggle> _reasonToggles = new List<Toggle>();
readonly List<string> _reasonSlugs = new List<string>();
public static CustomerSupportCanvas instance;
string _selectedTicketId;
string _selectedCategorySlug;
string _selectedCategoryLabel;
string _pushTicketId;
Coroutine _pollRoutine;
Coroutine _busyRoutine;
Coroutine _chatLayoutRoutine;
string _chatFingerprint;
bool _requestInFlight;
bool _reasonsBuilt;
ToggleGroup _reasonToggleGroup;
void Awake()
{
instance = this;
}
void OnDestroy()
{
if (instance == this)
instance = null;
}
void Start()
{
if (btnFaq != null)
@@ -91,16 +114,79 @@ public class CustomerSupportCanvas : MonoBehaviour
if (btnCancelTicket != null)
btnCancelTicket.onClick.AddListener(CloseNewTicketPanel);
SetCanvasGroupVisible(helpScreen, true, false);
SetCanvasGroupVisible(helpScreen, false, false);
SetCanvasGroupVisible(ticketScreen, false, false);
ClearChatSelection();
HideNewTicketPanel();
EnsureReasonToggles();
string pendingTicket = PushNotificationService.ConsumePendingTicket();
if (!string.IsNullOrEmpty(pendingTicket))
OpenFromNotification(pendingTicket);
}
/// <summary>
/// Opens the ticket named by a support push. A reply for the thread already on screen refreshes it immediately.
/// </summary>
public void OpenFromNotification(string ticketId)
{
if (string.IsNullOrEmpty(ticketId))
return;
bool ticketVisible = ticketScreen != null && ticketScreen.blocksRaycasts;
if (ticketVisible && _selectedTicketId == ticketId)
{
StartCoroutine(RefreshOpenThread(ticketId));
return;
}
if (_requestInFlight)
{
_pushTicketId = ticketId;
return;
}
RunExclusive(ShowTicketFromNotification(ticketId));
}
IEnumerator ShowTicketFromNotification(string ticketId)
{
HideNewTicketPanel();
SetCanvasGroupVisible(helpScreen, false, true);
SetCanvasGroupVisible(ticketScreen, true, true);
bool ok = false;
yield return EnsureSupportAuth(r => ok = r);
if (!ok)
yield break;
SupportCallResult<SupportTicketListResponse> list = null;
yield return SupportClient.ListTickets(r => list = r);
if (list != null && list.ok)
RebuildTicketList(list.data != null ? list.data.tickets : null);
if (_selectedTicketId == ticketId)
yield return RefreshOpenThread(ticketId);
else
SelectTicket(ticketId);
}
IEnumerator RefreshOpenThread(string ticketId)
{
SupportCallResult<SupportThread> result = null;
yield return SupportClient.GetThread(ticketId, r => result = r);
if (result == null || !result.ok)
yield break;
if (_selectedTicketId != ticketId)
yield break;
ApplyThread(result.data);
RefreshTicketPreviewFromThread(result.data);
}
void OnDisable()
{
StopPoll();
StopChatLayoutRoutine();
if (_busyRoutine != null)
{
StopCoroutine(_busyRoutine);
@@ -134,7 +220,6 @@ public class CustomerSupportCanvas : MonoBehaviour
HideNewTicketPanel();
SetCanvasGroupVisible(helpScreen, false, true);
SetCanvasGroupVisible(ticketScreen, false, true);
gameObject.SetActive(false);
}
void LaunchFAQ()
@@ -237,7 +322,7 @@ public class CustomerSupportCanvas : MonoBehaviour
yield break;
SupportCallResult<SupportThread> result = null;
yield return SupportClient.CreateTicket(category, body, r => result = r);
yield return SupportClient.CreateTicket(category, body, SupportClient.ReadUnityPlayerLog(), r => result = r);
if (result == null || !result.ok)
{
ShowError(result != null ? result.error : "Could not create ticket.");
@@ -273,8 +358,6 @@ public class CustomerSupportCanvas : MonoBehaviour
? LoginManager.SessionUsername
: (LoginManager.CurrentUser != null ? LoginManager.CurrentUser.username : null);
string password = LoginManager.SessionPassword;
Debug.Log("[CustomerSupport] support login as user='" + (username ?? "") +
"' passwordLength=" + (password != null ? password.Length : 0));
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
{
ShowError("Please sign in again to use customer support.");
@@ -345,9 +428,15 @@ public class CustomerSupportCanvas : MonoBehaviour
void SetChatInputVisible(bool visible)
{
if (inputField != null)
{
inputField.gameObject.SetActive(visible);
inputField.interactable = visible && !_requestInFlight;
}
if (btnSend != null)
{
btnSend.gameObject.SetActive(visible);
btnSend.interactable = visible && !_requestInFlight && !string.IsNullOrEmpty(_selectedTicketId);
}
}
void RebuildTicketList(SupportTicket[] tickets)
@@ -385,7 +474,12 @@ public class CustomerSupportCanvas : MonoBehaviour
TMP_Text label = item.GetComponentInChildren<TMP_Text>(true);
if (label != null)
label.text = "Ticket #" + (i + 1).ToString("000");
{
string title = "Ticket #" + (i + 1).ToString("000");
if (!string.IsNullOrEmpty(ticket.categoryLabel))
title += "\n" + ticket.categoryLabel;
label.text = title;
}
Image image = item.GetComponent<Image>();
if (image == null)
@@ -419,7 +513,13 @@ public class CustomerSupportCanvas : MonoBehaviour
if (thread == null)
return;
ClearChatMessages();
string fingerprint = BuildChatFingerprint(thread);
if (fingerprint == _chatFingerprint && ticketChatParent != null && ticketChatParent.childCount > 0)
return;
StopChatLayoutRoutine();
ClearChildren(ticketChatParent);
_chatFingerprint = fingerprint;
if (ticketChatItemPrefab == null || ticketChatParent == null || thread.messages == null)
return;
@@ -446,16 +546,190 @@ public class CustomerSupportCanvas : MonoBehaviour
{
TMP_Text text = activeSide.GetComponentInChildren<TMP_Text>(true);
if (text != null)
text.text = message.body ?? "";
text.text = FormatChatBody(message.body);
}
}
ScheduleChatLayoutRebuildAndScroll();
}
void ClearChatMessages()
{
StopChatLayoutRoutine();
_chatFingerprint = null;
ClearChildren(ticketChatParent);
}
void ScheduleChatLayoutRebuildAndScroll()
{
StopChatLayoutRoutine();
if (!isActiveAndEnabled || ticketChatParent == null)
return;
_chatLayoutRoutine = StartCoroutine(ChatLayoutRebuildAndScrollCoroutine());
}
IEnumerator ChatLayoutRebuildAndScrollCoroutine()
{
yield return new WaitForSecondsRealtime(ChatLayoutRebuildDelay);
if (ticketChatParent == null)
yield break;
RebuildChatItemLayouts();
AppendTrailingSpaceToChatItems();
RebuildChatItemLayouts();
ScrollChatToBottom();
_chatLayoutRoutine = null;
}
void RebuildChatItemLayouts()
{
if (ticketChatParent == null)
return;
for (int i = 0; i < ticketChatParent.childCount; i++)
{
var item = ticketChatParent.GetChild(i) as RectTransform;
if (item == null)
continue;
LayoutRebuilder.ForceRebuildLayoutImmediate(item);
}
LayoutRebuilder.ForceRebuildLayoutImmediate(ticketChatParent);
}
void AppendTrailingSpaceToChatItems()
{
if (ticketChatParent == null)
return;
for (int i = 0; i < ticketChatParent.childCount; i++)
{
Transform item = ticketChatParent.GetChild(i);
TMP_Text text = FindActiveChatText(item);
if (text == null)
continue;
if (!text.text.EndsWith(" ", StringComparison.Ordinal))
text.text += " ";
}
}
static TMP_Text FindActiveChatText(Transform item)
{
if (item == null)
return null;
for (int i = 0; i < item.childCount; i++)
{
Transform side = item.GetChild(i);
if (side == null || !side.gameObject.activeSelf)
continue;
TMP_Text text = side.GetComponentInChildren<TMP_Text>(true);
if (text != null)
return text;
}
return item.GetComponentInChildren<TMP_Text>(true);
}
static string FormatChatBody(string body)
{
if (string.IsNullOrEmpty(body))
return "";
var sb = new StringBuilder(body.Length + body.Length / ChatWrapEveryChars);
int col = 0;
int i = 0;
while (i < body.Length)
{
char c = body[i];
if (c == '\n')
{
sb.Append('\n');
col = 0;
i++;
continue;
}
if (char.IsWhiteSpace(c))
{
while (i < body.Length && char.IsWhiteSpace(body[i]) && body[i] != '\n')
i++;
continue;
}
int start = i;
while (i < body.Length && !char.IsWhiteSpace(body[i]) && body[i] != '\n')
i++;
int wordLen = i - start;
if (col > 0 && col + 1 + wordLen > ChatWrapEveryChars)
{
sb.Append('\n');
col = 0;
}
else if (col > 0)
{
sb.Append(' ');
col++;
}
if (wordLen > ChatWrapEveryChars)
{
for (int w = 0; w < wordLen; w++)
{
if (col >= ChatWrapEveryChars)
{
sb.Append('\n');
col = 0;
}
sb.Append(body[start + w]);
col++;
}
}
else
{
sb.Append(body, start, wordLen);
col += wordLen;
}
}
return sb.ToString();
}
void ScrollChatToBottom()
{
ScrollRect scroll = ticketChatScroll;
if (scroll == null && ticketChatParent != null)
scroll = ticketChatParent.GetComponentInParent<ScrollRect>();
if (scroll == null)
return;
Canvas.ForceUpdateCanvases();
scroll.verticalNormalizedPosition = 0f;
if (scroll.content != null)
LayoutRebuilder.ForceRebuildLayoutImmediate(scroll.content);
Canvas.ForceUpdateCanvases();
scroll.verticalNormalizedPosition = 0f;
}
void StopChatLayoutRoutine()
{
if (_chatLayoutRoutine == null)
return;
StopCoroutine(_chatLayoutRoutine);
_chatLayoutRoutine = null;
}
static string BuildChatFingerprint(SupportThread thread)
{
if (thread == null || thread.messages == null || thread.messages.Length == 0)
return "0";
SupportMessage last = thread.messages[thread.messages.Length - 1];
string lastId = last != null ? last.id : "";
return thread.messages.Length + ":" + lastId;
}
void RefreshTicketPreviewFromThread(SupportThread thread)
{
if (thread == null || thread.ticket == null || string.IsNullOrEmpty(thread.ticket.id))
@@ -596,13 +870,23 @@ public class CustomerSupportCanvas : MonoBehaviour
_requestInFlight = false;
_busyRoutine = null;
SetBusyUi(false);
if (!string.IsNullOrEmpty(_pushTicketId))
{
string ticketId = _pushTicketId;
_pushTicketId = null;
if (isActiveAndEnabled)
OpenFromNotification(ticketId);
}
}
}
void SetBusyUi(bool busy)
{
bool chatReady = !busy && !string.IsNullOrEmpty(_selectedTicketId);
if (btnSend != null)
btnSend.interactable = !busy && !string.IsNullOrEmpty(_selectedTicketId);
btnSend.interactable = chatReady;
if (inputField != null)
inputField.interactable = chatReady;
if (newTicketBtn != null)
newTicketBtn.interactable = !busy;
if (btnTickets != null)