Added support chat sync. Added admin support chat pages
This commit is contained in:
parent
0b6882d3f8
commit
8f3f9fa1fb
11 changed files with 508 additions and 93 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -443,3 +443,4 @@ Moonlight/obj/project.assets.json
|
|||
Moonlight/obj/project.nuget.cache
|
||||
Moonlight/obj/project.packagespec.json
|
||||
Moonlight/obj/Debug/net6.0/Moonlight.GeneratedMSBuildEditorConfig.editorconfig
|
||||
*.editorconfig
|
||||
|
|
|
@ -1,8 +1,4 @@
|
|||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using JWT.Algorithms;
|
||||
using JWT.Builder;
|
||||
using Logging.Net;
|
||||
using Logging.Net;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Moonlight.App.Database;
|
||||
using Moonlight.App.Database.Entities;
|
||||
|
|
|
@ -3,7 +3,7 @@ using Moonlight.App.Services.Sessions;
|
|||
|
||||
namespace Moonlight.App.Services.Support;
|
||||
|
||||
public class SupportAdminServer
|
||||
public class SupportAdminService
|
||||
{
|
||||
private readonly SupportServerService SupportServerService;
|
||||
private readonly IdentityService IdentityService;
|
||||
|
@ -14,7 +14,7 @@ public class SupportAdminServer
|
|||
private User Self;
|
||||
private User Recipient;
|
||||
|
||||
public SupportAdminServer(
|
||||
public SupportAdminService(
|
||||
SupportServerService supportServerService,
|
||||
IdentityService identityService,
|
||||
MessageService messageService)
|
||||
|
@ -60,6 +60,11 @@ public class SupportAdminServer
|
|||
);
|
||||
}
|
||||
|
||||
public async Task Close()
|
||||
{
|
||||
await SupportServerService.Close(Recipient);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
MessageService.Unsubscribe($"support.{Recipient.Id}.message", this);
|
|
@ -26,6 +26,8 @@ public class SupportServerService : IDisposable
|
|||
var sender = UserRepository.Get().First(x => x.Id == s.Id);
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
message.CreatedAt = DateTime.UtcNow;
|
||||
message.Sender = sender;
|
||||
|
@ -41,6 +43,8 @@ public class SupportServerService : IDisposable
|
|||
recipient.SupportPending = true;
|
||||
UserRepository.Update(recipient);
|
||||
|
||||
if (!message.IsSupport)
|
||||
{
|
||||
var systemMessage = new SupportMessage()
|
||||
{
|
||||
Recipient = recipient,
|
||||
|
@ -52,13 +56,43 @@ public class SupportServerService : IDisposable
|
|||
SupportMessageRepository.Add(systemMessage);
|
||||
|
||||
await MessageService.Emit($"support.{recipient.Id}.message", systemMessage);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
var recipient = UserRepository.Get().First(x => x.Id == r.Id);
|
||||
|
|
|
@ -60,7 +60,6 @@
|
|||
<Folder Include="App\Http\Middleware" />
|
||||
<Folder Include="App\Models\AuditLogData" />
|
||||
<Folder Include="resources\lang" />
|
||||
<Folder Include="Shared\Views\Admin\Support" />
|
||||
<Folder Include="wwwroot\assets\media" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
@ -62,7 +62,7 @@ namespace Moonlight
|
|||
|
||||
// Support
|
||||
builder.Services.AddSingleton<SupportServerService>();
|
||||
builder.Services.AddScoped<SupportAdminServer>();
|
||||
builder.Services.AddScoped<SupportAdminService>();
|
||||
builder.Services.AddScoped<SupportClientService>();
|
||||
|
||||
// Helpers
|
||||
|
|
157
Moonlight/Shared/Views/Admin/Support/Index.razor
Normal file
157
Moonlight/Shared/Views/Admin/Support/Index.razor
Normal 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);
|
||||
}
|
||||
}
|
194
Moonlight/Shared/Views/Admin/Support/View.razor
Normal file
194
Moonlight/Shared/Views/Admin/Support/View.razor
Normal 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();
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@
|
|||
<div class="row">
|
||||
<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)
|
||||
{
|
||||
|
@ -41,7 +42,14 @@
|
|||
</div>
|
||||
|
||||
<div class="p-5 rounded bg-light-info text-dark fw-semibold mw-lg-400px text-start">
|
||||
@if (message.IsSystem)
|
||||
{
|
||||
<TL>@(message.Message)</TL>
|
||||
}
|
||||
else
|
||||
{
|
||||
@(message.Message)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -88,6 +96,7 @@
|
|||
</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">
|
||||
|
@ -125,8 +134,6 @@
|
|||
SupportClientService.OnNewMessage += OnNewMessage;
|
||||
|
||||
await SupportClientService.Start();
|
||||
|
||||
Messages = (await SupportClientService.GetMessages()).Reverse().ToArray();
|
||||
}
|
||||
|
||||
private async void OnNewMessage(object? sender, SupportMessage e)
|
||||
|
@ -141,4 +148,9 @@
|
|||
Content = "";
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task LoadMessages(LazyLoader arg)
|
||||
{
|
||||
Messages = (await SupportClientService.GetMessages()).Reverse().ToArray();
|
||||
}
|
||||
}
|
|
@ -199,6 +199,14 @@ build_metadata.AdditionalFiles.CssScope =
|
|||
build_metadata.AdditionalFiles.TargetPath = U2hhcmVkXFZpZXdzXEFkbWluXFNlcnZlcnNcTmV3LnJhem9y
|
||||
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]
|
||||
build_metadata.AdditionalFiles.TargetPath = U2hhcmVkXFZpZXdzXEluZGV4LnJhem9y
|
||||
build_metadata.AdditionalFiles.CssScope =
|
||||
|
|
|
@ -215,3 +215,12 @@ less than a minute ago;less than a minute ago
|
|||
1 hour ago;1 hour ago
|
||||
1 minute ago;1 minute ago
|
||||
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
|
||||
|
|
Loading…
Reference in a new issue