push notifications
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user