Added typing indicators

This commit is contained in:
Marcel Baumgartner 2023-02-23 11:39:58 +01:00
parent 60693d25da
commit d02781b13a
6 changed files with 233 additions and 6 deletions

View file

@ -11,6 +11,9 @@ public class SupportAdminService
public EventHandler<SupportMessage> OnNewMessage;
public EventHandler OnUpdateTyping;
private List<string> TypingUsers = new();
private User Self;
private User Recipient;
@ -38,6 +41,48 @@ public class SupportAdminService
return Task.CompletedTask;
});
MessageService.Subscribe<SupportClientService, User>(
$"support.{Self.Id}.typing",
this,
user =>
{
HandleTyping(user);
return Task.CompletedTask;
});
}
private void HandleTyping(User user)
{
var name = $"{user.FirstName} {user.LastName}";
lock (TypingUsers)
{
if (!TypingUsers.Contains(name))
{
TypingUsers.Add(name);
OnUpdateTyping!.Invoke(this, null!);
Task.Run(async () =>
{
await Task.Delay(TimeSpan.FromSeconds(5));
if (TypingUsers.Contains(name))
{
TypingUsers.Remove(name);
OnUpdateTyping!.Invoke(this, null!);
}
});
}
}
}
public string[] GetTypingUsers()
{
lock (TypingUsers)
{
return TypingUsers.ToArray();
}
}
public async Task<SupportMessage[]> GetMessages()
@ -65,8 +110,19 @@ public class SupportAdminService
await SupportServerService.Close(Recipient);
}
public Task TriggerTyping()
{
Task.Run(async () =>
{
await MessageService.Emit($"support.{Recipient.Id}.admintyping", Self);
});
return Task.CompletedTask;
}
public void Dispose()
{
MessageService.Unsubscribe($"support.{Recipient.Id}.message", this);
MessageService.Unsubscribe($"support.{Recipient.Id}.typing", this);
}
}

View file

@ -11,6 +11,9 @@ public class SupportClientService : IDisposable
public EventHandler<SupportMessage> OnNewMessage;
public EventHandler OnUpdateTyping;
private List<string> TypingUsers = new();
private User Self;
public SupportClientService(
@ -36,6 +39,48 @@ public class SupportClientService : IDisposable
return Task.CompletedTask;
});
MessageService.Subscribe<SupportClientService, User>(
$"support.{Self.Id}.admintyping",
this,
user =>
{
HandleTyping(user);
return Task.CompletedTask;
});
}
private void HandleTyping(User user)
{
var name = $"{user.FirstName} {user.LastName}";
lock (TypingUsers)
{
if (!TypingUsers.Contains(name))
{
TypingUsers.Add(name);
OnUpdateTyping!.Invoke(this, null!);
Task.Run(async () =>
{
await Task.Delay(TimeSpan.FromSeconds(5));
if (TypingUsers.Contains(name))
{
TypingUsers.Remove(name);
OnUpdateTyping!.Invoke(this, null!);
}
});
}
}
}
public string[] GetTypingUsers()
{
lock (TypingUsers)
{
return TypingUsers.ToArray();
}
}
public async Task<SupportMessage[]> GetMessages()
@ -57,8 +102,19 @@ public class SupportClientService : IDisposable
);
}
public Task TriggerTyping()
{
Task.Run(async () =>
{
await MessageService.Emit($"support.{Self.Id}.typing", Self);
});
return Task.CompletedTask;
}
public void Dispose()
{
MessageService.Unsubscribe($"support.{Self.Id}.message", this);
MessageService.Unsubscribe($"support.{Self.Id}.admintyping", this);
}
}

View file

@ -92,7 +92,33 @@
</LazyLoader>
</div>
<div class="card-footer">
<textarea @bind="Content" class="form-control form-control-flush mb-3" rows="1" placeholder="Type a message">
@{
var typingUsers = SupportAdminService.GetTypingUsers();
}
@if (typingUsers.Any())
{
<span class="mb-5 fs-5 d-flex flex-row">
<div class="wave me-3">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>
@if (typingUsers.Length > 1)
{
<span>
@(typingUsers.Aggregate((current, next) => current + ", " + next)) <TL>are typing</TL>
</span>
}
else
{
<span>
@(typingUsers.First()) <TL>is typing</TL>
</span>
}
</span>
}
<textarea @bind="Content" @oninput="OnTyping" class="form-control mb-3" rows="1" placeholder="Type a message">
</textarea>
<div class="d-flex flex-stack">
<div class="d-flex align-items-center me-2">
@ -156,6 +182,8 @@
private SupportMessage[] Messages;
private string Content = "";
private DateTime LastTypingTimestamp = DateTime.UtcNow.AddMinutes(-10);
private async Task Load(LazyLoader arg)
{
User = UserRepository
@ -165,11 +193,17 @@
if (User != null)
{
SupportAdminService.OnNewMessage += OnNewMessage;
SupportAdminService.OnUpdateTyping += OnUpdateTyping;
await SupportAdminService.Start(User);
}
}
private async void OnUpdateTyping(object? sender, EventArgs e)
{
await InvokeAsync(StateHasChanged);
}
private async void OnNewMessage(object? sender, SupportMessage e)
{
Messages = (await SupportAdminService.GetMessages()).Reverse().ToArray();
@ -191,4 +225,14 @@
{
await SupportAdminService.Close();
}
private async Task OnTyping()
{
if ((DateTime.UtcNow - LastTypingTimestamp).TotalSeconds > 5)
{
LastTypingTimestamp = DateTime.UtcNow;
await SupportAdminService.TriggerTyping();
}
}
}

View file

@ -99,7 +99,30 @@
</LazyLoader>
</div>
<div class="card-footer">
<textarea @bind="Content" class="form-control form-control-flush mb-3" rows="1" placeholder="Type a message">
@{
var typingUsers = SupportClientService.GetTypingUsers();
}
@if (typingUsers.Any())
{
<span class="mb-5 fs-5 d-flex flex-row">
<div class="wave me-3">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>
@if (typingUsers.Length > 1)
{
<span>@(typingUsers.Aggregate((current, next) => current + ", " + next)) <TL>are typing</TL></span>
}
else
{
<span>@(typingUsers.First()) <TL>is typing</TL></span>
}
</span>
}
<textarea @bind="Content" @oninput="OnTyping" class="form-control mb-3" rows="1" placeholder="Type a message">
</textarea>
<div class="d-flex flex-stack">
<div class="d-flex align-items-center me-2">
@ -125,6 +148,8 @@
private SupportMessage[] Messages;
private string Content = "";
private DateTime LastTypingTimestamp = DateTime.UtcNow.AddMinutes(-10);
private async Task Load(LazyLoader lazyLoader)
{
User = (await IdentityService.Get())!;
@ -132,10 +157,16 @@
await lazyLoader.SetText("Starting chat client");
SupportClientService.OnNewMessage += OnNewMessage;
SupportClientService.OnUpdateTyping += OnUpdateTyping;
await SupportClientService.Start();
}
private async void OnUpdateTyping(object? sender, EventArgs e)
{
await InvokeAsync(StateHasChanged);
}
private async void OnNewMessage(object? sender, SupportMessage e)
{
Messages = (await SupportClientService.GetMessages()).Reverse().ToArray();
@ -153,4 +184,14 @@
{
Messages = (await SupportClientService.GetMessages()).Reverse().ToArray();
}
private async void OnTyping()
{
if ((DateTime.UtcNow - LastTypingTimestamp).TotalSeconds > 5)
{
LastTypingTimestamp = DateTime.UtcNow;
await SupportClientService.TriggerTyping();
}
}
}

View file

@ -224,3 +224,6 @@ Close ticket;Close ticket
Closing;Closing
The support team has been notified. Please be patient;The support team has been notified. Please be patient
The ticket is now closed. Type a message to open it again;The ticket is now closed. Type a message to open it again
1 day ago;1 day ago
is typing;is typing
are typing;are typing

View file

@ -15,3 +15,30 @@
.blur-unless-hover:hover {
filter: none;
}
div.wave {
}
div.wave .dot {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 3px;
background-color: var(--bs-body-color);
animation: wave 1.3s linear infinite;
}
div.wave .dot:nth-child(2) {
animation-delay: -1.1s;
}
div.wave .dot:nth-child(3) {
animation-delay: -0.9s;
}
@keyframes wave {
0%, 60%, 100% {
transform: initial;
}
30% {
transform: translateY(-15px);
}
}