Merge pull request #109 from Moonlight-Panel/NewSupportChatFeatures
Added multi line feature and file upload in the support chat
This commit is contained in:
8
Moonlight/App/Database/Entities/SupportChatSnippets.cs
Normal file
8
Moonlight/App/Database/Entities/SupportChatSnippets.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Moonlight.App.Database.Entities;
|
||||||
|
|
||||||
|
public class SupportChatSnippets
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public string Description { get; set; } = "";
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Moonlight.App.Helpers;
|
using Moonlight.App.Helpers;
|
||||||
using Moonlight.App.Models.Misc;
|
using Moonlight.App.Models.Misc;
|
||||||
|
using Moonlight.App.Services;
|
||||||
using Moonlight.App.Services.LogServices;
|
using Moonlight.App.Services.LogServices;
|
||||||
|
|
||||||
namespace Moonlight.App.Http.Controllers.Api.Moonlight;
|
namespace Moonlight.App.Http.Controllers.Api.Moonlight;
|
||||||
@@ -11,10 +12,13 @@ namespace Moonlight.App.Http.Controllers.Api.Moonlight;
|
|||||||
public class ResourcesController : Controller
|
public class ResourcesController : Controller
|
||||||
{
|
{
|
||||||
private readonly SecurityLogService SecurityLogService;
|
private readonly SecurityLogService SecurityLogService;
|
||||||
|
private readonly BucketService BucketService;
|
||||||
|
|
||||||
public ResourcesController(SecurityLogService securityLogService)
|
public ResourcesController(SecurityLogService securityLogService,
|
||||||
|
BucketService bucketService)
|
||||||
{
|
{
|
||||||
SecurityLogService = securityLogService;
|
SecurityLogService = securityLogService;
|
||||||
|
BucketService = bucketService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("images/{name}")]
|
[HttpGet("images/{name}")]
|
||||||
@@ -26,6 +30,7 @@ public class ResourcesController : Controller
|
|||||||
{
|
{
|
||||||
x.Add<string>(name);
|
x.Add<string>(name);
|
||||||
});
|
});
|
||||||
|
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,4 +43,33 @@ public class ResourcesController : Controller
|
|||||||
|
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("bucket/{bucket}/{name}")]
|
||||||
|
public async Task<ActionResult> GetBucket([FromRoute] string bucket, [FromRoute] string name)
|
||||||
|
{
|
||||||
|
if (name.Contains(".."))
|
||||||
|
{
|
||||||
|
await SecurityLogService.Log(SecurityLogType.PathTransversal, x =>
|
||||||
|
{
|
||||||
|
x.Add<string>(name);
|
||||||
|
});
|
||||||
|
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var fs = await BucketService.GetFile(bucket, name);
|
||||||
|
|
||||||
|
return File(fs, MimeTypes.GetMimeType(name), name);
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return Problem();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
68
Moonlight/App/Services/BucketService.cs
Normal file
68
Moonlight/App/Services/BucketService.cs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
using Logging.Net;
|
||||||
|
using Moonlight.App.Helpers;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Services;
|
||||||
|
|
||||||
|
public class BucketService
|
||||||
|
{
|
||||||
|
private string BucketPath;
|
||||||
|
|
||||||
|
public BucketService()
|
||||||
|
{
|
||||||
|
BucketPath = PathBuilder.Dir("storage", "uploads");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<string[]> GetBuckets()
|
||||||
|
{
|
||||||
|
var buckets = Directory.GetDirectories(BucketPath)
|
||||||
|
.Select(x =>
|
||||||
|
x.Replace(BucketPath, "").TrimEnd('/')
|
||||||
|
)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
return Task.FromResult(buckets);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task EnsureBucket(string name)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(BucketPath + name);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> StoreFile(string bucket, Stream dataStream, string? name = null)
|
||||||
|
{
|
||||||
|
await EnsureBucket(bucket);
|
||||||
|
|
||||||
|
var extension = "";
|
||||||
|
|
||||||
|
if (name != null)
|
||||||
|
extension = Path.GetExtension(name);
|
||||||
|
|
||||||
|
var fileName = Path.GetRandomFileName() + extension; //TODO: Add check for existing file
|
||||||
|
var filePath = BucketPath + PathBuilder.File(bucket, fileName);
|
||||||
|
|
||||||
|
var fileStream = File.Create(filePath);
|
||||||
|
|
||||||
|
await dataStream.CopyToAsync(fileStream);
|
||||||
|
await fileStream.FlushAsync();
|
||||||
|
|
||||||
|
fileStream.Close();
|
||||||
|
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Stream> GetFile(string bucket, string file)
|
||||||
|
{
|
||||||
|
var filePath = BucketPath + PathBuilder.File(bucket, file);
|
||||||
|
|
||||||
|
if (File.Exists(filePath))
|
||||||
|
{
|
||||||
|
var stream = File.Open(filePath, FileMode.Open);
|
||||||
|
|
||||||
|
return Task.FromResult<Stream>(stream);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
throw new FileNotFoundException();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,4 +20,9 @@ public class ResourceService
|
|||||||
{
|
{
|
||||||
return $"{AppUrl}/api/moonlight/avatar/{user.Id}";
|
return $"{AppUrl}/api/moonlight/avatar/{user.Id}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string BucketItem(string bucket, string name)
|
||||||
|
{
|
||||||
|
return $"{AppUrl}/api/moonlight/resources/bucket/{bucket}/{name}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
using Moonlight.App.Database.Entities;
|
using Microsoft.AspNetCore.Components.Forms;
|
||||||
|
using Moonlight.App.Database.Entities;
|
||||||
using Moonlight.App.Events;
|
using Moonlight.App.Events;
|
||||||
using Moonlight.App.Services.Sessions;
|
using Moonlight.App.Services.Sessions;
|
||||||
|
|
||||||
namespace Moonlight.App.Services.SupportChat;
|
namespace Moonlight.App.Services.SupportChat;
|
||||||
|
|
||||||
public class SupportChatAdminService
|
public class SupportChatAdminService : IDisposable
|
||||||
{
|
{
|
||||||
private readonly EventSystem Event;
|
private readonly EventSystem Event;
|
||||||
private readonly IdentityService IdentityService;
|
private readonly IdentityService IdentityService;
|
||||||
private readonly SupportChatServerService ServerService;
|
private readonly SupportChatServerService ServerService;
|
||||||
|
private readonly BucketService BucketService;
|
||||||
|
|
||||||
public Func<SupportChatMessage, Task>? OnMessage { get; set; }
|
public Func<SupportChatMessage, Task>? OnMessage { get; set; }
|
||||||
public Func<string[], Task>? OnTypingChanged { get; set; }
|
public Func<string[], Task>? OnTypingChanged { get; set; }
|
||||||
@@ -20,11 +22,13 @@ public class SupportChatAdminService
|
|||||||
public SupportChatAdminService(
|
public SupportChatAdminService(
|
||||||
EventSystem eventSystem,
|
EventSystem eventSystem,
|
||||||
SupportChatServerService serverService,
|
SupportChatServerService serverService,
|
||||||
IdentityService identityService)
|
IdentityService identityService,
|
||||||
|
BucketService bucketService)
|
||||||
{
|
{
|
||||||
Event = eventSystem;
|
Event = eventSystem;
|
||||||
ServerService = serverService;
|
ServerService = serverService;
|
||||||
IdentityService = identityService;
|
IdentityService = identityService;
|
||||||
|
BucketService = bucketService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Start(User recipient)
|
public async Task Start(User recipient)
|
||||||
@@ -60,11 +64,21 @@ public class SupportChatAdminService
|
|||||||
return await ServerService.GetMessages(Recipient);
|
return await ServerService.GetMessages(Recipient);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SupportChatMessage> SendMessage(string content)
|
public async Task<SupportChatMessage> SendMessage(string content, IBrowserFile? browserFile = null)
|
||||||
{
|
{
|
||||||
if (User != null)
|
if (User != null)
|
||||||
{
|
{
|
||||||
return await ServerService.SendMessage(Recipient, content, User);
|
string? attachment = null;
|
||||||
|
|
||||||
|
if (browserFile != null)
|
||||||
|
{
|
||||||
|
attachment = await BucketService.StoreFile(
|
||||||
|
"supportChat",
|
||||||
|
browserFile.OpenReadStream(1024 * 1024 * 5),
|
||||||
|
browserFile.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await ServerService.SendMessage(Recipient, content, User, attachment);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null!;
|
return null!;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Logging.Net;
|
using Logging.Net;
|
||||||
|
using Microsoft.AspNetCore.Components.Forms;
|
||||||
using Moonlight.App.Database.Entities;
|
using Moonlight.App.Database.Entities;
|
||||||
using Moonlight.App.Events;
|
using Moonlight.App.Events;
|
||||||
using Moonlight.App.Services.Sessions;
|
using Moonlight.App.Services.Sessions;
|
||||||
@@ -8,9 +9,10 @@ namespace Moonlight.App.Services.SupportChat;
|
|||||||
public class SupportChatClientService : IDisposable
|
public class SupportChatClientService : IDisposable
|
||||||
{
|
{
|
||||||
private readonly EventSystem Event;
|
private readonly EventSystem Event;
|
||||||
|
private readonly BucketService BucketService;
|
||||||
private readonly IdentityService IdentityService;
|
private readonly IdentityService IdentityService;
|
||||||
private readonly SupportChatServerService ServerService;
|
private readonly SupportChatServerService ServerService;
|
||||||
|
|
||||||
public Func<SupportChatMessage, Task>? OnMessage { get; set; }
|
public Func<SupportChatMessage, Task>? OnMessage { get; set; }
|
||||||
public Func<string[], Task>? OnTypingChanged { get; set; }
|
public Func<string[], Task>? OnTypingChanged { get; set; }
|
||||||
|
|
||||||
@@ -20,11 +22,13 @@ public class SupportChatClientService : IDisposable
|
|||||||
public SupportChatClientService(
|
public SupportChatClientService(
|
||||||
EventSystem eventSystem,
|
EventSystem eventSystem,
|
||||||
SupportChatServerService serverService,
|
SupportChatServerService serverService,
|
||||||
IdentityService identityService)
|
IdentityService identityService,
|
||||||
|
BucketService bucketService)
|
||||||
{
|
{
|
||||||
Event = eventSystem;
|
Event = eventSystem;
|
||||||
ServerService = serverService;
|
ServerService = serverService;
|
||||||
IdentityService = identityService;
|
IdentityService = identityService;
|
||||||
|
BucketService = bucketService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Start()
|
public async Task Start()
|
||||||
@@ -59,11 +63,21 @@ public class SupportChatClientService : IDisposable
|
|||||||
return await ServerService.GetMessages(User);
|
return await ServerService.GetMessages(User);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SupportChatMessage> SendMessage(string content)
|
public async Task<SupportChatMessage> SendMessage(string content, IBrowserFile? browserFile = null)
|
||||||
{
|
{
|
||||||
if (User != null)
|
if (User != null)
|
||||||
{
|
{
|
||||||
return await ServerService.SendMessage(User, content, User);
|
string? attachment = null;
|
||||||
|
|
||||||
|
if (browserFile != null)
|
||||||
|
{
|
||||||
|
attachment = await BucketService.StoreFile(
|
||||||
|
"supportChat",
|
||||||
|
browserFile.OpenReadStream(1024 * 1024 * 5),
|
||||||
|
browserFile.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await ServerService.SendMessage(User, content, User, attachment);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null!;
|
return null!;
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ namespace Moonlight
|
|||||||
builder.Services.AddScoped<FileDownloadService>();
|
builder.Services.AddScoped<FileDownloadService>();
|
||||||
builder.Services.AddScoped<ForgeService>();
|
builder.Services.AddScoped<ForgeService>();
|
||||||
builder.Services.AddScoped<FabricService>();
|
builder.Services.AddScoped<FabricService>();
|
||||||
|
builder.Services.AddSingleton<BucketService>();
|
||||||
|
|
||||||
builder.Services.AddScoped<GoogleOAuth2Service>();
|
builder.Services.AddScoped<GoogleOAuth2Service>();
|
||||||
builder.Services.AddScoped<DiscordOAuth2Service>();
|
builder.Services.AddScoped<DiscordOAuth2Service>();
|
||||||
|
|||||||
61
Moonlight/Shared/Components/Forms/SmartFileSelect.razor
Normal file
61
Moonlight/Shared/Components/Forms/SmartFileSelect.razor
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
@using Moonlight.App.Helpers.Files
|
||||||
|
@using Moonlight.App.Services
|
||||||
|
@using Moonlight.App.Services.Interop
|
||||||
|
@using Logging.Net
|
||||||
|
|
||||||
|
@inject ToastService ToastService
|
||||||
|
@inject SmartTranslateService SmartTranslateService
|
||||||
|
|
||||||
|
<InputFile OnChange="OnFileChanged" type="file" id="fileUpload" hidden=""/>
|
||||||
|
<label for="fileUpload" class="btn btn-primary me-3">
|
||||||
|
@if (SelectedFile != null)
|
||||||
|
{
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control" value="@(SelectedFile.Name)">
|
||||||
|
<button class="btn btn-danger" type="button" @onclick="RemoveSelection">
|
||||||
|
<i class="bx bx-md bx-x"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="svg-icon svg-icon-2">
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path opacity="0.3" d="M10 4H21C21.6 4 22 4.4 22 5V7H10V4Z" fill="currentColor"></path>
|
||||||
|
<path d="M10.4 3.60001L12 6H21C21.6 6 22 6.4 22 7V19C22 19.6 21.6 20 21 20H3C2.4 20 2 19.6 2 19V4C2 3.4 2.4 3 3 3H9.20001C9.70001 3 10.2 3.20001 10.4 3.60001ZM16 11.6L12.7 8.29999C12.3 7.89999 11.7 7.89999 11.3 8.29999L8 11.6H11V17C11 17.6 11.4 18 12 18C12.6 18 13 17.6 13 17V11.6H16Z" fill="currentColor"></path>
|
||||||
|
<path opacity="0.3" d="M11 11.6V17C11 17.6 11.4 18 12 18C12.6 18 13 17.6 13 17V11.6H11Z" fill="currentColor"></path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
public IBrowserFile? SelectedFile { get; set; }
|
||||||
|
|
||||||
|
private async Task OnFileChanged(InputFileChangeEventArgs arg)
|
||||||
|
{
|
||||||
|
if (arg.FileCount > 0)
|
||||||
|
{
|
||||||
|
if (arg.File.Size < 1024 * 1024 * 5)
|
||||||
|
{
|
||||||
|
SelectedFile = arg.File;
|
||||||
|
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ToastService.Error(SmartTranslateService.Translate("The uploaded file should not be bigger than 5MB"));
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectedFile = null;
|
||||||
|
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RemoveSelection()
|
||||||
|
{
|
||||||
|
SelectedFile = null;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,12 +4,15 @@
|
|||||||
@using Moonlight.App.Repositories
|
@using Moonlight.App.Repositories
|
||||||
@using Moonlight.App.Services
|
@using Moonlight.App.Services
|
||||||
@using Moonlight.App.Services.SupportChat
|
@using Moonlight.App.Services.SupportChat
|
||||||
|
@using System.Text.RegularExpressions
|
||||||
|
|
||||||
@inject SupportChatAdminService AdminService
|
@inject SupportChatAdminService AdminService
|
||||||
@inject UserRepository UserRepository
|
@inject UserRepository UserRepository
|
||||||
@inject SmartTranslateService SmartTranslateService
|
@inject SmartTranslateService SmartTranslateService
|
||||||
@inject ResourceService ResourceService
|
@inject ResourceService ResourceService
|
||||||
|
|
||||||
|
@implements IDisposable
|
||||||
|
|
||||||
<OnlyAdmin>
|
<OnlyAdmin>
|
||||||
<LazyLoader Load="Load">
|
<LazyLoader Load="Load">
|
||||||
@if (User == null)
|
@if (User == null)
|
||||||
@@ -43,7 +46,9 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<span><TL>System</TL></span>
|
<span>
|
||||||
|
<TL>System</TL>
|
||||||
|
</span>
|
||||||
}
|
}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -59,7 +64,26 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@(message.Content)
|
foreach (var line in message.Content.Split("\n"))
|
||||||
|
{
|
||||||
|
@(line)<br/>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.Attachment != "")
|
||||||
|
{
|
||||||
|
<div class="mt-3">
|
||||||
|
@if (Regex.IsMatch(message.Attachment, @"\.(jpg|jpeg|png|gif|bmp)$"))
|
||||||
|
{
|
||||||
|
<img src="@(ResourceService.BucketItem("supportChat", message.Attachment))" class="img-fluid" alt="Attachment"/>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<a class="btn btn-secondary" href="@(ResourceService.BucketItem("supportChat", message.Attachment))">
|
||||||
|
<i class="me-2 bx bx-download"></i> @(message.Attachment)
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -82,7 +106,26 @@
|
|||||||
</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">
|
||||||
@(message.Content)
|
@foreach (var line in message.Content.Split("\n"))
|
||||||
|
{
|
||||||
|
@(line)<br/>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (message.Attachment != "")
|
||||||
|
{
|
||||||
|
<div class="mt-3">
|
||||||
|
@if (Regex.IsMatch(message.Attachment, @"\.(jpg|jpeg|png|gif|bmp)$"))
|
||||||
|
{
|
||||||
|
<img src="@(ResourceService.BucketItem("supportChat", message.Attachment))" class="img-fluid" alt="Attachment"/>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<a class="btn btn-secondary" href="@(ResourceService.BucketItem("supportChat", message.Attachment))">
|
||||||
|
<i class="me-2 bx bx-download"></i> @(message.Attachment)
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -118,11 +161,9 @@
|
|||||||
<div class="d-flex flex-stack">
|
<div class="d-flex flex-stack">
|
||||||
<table class="w-100">
|
<table class="w-100">
|
||||||
<tr>
|
<tr>
|
||||||
<!--<td class="align-top">
|
<td class="align-top">
|
||||||
<button class="btn btn-sm btn-icon btn-active-light-primary me-1" type="button">
|
<SmartFileSelect @ref="SmartFileSelect"></SmartFileSelect>
|
||||||
<i class="bx bx-upload fs-3"></i>
|
</td>
|
||||||
</button>
|
|
||||||
</td>-->
|
|
||||||
<td class="w-100">
|
<td class="w-100">
|
||||||
<textarea @bind="Content" @oninput="OnTyping" class="form-control mb-3 form-control-flush" rows="1" placeholder="Type a message">
|
<textarea @bind="Content" @oninput="OnTyping" class="form-control mb-3 form-control-flush" rows="1" placeholder="Type a message">
|
||||||
</textarea>
|
</textarea>
|
||||||
@@ -186,6 +227,8 @@
|
|||||||
private string Content = "";
|
private string Content = "";
|
||||||
private DateTime LastTypingTimestamp = DateTime.UtcNow.AddMinutes(-10);
|
private DateTime LastTypingTimestamp = DateTime.UtcNow.AddMinutes(-10);
|
||||||
|
|
||||||
|
private SmartFileSelect SmartFileSelect;
|
||||||
|
|
||||||
private async Task Load(LazyLoader arg)
|
private async Task Load(LazyLoader arg)
|
||||||
{
|
{
|
||||||
User = UserRepository
|
User = UserRepository
|
||||||
@@ -200,7 +243,7 @@
|
|||||||
await AdminService.Start(User);
|
await AdminService.Start(User);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadMessages(LazyLoader arg)
|
private async Task LoadMessages(LazyLoader arg)
|
||||||
{
|
{
|
||||||
Messages = (await AdminService.GetMessages()).ToList();
|
Messages = (await AdminService.GetMessages()).ToList();
|
||||||
@@ -217,21 +260,26 @@
|
|||||||
{
|
{
|
||||||
Messages.Insert(0, arg);
|
Messages.Insert(0, arg);
|
||||||
|
|
||||||
//TODO: Sound when message from system or admin
|
//TODO: Sound when message from system or admin
|
||||||
|
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task Send()
|
private async Task Send()
|
||||||
{
|
{
|
||||||
if(string.IsNullOrEmpty(Content))
|
if (SmartFileSelect.SelectedFile != null && string.IsNullOrEmpty(Content))
|
||||||
|
Content = "File upload";
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(Content))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var message = await AdminService.SendMessage(Content);
|
var message = await AdminService.SendMessage(Content, SmartFileSelect.SelectedFile);
|
||||||
Content = "";
|
Content = "";
|
||||||
|
|
||||||
|
await SmartFileSelect.RemoveSelection();
|
||||||
|
|
||||||
Messages.Insert(0, message);
|
Messages.Insert(0, message);
|
||||||
|
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,7 +287,7 @@
|
|||||||
{
|
{
|
||||||
await AdminService.Close();
|
await AdminService.Close();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnTyping()
|
private async Task OnTyping()
|
||||||
{
|
{
|
||||||
if ((DateTime.UtcNow - LastTypingTimestamp).TotalSeconds > 5)
|
if ((DateTime.UtcNow - LastTypingTimestamp).TotalSeconds > 5)
|
||||||
@@ -249,4 +297,9 @@
|
|||||||
await AdminService.SendTyping();
|
await AdminService.SendTyping();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
AdminService?.Dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,18 +4,21 @@
|
|||||||
@using Moonlight.App.Helpers
|
@using Moonlight.App.Helpers
|
||||||
@using Moonlight.App.Services.SupportChat
|
@using Moonlight.App.Services.SupportChat
|
||||||
@using Logging.Net
|
@using Logging.Net
|
||||||
|
@using System.Text.RegularExpressions
|
||||||
|
|
||||||
@inject ResourceService ResourceService
|
@inject ResourceService ResourceService
|
||||||
@inject SupportChatClientService ClientService
|
@inject SupportChatClientService ClientService
|
||||||
@inject SmartTranslateService SmartTranslateService
|
@inject SmartTranslateService SmartTranslateService
|
||||||
|
|
||||||
|
@implements IDisposable
|
||||||
|
|
||||||
<LazyLoader Load="Load">
|
<LazyLoader Load="Load">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<LazyLoader Load="LoadMessages">
|
<LazyLoader Load="LoadMessages">
|
||||||
<div class="scroll-y me-n5 pe-5" style="max-height: 55vh; display: flex; flex-direction: column-reverse;">
|
<div class="scroll-y me-n5 pe-5" style="max-height: 55vh; display: flex; flex-direction: column-reverse;">
|
||||||
@foreach (var message in Messages)
|
@foreach (var message in Messages.ToArray())
|
||||||
{
|
{
|
||||||
if (message.Sender == null || message.Sender.Id != User.Id)
|
if (message.Sender == null || message.Sender.Id != User.Id)
|
||||||
{
|
{
|
||||||
@@ -33,7 +36,9 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<span><TL>System</TL></span>
|
<span>
|
||||||
|
<TL>System</TL>
|
||||||
|
</span>
|
||||||
}
|
}
|
||||||
</a>
|
</a>
|
||||||
<span class="text-muted fs-7 mb-1">@(Formatter.FormatAgoFromDateTime(message.CreatedAt, SmartTranslateService))</span>
|
<span class="text-muted fs-7 mb-1">@(Formatter.FormatAgoFromDateTime(message.CreatedAt, SmartTranslateService))</span>
|
||||||
@@ -47,7 +52,26 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@(message.Content)
|
foreach (var line in message.Content.Split("\n"))
|
||||||
|
{
|
||||||
|
@(line)<br/>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.Attachment != "")
|
||||||
|
{
|
||||||
|
<div class="mt-3">
|
||||||
|
@if (Regex.IsMatch(message.Attachment, @"\.(jpg|jpeg|png|gif|bmp)$"))
|
||||||
|
{
|
||||||
|
<img src="@(ResourceService.BucketItem("supportChat", message.Attachment))" class="img-fluid" alt="Attachment"/>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<a class="btn btn-secondary" href="@(ResourceService.BucketItem("supportChat", message.Attachment))">
|
||||||
|
<i class="me-2 bx bx-download"></i> @(message.Attachment)
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,7 +94,26 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="p-5 rounded bg-light-primary text-dark fw-semibold mw-lg-400px text-end">
|
<div class="p-5 rounded bg-light-primary text-dark fw-semibold mw-lg-400px text-end">
|
||||||
@(message.Content)
|
@foreach (var line in message.Content.Split("\n"))
|
||||||
|
{
|
||||||
|
@(line)<br/>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (message.Attachment != "")
|
||||||
|
{
|
||||||
|
<div class="mt-3">
|
||||||
|
@if (Regex.IsMatch(message.Attachment, @"\.(jpg|jpeg|png|gif|bmp)$"))
|
||||||
|
{
|
||||||
|
<img src="@(ResourceService.BucketItem("supportChat", message.Attachment))" class="img-fluid" alt="Attachment"/>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<a class="btn btn-secondary" href="@(ResourceService.BucketItem("supportChat", message.Attachment))">
|
||||||
|
<i class="me-2 bx bx-download"></i> @(message.Attachment)
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -84,7 +127,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="ms-3">
|
<div class="ms-3">
|
||||||
<a class="fs-5 fw-bold text-gray-900 text-hover-primary me-1">
|
<a class="fs-5 fw-bold text-gray-900 text-hover-primary me-1">
|
||||||
<span><TL>System</TL></span>
|
<span>
|
||||||
|
<TL>System</TL>
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -108,11 +153,15 @@
|
|||||||
</div>
|
</div>
|
||||||
@if (Typing.Length > 1)
|
@if (Typing.Length > 1)
|
||||||
{
|
{
|
||||||
<span>@(Typing.Aggregate((current, next) => current + ", " + next)) <TL>are typing</TL></span>
|
<span>
|
||||||
|
@(Typing.Aggregate((current, next) => current + ", " + next)) <TL>are typing</TL>
|
||||||
|
</span>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<span>@(Typing.First()) <TL>is typing</TL></span>
|
<span>
|
||||||
|
@(Typing.First()) <TL>is typing</TL>
|
||||||
|
</span>
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
@@ -120,11 +169,9 @@
|
|||||||
<div class="d-flex flex-stack">
|
<div class="d-flex flex-stack">
|
||||||
<table class="w-100">
|
<table class="w-100">
|
||||||
<tr>
|
<tr>
|
||||||
<!--<td class="align-top">
|
<td class="align-top">
|
||||||
<button class="btn btn-sm btn-icon btn-active-light-primary me-1" type="button">
|
<SmartFileSelect @ref="SmartFileSelect"></SmartFileSelect>
|
||||||
<i class="bx bx-upload fs-3"></i>
|
</td>
|
||||||
</button>
|
|
||||||
</td>-->
|
|
||||||
<td class="w-100">
|
<td class="w-100">
|
||||||
<textarea @bind="Content" @oninput="OnTyping" class="form-control mb-3 form-control-flush" rows="1" placeholder="Type a message">
|
<textarea @bind="Content" @oninput="OnTyping" class="form-control mb-3 form-control-flush" rows="1" placeholder="Type a message">
|
||||||
</textarea>
|
</textarea>
|
||||||
@@ -148,23 +195,25 @@
|
|||||||
{
|
{
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
public User User { get; set; }
|
public User User { get; set; }
|
||||||
|
|
||||||
private List<SupportChatMessage> Messages = new();
|
private List<SupportChatMessage> Messages = new();
|
||||||
private string[] Typing = Array.Empty<string>();
|
private string[] Typing = Array.Empty<string>();
|
||||||
|
|
||||||
private string Content = "";
|
private string Content = "";
|
||||||
private DateTime LastTypingTimestamp = DateTime.UtcNow.AddMinutes(-10);
|
private DateTime LastTypingTimestamp = DateTime.UtcNow.AddMinutes(-10);
|
||||||
|
|
||||||
|
private SmartFileSelect SmartFileSelect;
|
||||||
|
|
||||||
private async Task Load(LazyLoader lazyLoader)
|
private async Task Load(LazyLoader lazyLoader)
|
||||||
{
|
{
|
||||||
await lazyLoader.SetText("Starting chat client");
|
await lazyLoader.SetText("Starting chat client");
|
||||||
|
|
||||||
ClientService.OnMessage += OnMessage;
|
ClientService.OnMessage += OnMessage;
|
||||||
ClientService.OnTypingChanged += OnTypingChanged;
|
ClientService.OnTypingChanged += OnTypingChanged;
|
||||||
|
|
||||||
await ClientService.Start();
|
await ClientService.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadMessages(LazyLoader arg)
|
private async Task LoadMessages(LazyLoader arg)
|
||||||
{
|
{
|
||||||
Messages = (await ClientService.GetMessages()).ToList();
|
Messages = (await ClientService.GetMessages()).ToList();
|
||||||
@@ -188,17 +237,19 @@
|
|||||||
|
|
||||||
private async Task Send()
|
private async Task Send()
|
||||||
{
|
{
|
||||||
if(string.IsNullOrEmpty(Content))
|
if (SmartFileSelect.SelectedFile != null && string.IsNullOrEmpty(Content))
|
||||||
return;
|
Content = "File upload";
|
||||||
|
|
||||||
var message = await ClientService.SendMessage(Content);
|
var message = await ClientService.SendMessage(Content, SmartFileSelect.SelectedFile);
|
||||||
Content = "";
|
Content = "";
|
||||||
|
|
||||||
|
await SmartFileSelect.RemoveSelection();
|
||||||
|
|
||||||
Messages.Insert(0, message);
|
Messages.Insert(0, message);
|
||||||
|
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnTyping()
|
private async void OnTyping()
|
||||||
{
|
{
|
||||||
if ((DateTime.UtcNow - LastTypingTimestamp).TotalSeconds > 5)
|
if ((DateTime.UtcNow - LastTypingTimestamp).TotalSeconds > 5)
|
||||||
@@ -207,4 +258,13 @@
|
|||||||
await ClientService.SendTyping();
|
await ClientService.SendTyping();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
ClientService?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnFileChange(InputFileChangeEventArgs obj)
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user