Added support chat sync. Added admin support chat pages

This commit is contained in:
Marcel Baumgartner 2023-02-22 13:55:59 +01:00
parent 0b6882d3f8
commit 8f3f9fa1fb
11 changed files with 508 additions and 93 deletions

1
.gitignore vendored
View file

@ -443,3 +443,4 @@ Moonlight/obj/project.assets.json
Moonlight/obj/project.nuget.cache Moonlight/obj/project.nuget.cache
Moonlight/obj/project.packagespec.json Moonlight/obj/project.packagespec.json
Moonlight/obj/Debug/net6.0/Moonlight.GeneratedMSBuildEditorConfig.editorconfig Moonlight/obj/Debug/net6.0/Moonlight.GeneratedMSBuildEditorConfig.editorconfig
*.editorconfig

View file

@ -1,8 +1,4 @@
using System.Security.Cryptography; using Logging.Net;
using System.Text;
using JWT.Algorithms;
using JWT.Builder;
using Logging.Net;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Moonlight.App.Database; using Moonlight.App.Database;
using Moonlight.App.Database.Entities; using Moonlight.App.Database.Entities;

View file

@ -3,7 +3,7 @@ using Moonlight.App.Services.Sessions;
namespace Moonlight.App.Services.Support; namespace Moonlight.App.Services.Support;
public class SupportAdminServer public class SupportAdminService
{ {
private readonly SupportServerService SupportServerService; private readonly SupportServerService SupportServerService;
private readonly IdentityService IdentityService; private readonly IdentityService IdentityService;
@ -14,7 +14,7 @@ public class SupportAdminServer
private User Self; private User Self;
private User Recipient; private User Recipient;
public SupportAdminServer( public SupportAdminService(
SupportServerService supportServerService, SupportServerService supportServerService,
IdentityService identityService, IdentityService identityService,
MessageService messageService) MessageService messageService)
@ -60,6 +60,11 @@ public class SupportAdminServer
); );
} }
public async Task Close()
{
await SupportServerService.Close(Recipient);
}
public void Dispose() public void Dispose()
{ {
MessageService.Unsubscribe($"support.{Recipient.Id}.message", this); MessageService.Unsubscribe($"support.{Recipient.Id}.message", this);

View file

@ -27,38 +27,72 @@ public class SupportServerService : IDisposable
Task.Run(async () => Task.Run(async () =>
{ {
message.CreatedAt = DateTime.UtcNow; try
message.Sender = sender;
message.Recipient = recipient;
message.IsSupport = isSupport;
SupportMessageRepository.Add(message);
await MessageService.Emit($"support.{recipient.Id}.message", message);
if (!recipient.SupportPending)
{ {
recipient.SupportPending = true; message.CreatedAt = DateTime.UtcNow;
UserRepository.Update(recipient); message.Sender = sender;
message.Recipient = recipient;
message.IsSupport = isSupport;
var systemMessage = new SupportMessage() SupportMessageRepository.Add(message);
await MessageService.Emit($"support.{recipient.Id}.message", message);
if (!recipient.SupportPending)
{ {
Recipient = recipient, recipient.SupportPending = true;
Sender = null, UserRepository.Update(recipient);
IsSystem = true,
Message = "The support team has been notified. Please be patient"
};
SupportMessageRepository.Add(systemMessage); if (!message.IsSupport)
{
var systemMessage = new SupportMessage()
{
Recipient = recipient,
Sender = null,
IsSystem = true,
Message = "The support team has been notified. Please be patient"
};
await MessageService.Emit($"support.{recipient.Id}.message", systemMessage); SupportMessageRepository.Add(systemMessage);
Logger.Info("Support ticket created: " + recipient.Id); await MessageService.Emit($"support.{recipient.Id}.message", systemMessage);
//TODO: Ping or so }
await MessageService.Emit($"support.new", recipient);
Logger.Info("Support ticket created: " + recipient.Id);
//TODO: Ping or so
}
}
catch (Exception e)
{
Logger.Error("Error sending message");
Logger.Error(e);
} }
}); });
} }
public async Task Close(User user)
{
var recipient = UserRepository.Get().First(x => x.Id == user.Id);
recipient.SupportPending = false;
UserRepository.Update(recipient);
var systemMessage = new SupportMessage()
{
Recipient = recipient,
Sender = null,
IsSystem = true,
Message = "The ticket is now closed. Type a message to open it again"
};
SupportMessageRepository.Add(systemMessage);
await MessageService.Emit($"support.{recipient.Id}.message", systemMessage);
await MessageService.Emit($"support.close", recipient);
}
public Task<SupportMessage[]> GetMessages(User r) public Task<SupportMessage[]> GetMessages(User r)
{ {
var recipient = UserRepository.Get().First(x => x.Id == r.Id); var recipient = UserRepository.Get().First(x => x.Id == r.Id);

View file

@ -60,7 +60,6 @@
<Folder Include="App\Http\Middleware" /> <Folder Include="App\Http\Middleware" />
<Folder Include="App\Models\AuditLogData" /> <Folder Include="App\Models\AuditLogData" />
<Folder Include="resources\lang" /> <Folder Include="resources\lang" />
<Folder Include="Shared\Views\Admin\Support" />
<Folder Include="wwwroot\assets\media" /> <Folder Include="wwwroot\assets\media" />
</ItemGroup> </ItemGroup>

View file

@ -62,7 +62,7 @@ namespace Moonlight
// Support // Support
builder.Services.AddSingleton<SupportServerService>(); builder.Services.AddSingleton<SupportServerService>();
builder.Services.AddScoped<SupportAdminServer>(); builder.Services.AddScoped<SupportAdminService>();
builder.Services.AddScoped<SupportClientService>(); builder.Services.AddScoped<SupportClientService>();
// Helpers // Helpers

View file

@ -0,0 +1,157 @@
@page "/admin/support"
@using Moonlight.App.Repositories
@using Moonlight.App.Database.Entities
@using Microsoft.EntityFrameworkCore
@using Moonlight.App.Database
@using Moonlight.App.Services
@inject SupportMessageRepository SupportMessageRepository
@inject ConfigService ConfigService
@inject MessageService MessageService
@implements IDisposable
<OnlyAdmin>
<LazyLoader @ref="LazyLoader" Load="Load">
<div class="card">
<div class="card-body">
<div class="d-flex flex-column flex-xl-row p-7">
<div class="flex-lg-row-fluid me-xl-15 mb-20 mb-xl-0">
<div class="mb-0">
<h1 class="text-dark mb-10">
<TL>Open tickets</TL>
</h1>
<div class="mb-10">
@if (Users.Any())
{
foreach (var user in Users)
{
<div class="d-flex mb-10">
<span class="svg-icon svg-icon-2x me-5 ms-n1 mt-2 svg-icon-success">
<i class="text-primary bx bx-md bx-message-dots"></i>
</span>
<div class="d-flex flex-column">
<div class="d-flex align-items-center mb-2">
<a href="/admin/support/view/@(user.Id)" class="text-dark text-hover-primary fs-4 me-3 fw-semibold">
@(user.FirstName) @(user.LastName)
</a>
</div>
<span class="text-muted fw-semibold fs-6">
@{
var lastMessage = MessageCache.ContainsKey(user) ? MessageCache[user] : null;
}
@if (lastMessage == null)
{
<TL>No message sent yet</TL>
}
else
{
@(lastMessage.Message)
}
</span>
</div>
</div>
}
}
else
{
<div class="alert alert-info">
<TL>No support ticket is currently open</TL>
</div>
}
</div>
</div>
</div>
<div class="flex-column flex-lg-row-auto w-100 mw-lg-300px mw-xxl-350px">
<div class="card-rounded bg-primary bg-opacity-5 p-10 mb-15">
<h2 class="text-dark fw-bold mb-11">
<TL>Actions</TL>
</h2>
<div class="d-flex align-items-center mb-10">
<!--begin::Icon-->
<i class="bi bi-file-earmark-text text-primary fs-1 me-5"></i>
<!--end::SymIconbol-->
<!--begin::Info-->
<div class="d-flex flex-column">
<h5 class="text-gray-800 fw-bold">Project Briefing</h5>
<!--begin::Section-->
<div class="fw-semibold">
<!--begin::Desc-->
<span class="text-muted">Check out our</span>
<!--end::Desc-->
<!--begin::Link-->
<a href="#" class="link-primary">Support Policy</a>
<!--end::Link-->
</div>
<!--end::Section-->
</div>
<!--end::Info-->
</div>
</div>
</div>
</div>
</div>
</div>
</LazyLoader>
</OnlyAdmin>
@code
{
private User[] Users;
private Dictionary<User, SupportMessage?> MessageCache;
private LazyLoader? LazyLoader;
protected override void OnInitialized()
{
MessageCache = new();
MessageService.Subscribe<Index, User>("support.new", this, async user =>
{
if (LazyLoader != null)
await LazyLoader.Reload();
});
MessageService.Subscribe<Index, User>("support.close", this, async user =>
{
if (LazyLoader != null)
await LazyLoader.Reload();
});
}
private Task Load(LazyLoader arg)
{
// We dont want cache here
Users = (new UserRepository(new DataContext(ConfigService)))
.Get()
.Where(x => x.SupportPending)
.ToArray();
foreach (var user in Users)
{
var lastMessage = SupportMessageRepository
.Get()
.Include(x => x.Recipient)
.OrderByDescending(x => x.Id)
.FirstOrDefault(x => x.Recipient!.Id == user.Id);
MessageCache.Add(user, lastMessage);
}
return Task.CompletedTask;
}
public void Dispose()
{
MessageService.Unsubscribe("support.new", this);
MessageService.Unsubscribe("support.close", this);
}
}

View file

@ -0,0 +1,194 @@
@page "/admin/support/view/{Id:int}"
@using Moonlight.App.Services.Support
@using Moonlight.App.Database.Entities
@using Moonlight.App.Helpers
@using Moonlight.App.Repositories
@using Moonlight.App.Services
@inject SupportAdminService SupportAdminService
@inject UserRepository UserRepository
@inject SmartTranslateService SmartTranslateService
@inject ResourceService ResourceService
<OnlyAdmin>
<LazyLoader Load="Load">
@if (User == null)
{
<div class="alert alert-danger">
<TL>User not found</TL>
</div>
}
else
{
<div class="row">
<div class="d-flex flex-column flex-xl-row p-7">
<div class="flex-lg-row-fluid me-xl-15 mb-20 mb-xl-0">
<div class="card">
<div class="card-body">
<LazyLoader Load="LoadMessages">
<div class="scroll-y me-n5 pe-5" style="max-height: 65vh; display: flex; flex-direction: column-reverse;">
@foreach (var message in Messages)
{
if (message.IsSystem || message.IsSupport)
{
<div class="d-flex justify-content-end mb-10 ">
<div class="d-flex flex-column align-items-end">
<div class="d-flex align-items-center mb-2">
<div class="me-3">
<span class="text-muted fs-7 mb-1">@(Formatter.FormatAgoFromDateTime(message.CreatedAt, SmartTranslateService))</span>
<a class="fs-5 fw-bold text-gray-900 text-hover-primary ms-1">
@if (message.IsSupport && !message.IsSystem)
{
<span>@(message.Sender!.FirstName) @(message.Sender!.LastName)</span>
}
else
{
<span>System</span>
}
</a>
</div>
<div class="symbol symbol-35px symbol-circle ">
<img alt="Logo" src="@(ResourceService.Image("logo.svg"))">
</div>
</div>
<div class="p-5 rounded bg-light-primary text-dark fw-semibold mw-lg-400px text-end">
@if (message.IsSystem)
{
<TL>@(message.Message)</TL>
}
else
{
@(message.Message)
}
</div>
</div>
</div>
}
else
{
<div class="d-flex justify-content-start mb-10 ">
<div class="d-flex flex-column align-items-start">
<div class="d-flex align-items-center mb-2">
<div class="symbol symbol-35px symbol-circle ">
<img alt="Avatar" src="@(ResourceService.Avatar(message.Sender))">
</div>
<div class="ms-3">
<a class="fs-5 fw-bold text-gray-900 text-hover-primary me-1">
<span>@(message.Sender!.FirstName) @(message.Sender!.LastName)</span>
</a>
<span class="text-muted fs-7 mb-1">@(Formatter.FormatAgoFromDateTime(message.CreatedAt, SmartTranslateService))</span>
</div>
</div>
<div class="p-5 rounded bg-light-info text-dark fw-semibold mw-lg-400px text-start">
@(message.Message)
</div>
</div>
</div>
}
}
</div>
</LazyLoader>
</div>
<div class="card-footer">
<textarea @bind="Content" class="form-control form-control-flush mb-3" rows="1" placeholder="Type a message">
</textarea>
<div class="d-flex flex-stack">
<div class="d-flex align-items-center me-2">
<button class="btn btn-sm btn-icon btn-active-light-primary me-1" type="button">
<i class="bx bx-upload fs-3"></i>
</button>
</div>
<WButton Text="@(SmartTranslateService.Translate("Send"))"
WorkingText="@(SmartTranslateService.Translate("Sending"))"
CssClasses="btn-primary"
OnClick="Send">
</WButton>
</div>
</div>
</div>
</div>
<div class="flex-column flex-lg-row-auto w-100 mw-lg-300px mw-xxl-350px">
<div class="card p-10 mb-15">
<h2 class="text-dark fw-bold mb-11">
<TL>User information</TL>
</h2>
<div class="d-flex align-items-center mb-6">
<spna class="fw-semibold text-gray-800 fs-5 m-0">
<TL>Firstname</TL>: @(User.FirstName)
</spna>
</div>
<div class="d-flex align-items-center mb-6">
<spna class="fw-semibold text-gray-800 fs-5 m-0">
<TL>Lastname</TL>: @(User.LastName)
</spna>
</div>
<div class="d-flex align-items-center mb-6">
<spna class="fw-semibold text-gray-800 fs-5 m-0">
<TL>Email</TL>: @(User.Email)
</spna>
</div>
<div class="d-flex align-items-center mb-6">
<spna class="fw-semibold text-gray-800 fs-5 m-0">
<WButton Text="@(SmartTranslateService.Translate("Close ticket"))"
WorkingText="@(SmartTranslateService.Translate("Closing"))"
CssClasses="btn-danger"
OnClick="CloseTicket">
</WButton>
</spna>
</div>
</div>
</div>
</div>
</div>
}
</LazyLoader>
</OnlyAdmin>
@code
{
[Parameter]
public int Id { get; set; }
private User? User;
private SupportMessage[] Messages;
private string Content = "";
private async Task Load(LazyLoader arg)
{
User = UserRepository
.Get()
.FirstOrDefault(x => x.Id == Id);
if (User != null)
{
SupportAdminService.OnNewMessage += OnNewMessage;
await SupportAdminService.Start(User);
}
}
private async void OnNewMessage(object? sender, SupportMessage e)
{
Messages = (await SupportAdminService.GetMessages()).Reverse().ToArray();
await InvokeAsync(StateHasChanged);
}
private async Task LoadMessages(LazyLoader arg)
{
Messages = (await SupportAdminService.GetMessages()).Reverse().ToArray();
}
private async Task Send()
{
await SupportAdminService.SendMessage(Content);
Content = "";
}
private async Task CloseTicket()
{
await SupportAdminService.Close();
}
}

View file

@ -14,80 +14,89 @@
<div class="row"> <div class="row">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<div class="scroll-y me-n5 pe-5" style="max-height: 65vh; display: flex; flex-direction: column-reverse;"> <LazyLoader Load="LoadMessages">
@foreach (var message in Messages) <div class="scroll-y me-n5 pe-5" style="max-height: 65vh; display: flex; flex-direction: column-reverse;">
{ @foreach (var message in Messages)
if (message.IsSystem || message.IsSupport)
{ {
<div class="d-flex justify-content-start mb-10 "> if (message.IsSystem || message.IsSupport)
<div class="d-flex flex-column align-items-start"> {
<div class="d-flex align-items-center mb-2"> <div class="d-flex justify-content-start mb-10 ">
<div class="symbol symbol-35px symbol-circle "> <div class="d-flex flex-column align-items-start">
<img alt="Logo" src="@(ResourceService.Image("logo.svg"))"> <div class="d-flex align-items-center mb-2">
<div class="symbol symbol-35px symbol-circle ">
<img alt="Logo" src="@(ResourceService.Image("logo.svg"))">
</div>
<div class="ms-3">
<a class="fs-5 fw-bold text-gray-900 text-hover-primary me-1">
@if (message.IsSupport && !message.IsSystem)
{
<span>@(message.Sender!.FirstName) @(message.Sender!.LastName)</span>
}
else
{
<span>System</span>
}
</a>
<span class="text-muted fs-7 mb-1">@(Formatter.FormatAgoFromDateTime(message.CreatedAt, SmartTranslateService))</span>
</div>
</div> </div>
<div class="ms-3">
<a class="fs-5 fw-bold text-gray-900 text-hover-primary me-1"> <div class="p-5 rounded bg-light-info text-dark fw-semibold mw-lg-400px text-start">
@if (message.IsSupport && !message.IsSystem) @if (message.IsSystem)
{ {
<TL>@(message.Message)</TL>
}
else
{
@(message.Message)
}
</div>
</div>
</div>
}
else
{
<div class="d-flex justify-content-end mb-10 ">
<div class="d-flex flex-column align-items-end">
<div class="d-flex align-items-center mb-2">
<div class="me-3">
<span class="text-muted fs-7 mb-1">@(Formatter.FormatAgoFromDateTime(message.CreatedAt, SmartTranslateService))</span>
<a class="fs-5 fw-bold text-gray-900 text-hover-primary ms-1">
<span>@(message.Sender!.FirstName) @(message.Sender!.LastName)</span> <span>@(message.Sender!.FirstName) @(message.Sender!.LastName)</span>
} </a>
else </div>
{ <div class="symbol symbol-35px symbol-circle ">
<span>System</span> <img alt="Avatar" src="@(ResourceService.Avatar(message.Sender))">
} </div>
</a> </div>
<span class="text-muted fs-7 mb-1">@(Formatter.FormatAgoFromDateTime(message.CreatedAt, SmartTranslateService))</span>
<div class="p-5 rounded bg-light-primary text-dark fw-semibold mw-lg-400px text-end">
@(message.Message)
</div> </div>
</div> </div>
<div class="p-5 rounded bg-light-info text-dark fw-semibold mw-lg-400px text-start">
@(message.Message)
</div>
</div> </div>
</div> }
} }
else <div class="d-flex justify-content-start mb-10 ">
{ <div class="d-flex flex-column align-items-start">
<div class="d-flex justify-content-end mb-10 "> <div class="d-flex align-items-center mb-2">
<div class="d-flex flex-column align-items-end"> <div class="symbol symbol-35px symbol-circle ">
<div class="d-flex align-items-center mb-2"> <img alt="Logo" src="@(ResourceService.Image("logo.svg"))">
<div class="me-3">
<span class="text-muted fs-7 mb-1">@(Formatter.FormatAgoFromDateTime(message.CreatedAt, SmartTranslateService))</span>
<a class="fs-5 fw-bold text-gray-900 text-hover-primary ms-1">
<span>@(message.Sender!.FirstName) @(message.Sender!.LastName)</span>
</a>
</div>
<div class="symbol symbol-35px symbol-circle ">
<img alt="Avatar" src="@(ResourceService.Avatar(message.Sender))">
</div>
</div> </div>
<div class="ms-3">
<div class="p-5 rounded bg-light-primary text-dark fw-semibold mw-lg-400px text-end"> <a class="fs-5 fw-bold text-gray-900 text-hover-primary me-1">
@(message.Message) <span>System</span>
</a>
</div> </div>
</div> </div>
</div>
}
}
<div class="d-flex justify-content-start mb-10 ">
<div class="d-flex flex-column align-items-start">
<div class="d-flex align-items-center mb-2">
<div class="symbol symbol-35px symbol-circle ">
<img alt="Logo" src="@(ResourceService.Image("logo.svg"))">
</div>
<div class="ms-3">
<a class="fs-5 fw-bold text-gray-900 text-hover-primary me-1">
<span>System</span>
</a>
</div>
</div>
<div class="p-5 rounded bg-light-info text-dark fw-semibold mw-lg-400px text-start"> <div class="p-5 rounded bg-light-info text-dark fw-semibold mw-lg-400px text-start">
<TL>Welcome to the support chat. Ask your question here and we will help you</TL> <TL>Welcome to the support chat. Ask your question here and we will help you</TL>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </LazyLoader>
</div> </div>
<div class="card-footer"> <div class="card-footer">
<textarea @bind="Content" class="form-control form-control-flush mb-3" rows="1" placeholder="Type a message"> <textarea @bind="Content" class="form-control form-control-flush mb-3" rows="1" placeholder="Type a message">
@ -125,8 +134,6 @@
SupportClientService.OnNewMessage += OnNewMessage; SupportClientService.OnNewMessage += OnNewMessage;
await SupportClientService.Start(); await SupportClientService.Start();
Messages = (await SupportClientService.GetMessages()).Reverse().ToArray();
} }
private async void OnNewMessage(object? sender, SupportMessage e) private async void OnNewMessage(object? sender, SupportMessage e)
@ -141,4 +148,9 @@
Content = ""; Content = "";
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
} }
private async Task LoadMessages(LazyLoader arg)
{
Messages = (await SupportClientService.GetMessages()).Reverse().ToArray();
}
} }

View file

@ -199,6 +199,14 @@ build_metadata.AdditionalFiles.CssScope =
build_metadata.AdditionalFiles.TargetPath = U2hhcmVkXFZpZXdzXEFkbWluXFNlcnZlcnNcTmV3LnJhem9y build_metadata.AdditionalFiles.TargetPath = U2hhcmVkXFZpZXdzXEFkbWluXFNlcnZlcnNcTmV3LnJhem9y
build_metadata.AdditionalFiles.CssScope = build_metadata.AdditionalFiles.CssScope =
[C:/Users/marce/source/repos/MoonlightPublic/Moonlight/Moonlight/Shared/Views/Admin/Support/Index.razor]
build_metadata.AdditionalFiles.TargetPath = U2hhcmVkXFZpZXdzXEFkbWluXFN1cHBvcnRcSW5kZXgucmF6b3I=
build_metadata.AdditionalFiles.CssScope =
[C:/Users/marce/source/repos/MoonlightPublic/Moonlight/Moonlight/Shared/Views/Admin/Support/View.razor]
build_metadata.AdditionalFiles.TargetPath = U2hhcmVkXFZpZXdzXEFkbWluXFN1cHBvcnRcVmlldy5yYXpvcg==
build_metadata.AdditionalFiles.CssScope =
[C:/Users/marce/source/repos/MoonlightPublic/Moonlight/Moonlight/Shared/Views/Index.razor] [C:/Users/marce/source/repos/MoonlightPublic/Moonlight/Moonlight/Shared/Views/Index.razor]
build_metadata.AdditionalFiles.TargetPath = U2hhcmVkXFZpZXdzXEluZGV4LnJhem9y build_metadata.AdditionalFiles.TargetPath = U2hhcmVkXFZpZXdzXEluZGV4LnJhem9y
build_metadata.AdditionalFiles.CssScope = build_metadata.AdditionalFiles.CssScope =

View file

@ -215,3 +215,12 @@ less than a minute ago;less than a minute ago
1 hour ago;1 hour ago 1 hour ago;1 hour ago
1 minute ago;1 minute ago 1 minute ago;1 minute ago
Failed;Failed Failed;Failed
hours ago; hours ago
Open tickets;Open tickets
Actions;Actions
No support ticket is currently open;No support ticket is currently open
User information;User information
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