Merge pull request #84 from Moonlight-Panel/RewriteSupportChatBackend
Rewritten support chat backend. Added discord notifications
This commit is contained in:
@@ -45,6 +45,7 @@ public class DataContext : DbContext
|
|||||||
public DbSet<CloudPanel> CloudPanels { get; set; }
|
public DbSet<CloudPanel> CloudPanels { get; set; }
|
||||||
public DbSet<MySqlDatabase> Databases { get; set; }
|
public DbSet<MySqlDatabase> Databases { get; set; }
|
||||||
public DbSet<WebSpace> WebSpaces { get; set; }
|
public DbSet<WebSpace> WebSpaces { get; set; }
|
||||||
|
public DbSet<SupportChatMessage> SupportChatMessages { get; set; }
|
||||||
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
{
|
{
|
||||||
|
|||||||
21
Moonlight/App/Database/Entities/SupportChatMessage.cs
Normal file
21
Moonlight/App/Database/Entities/SupportChatMessage.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using Moonlight.App.Models.Misc;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Database.Entities;
|
||||||
|
|
||||||
|
public class SupportChatMessage
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
public string Content { get; set; } = "";
|
||||||
|
public string Attachment { get; set; } = "";
|
||||||
|
|
||||||
|
public User? Sender { get; set; }
|
||||||
|
public User Recipient { get; set; }
|
||||||
|
|
||||||
|
public bool IsQuestion { get; set; } = false;
|
||||||
|
public QuestionType QuestionType { get; set; }
|
||||||
|
public string Answer { get; set; } = "";
|
||||||
|
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public DateTime UpdatedAt { get; set; }
|
||||||
|
}
|
||||||
1087
Moonlight/App/Database/Migrations/20230420213846_AddedNewSupportChatModels.Designer.cs
generated
Normal file
1087
Moonlight/App/Database/Migrations/20230420213846_AddedNewSupportChatModels.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,138 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Moonlight.App.Database.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddedNewSupportChatModels : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Websites");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "PleskServers");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "SupportChatMessages",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||||
|
Content = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
Attachment = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
SenderId = table.Column<int>(type: "int", nullable: true),
|
||||||
|
RecipientId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
IsQuestion = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||||
|
QuestionType = table.Column<int>(type: "int", nullable: false),
|
||||||
|
Answer = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_SupportChatMessages", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_SupportChatMessages_Users_RecipientId",
|
||||||
|
column: x => x.RecipientId,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_SupportChatMessages_Users_SenderId",
|
||||||
|
column: x => x.SenderId,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id");
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_SupportChatMessages_RecipientId",
|
||||||
|
table: "SupportChatMessages",
|
||||||
|
column: "RecipientId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_SupportChatMessages_SenderId",
|
||||||
|
table: "SupportChatMessages",
|
||||||
|
column: "SenderId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "SupportChatMessages");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "PleskServers",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||||
|
ApiKey = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
ApiUrl = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
Name = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_PleskServers", x => x.Id);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Websites",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||||
|
OwnerId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
PleskServerId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
BaseDomain = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
FtpLogin = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
FtpPassword = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
PleskId = table.Column<int>(type: "int", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Websites", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Websites_PleskServers_PleskServerId",
|
||||||
|
column: x => x.PleskServerId,
|
||||||
|
principalTable: "PleskServers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Websites_Users_OwnerId",
|
||||||
|
column: x => x.OwnerId,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Websites_OwnerId",
|
||||||
|
table: "Websites",
|
||||||
|
column: "OwnerId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Websites_PleskServerId",
|
||||||
|
table: "Websites",
|
||||||
|
column: "PleskServerId");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -453,29 +453,6 @@ namespace Moonlight.App.Database.Migrations
|
|||||||
b.ToTable("NotificationClients");
|
b.ToTable("NotificationClients");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Moonlight.App.Database.Entities.PleskServer", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.Property<string>("ApiKey")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("longtext");
|
|
||||||
|
|
||||||
b.Property<string>("ApiUrl")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("longtext");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("longtext");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("PleskServers");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Revoke", b =>
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Revoke", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@@ -673,6 +650,51 @@ namespace Moonlight.App.Database.Migrations
|
|||||||
b.ToTable("Subscriptions");
|
b.ToTable("Subscriptions");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.SupportChatMessage", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Answer")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("Attachment")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("Content")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsQuestion")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<int>("QuestionType")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("RecipientId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int?>("SenderId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RecipientId");
|
||||||
|
|
||||||
|
b.HasIndex("SenderId");
|
||||||
|
|
||||||
|
b.ToTable("SupportChatMessages");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Moonlight.App.Database.Entities.SupportMessage", b =>
|
modelBuilder.Entity("Moonlight.App.Database.Entities.SupportMessage", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@@ -836,42 +858,6 @@ namespace Moonlight.App.Database.Migrations
|
|||||||
b.ToTable("WebSpaces");
|
b.ToTable("WebSpaces");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Website", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.Property<string>("BaseDomain")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("longtext");
|
|
||||||
|
|
||||||
b.Property<string>("FtpLogin")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("longtext");
|
|
||||||
|
|
||||||
b.Property<string>("FtpPassword")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("longtext");
|
|
||||||
|
|
||||||
b.Property<int>("OwnerId")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.Property<int>("PleskId")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.Property<int>("PleskServerId")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("OwnerId");
|
|
||||||
|
|
||||||
b.HasIndex("PleskServerId");
|
|
||||||
|
|
||||||
b.ToTable("Websites");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Moonlight.App.Database.Entities.DdosAttack", b =>
|
modelBuilder.Entity("Moonlight.App.Database.Entities.DdosAttack", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Moonlight.App.Database.Entities.Node", "Node")
|
b.HasOne("Moonlight.App.Database.Entities.Node", "Node")
|
||||||
@@ -1007,6 +993,23 @@ namespace Moonlight.App.Database.Migrations
|
|||||||
.HasForeignKey("ServerId");
|
.HasForeignKey("ServerId");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.SupportChatMessage", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Moonlight.App.Database.Entities.User", "Recipient")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RecipientId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Moonlight.App.Database.Entities.User", "Sender")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SenderId");
|
||||||
|
|
||||||
|
b.Navigation("Recipient");
|
||||||
|
|
||||||
|
b.Navigation("Sender");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Moonlight.App.Database.Entities.SupportMessage", b =>
|
modelBuilder.Entity("Moonlight.App.Database.Entities.SupportMessage", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Moonlight.App.Database.Entities.User", "Recipient")
|
b.HasOne("Moonlight.App.Database.Entities.User", "Recipient")
|
||||||
@@ -1050,25 +1053,6 @@ namespace Moonlight.App.Database.Migrations
|
|||||||
b.Navigation("Owner");
|
b.Navigation("Owner");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Website", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Moonlight.App.Database.Entities.User", "Owner")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("OwnerId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("Moonlight.App.Database.Entities.PleskServer", "PleskServer")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("PleskServerId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Owner");
|
|
||||||
|
|
||||||
b.Navigation("PleskServer");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Image", b =>
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Image", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("DockerImages");
|
b.Navigation("DockerImages");
|
||||||
|
|||||||
@@ -80,7 +80,15 @@ public class EventSystem
|
|||||||
|
|
||||||
var del = (Delegate)subscriber.Action;
|
var del = (Delegate)subscriber.Action;
|
||||||
|
|
||||||
((Task)del.DynamicInvoke(data)!).Wait();
|
try
|
||||||
|
{
|
||||||
|
((Task)del.DynamicInvoke(data)!).Wait();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.Warn($"Error emitting '{subscriber.Id} on {subscriber.Handle}'");
|
||||||
|
Logger.Warn(e);
|
||||||
|
}
|
||||||
|
|
||||||
stopWatch.Stop();
|
stopWatch.Stop();
|
||||||
|
|
||||||
|
|||||||
101
Moonlight/App/Services/DiscordNotificationService.cs
Normal file
101
Moonlight/App/Services/DiscordNotificationService.cs
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
using Discord;
|
||||||
|
using Discord.Webhook;
|
||||||
|
using Logging.Net;
|
||||||
|
using Moonlight.App.Database.Entities;
|
||||||
|
using Moonlight.App.Events;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Services;
|
||||||
|
|
||||||
|
public class DiscordNotificationService
|
||||||
|
{
|
||||||
|
private readonly EventSystem Event;
|
||||||
|
private readonly ResourceService ResourceService;
|
||||||
|
private readonly DiscordWebhookClient Client;
|
||||||
|
private readonly string AppUrl;
|
||||||
|
|
||||||
|
public DiscordNotificationService(
|
||||||
|
EventSystem eventSystem,
|
||||||
|
ConfigService configService,
|
||||||
|
ResourceService resourceService)
|
||||||
|
{
|
||||||
|
Event = eventSystem;
|
||||||
|
ResourceService = resourceService;
|
||||||
|
|
||||||
|
var config = configService.GetSection("Moonlight").GetSection("DiscordNotifications");
|
||||||
|
|
||||||
|
if (config.GetValue<bool>("Enable"))
|
||||||
|
{
|
||||||
|
Logger.Info("Discord notifications enabled");
|
||||||
|
|
||||||
|
Client = new(config.GetValue<string>("WebHook"));
|
||||||
|
AppUrl = configService.GetSection("Moonlight").GetValue<string>("AppUrl");
|
||||||
|
|
||||||
|
Event.On<User>("supportChat.new", this, OnNewSupportChat);
|
||||||
|
Event.On<SupportChatMessage>("supportChat.message", this, OnSupportChatMessage);
|
||||||
|
Event.On<User>("supportChat.close", this, OnSupportChatClose);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.Info("Discord notifications disabled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnSupportChatClose(User user)
|
||||||
|
{
|
||||||
|
await SendNotification("", builder =>
|
||||||
|
{
|
||||||
|
builder.Title = "A new support chat has been marked as closed";
|
||||||
|
builder.Color = Color.Red;
|
||||||
|
builder.AddField("Email", user.Email);
|
||||||
|
builder.AddField("Firstname", user.FirstName);
|
||||||
|
builder.AddField("Lastname", user.LastName);
|
||||||
|
builder.Url = $"{AppUrl}/admin/support/view/{user.Id}";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnSupportChatMessage(SupportChatMessage message)
|
||||||
|
{
|
||||||
|
if(message.Sender == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await SendNotification("", builder =>
|
||||||
|
{
|
||||||
|
builder.Title = "New message in support chat";
|
||||||
|
builder.Color = Color.Blue;
|
||||||
|
builder.AddField("Message", message.Content);
|
||||||
|
builder.Author = new EmbedAuthorBuilder()
|
||||||
|
.WithName($"{message.Sender.FirstName} {message.Sender.LastName}")
|
||||||
|
.WithIconUrl(ResourceService.Avatar(message.Sender));
|
||||||
|
builder.Url = $"{AppUrl}/admin/support/view/{message.Recipient.Id}";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnNewSupportChat(User user)
|
||||||
|
{
|
||||||
|
await SendNotification("", builder =>
|
||||||
|
{
|
||||||
|
builder.Title = "A new support chat has been marked as active";
|
||||||
|
builder.Color = Color.Green;
|
||||||
|
builder.AddField("Email", user.Email);
|
||||||
|
builder.AddField("Firstname", user.FirstName);
|
||||||
|
builder.AddField("Lastname", user.LastName);
|
||||||
|
builder.Url = $"{AppUrl}/admin/support/view/{user.Id}";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SendNotification(string content, Action<EmbedBuilder>? embed = null)
|
||||||
|
{
|
||||||
|
Logger.Debug(Client);
|
||||||
|
|
||||||
|
var e = new EmbedBuilder();
|
||||||
|
embed?.Invoke(e);
|
||||||
|
|
||||||
|
await Client.SendMessageAsync(
|
||||||
|
content,
|
||||||
|
false,
|
||||||
|
new []{e.Build()},
|
||||||
|
"Moonlight Notification",
|
||||||
|
ResourceService.Image("logo.svg")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
127
Moonlight/App/Services/SupportChat/SupportChatAdminService.cs
Normal file
127
Moonlight/App/Services/SupportChat/SupportChatAdminService.cs
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
using Moonlight.App.Database.Entities;
|
||||||
|
using Moonlight.App.Events;
|
||||||
|
using Moonlight.App.Services.Sessions;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Services.SupportChat;
|
||||||
|
|
||||||
|
public class SupportChatAdminService
|
||||||
|
{
|
||||||
|
private readonly EventSystem Event;
|
||||||
|
private readonly IdentityService IdentityService;
|
||||||
|
private readonly SupportChatServerService ServerService;
|
||||||
|
|
||||||
|
public Func<SupportChatMessage, Task>? OnMessage { get; set; }
|
||||||
|
public Func<string[], Task>? OnTypingChanged { get; set; }
|
||||||
|
|
||||||
|
private User? User;
|
||||||
|
private User Recipient = null!;
|
||||||
|
private readonly List<User> TypingUsers = new();
|
||||||
|
|
||||||
|
public SupportChatAdminService(
|
||||||
|
EventSystem eventSystem,
|
||||||
|
SupportChatServerService serverService,
|
||||||
|
IdentityService identityService)
|
||||||
|
{
|
||||||
|
Event = eventSystem;
|
||||||
|
ServerService = serverService;
|
||||||
|
IdentityService = identityService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Start(User recipient)
|
||||||
|
{
|
||||||
|
User = await IdentityService.Get();
|
||||||
|
Recipient = recipient;
|
||||||
|
|
||||||
|
if (User != null)
|
||||||
|
{
|
||||||
|
await Event.On<SupportChatMessage>($"supportChat.{Recipient.Id}.message", this, async message =>
|
||||||
|
{
|
||||||
|
if(OnMessage != null)
|
||||||
|
await OnMessage.Invoke(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Event.On<User>($"supportChat.{Recipient.Id}.typing", this, async user =>
|
||||||
|
{
|
||||||
|
await HandleTyping(user);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SupportChatMessage[]> GetMessages()
|
||||||
|
{
|
||||||
|
if (User == null)
|
||||||
|
return Array.Empty<SupportChatMessage>();
|
||||||
|
|
||||||
|
return await ServerService.GetMessages(Recipient);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendMessage(string content)
|
||||||
|
{
|
||||||
|
if (User != null)
|
||||||
|
{
|
||||||
|
await ServerService.SendMessage(Recipient, content, User);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task HandleTyping(User user)
|
||||||
|
{
|
||||||
|
lock (TypingUsers)
|
||||||
|
{
|
||||||
|
if (!TypingUsers.Contains(user))
|
||||||
|
{
|
||||||
|
TypingUsers.Add(user);
|
||||||
|
|
||||||
|
if (OnTypingChanged != null)
|
||||||
|
{
|
||||||
|
OnTypingChanged.Invoke(
|
||||||
|
TypingUsers
|
||||||
|
.Where(x => x.Id != User!.Id)
|
||||||
|
.Select(x => $"{x.FirstName} {x.LastName}")
|
||||||
|
.ToArray()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(5));
|
||||||
|
|
||||||
|
if (TypingUsers.Contains(user))
|
||||||
|
{
|
||||||
|
TypingUsers.Remove(user);
|
||||||
|
|
||||||
|
if (OnTypingChanged != null)
|
||||||
|
{
|
||||||
|
await OnTypingChanged.Invoke(
|
||||||
|
TypingUsers
|
||||||
|
.Where(x => x.Id != User!.Id)
|
||||||
|
.Select(x => $"{x.FirstName} {x.LastName}")
|
||||||
|
.ToArray()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendTyping()
|
||||||
|
{
|
||||||
|
await Event.Emit($"supportChat.{Recipient.Id}.typing", User);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Close()
|
||||||
|
{
|
||||||
|
await ServerService.CloseChat(Recipient);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void Dispose()
|
||||||
|
{
|
||||||
|
if (User != null)
|
||||||
|
{
|
||||||
|
await Event.Off($"supportChat.{Recipient.Id}.message", this);
|
||||||
|
await Event.Off($"supportChat.{Recipient.Id}.typing", this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
121
Moonlight/App/Services/SupportChat/SupportChatClientService.cs
Normal file
121
Moonlight/App/Services/SupportChat/SupportChatClientService.cs
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
using Logging.Net;
|
||||||
|
using Moonlight.App.Database.Entities;
|
||||||
|
using Moonlight.App.Events;
|
||||||
|
using Moonlight.App.Services.Sessions;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Services.SupportChat;
|
||||||
|
|
||||||
|
public class SupportChatClientService : IDisposable
|
||||||
|
{
|
||||||
|
private readonly EventSystem Event;
|
||||||
|
private readonly IdentityService IdentityService;
|
||||||
|
private readonly SupportChatServerService ServerService;
|
||||||
|
|
||||||
|
public Func<SupportChatMessage, Task>? OnMessage { get; set; }
|
||||||
|
public Func<string[], Task>? OnTypingChanged { get; set; }
|
||||||
|
|
||||||
|
private User? User;
|
||||||
|
private readonly List<User> TypingUsers = new();
|
||||||
|
|
||||||
|
public SupportChatClientService(
|
||||||
|
EventSystem eventSystem,
|
||||||
|
SupportChatServerService serverService,
|
||||||
|
IdentityService identityService)
|
||||||
|
{
|
||||||
|
Event = eventSystem;
|
||||||
|
ServerService = serverService;
|
||||||
|
IdentityService = identityService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Start()
|
||||||
|
{
|
||||||
|
User = await IdentityService.Get();
|
||||||
|
|
||||||
|
if (User != null)
|
||||||
|
{
|
||||||
|
await Event.On<SupportChatMessage>($"supportChat.{User.Id}.message", this, async message =>
|
||||||
|
{
|
||||||
|
if(OnMessage != null)
|
||||||
|
await OnMessage.Invoke(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Event.On<User>($"supportChat.{User.Id}.typing", this, async user =>
|
||||||
|
{
|
||||||
|
await HandleTyping(user);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SupportChatMessage[]> GetMessages()
|
||||||
|
{
|
||||||
|
if (User == null)
|
||||||
|
return Array.Empty<SupportChatMessage>();
|
||||||
|
|
||||||
|
return await ServerService.GetMessages(User);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendMessage(string content)
|
||||||
|
{
|
||||||
|
if (User != null)
|
||||||
|
{
|
||||||
|
await ServerService.SendMessage(User, content, User);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task HandleTyping(User user)
|
||||||
|
{
|
||||||
|
lock (TypingUsers)
|
||||||
|
{
|
||||||
|
if (!TypingUsers.Contains(user))
|
||||||
|
{
|
||||||
|
TypingUsers.Add(user);
|
||||||
|
|
||||||
|
if (OnTypingChanged != null)
|
||||||
|
{
|
||||||
|
OnTypingChanged.Invoke(
|
||||||
|
TypingUsers
|
||||||
|
.Where(x => x.Id != User!.Id)
|
||||||
|
.Select(x => $"{x.FirstName} {x.LastName}")
|
||||||
|
.ToArray()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(5));
|
||||||
|
|
||||||
|
if (TypingUsers.Contains(user))
|
||||||
|
{
|
||||||
|
TypingUsers.Remove(user);
|
||||||
|
|
||||||
|
if (OnTypingChanged != null)
|
||||||
|
{
|
||||||
|
await OnTypingChanged.Invoke(
|
||||||
|
TypingUsers
|
||||||
|
.Where(x => x.Id != User!.Id)
|
||||||
|
.Select(x => $"{x.FirstName} {x.LastName}")
|
||||||
|
.ToArray()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendTyping()
|
||||||
|
{
|
||||||
|
await Event.Emit($"supportChat.{User!.Id}.typing", User);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void Dispose()
|
||||||
|
{
|
||||||
|
if (User != null)
|
||||||
|
{
|
||||||
|
await Event.Off($"supportChat.{User.Id}.message", this);
|
||||||
|
await Event.Off($"supportChat.{User.Id}.typing", this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
144
Moonlight/App/Services/SupportChat/SupportChatServerService.cs
Normal file
144
Moonlight/App/Services/SupportChat/SupportChatServerService.cs
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Moonlight.App.Database.Entities;
|
||||||
|
using Moonlight.App.Events;
|
||||||
|
using Moonlight.App.Repositories;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Services.SupportChat;
|
||||||
|
|
||||||
|
public class SupportChatServerService
|
||||||
|
{
|
||||||
|
private readonly IServiceScopeFactory ServiceScopeFactory;
|
||||||
|
private readonly DateTimeService DateTimeService;
|
||||||
|
private readonly EventSystem Event;
|
||||||
|
|
||||||
|
public SupportChatServerService(
|
||||||
|
IServiceScopeFactory serviceScopeFactory,
|
||||||
|
DateTimeService dateTimeService,
|
||||||
|
EventSystem eventSystem)
|
||||||
|
{
|
||||||
|
ServiceScopeFactory = serviceScopeFactory;
|
||||||
|
DateTimeService = dateTimeService;
|
||||||
|
Event = eventSystem;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<SupportChatMessage[]> GetMessages(User recipient)
|
||||||
|
{
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
var msgRepo = scope.ServiceProvider.GetRequiredService<Repository<SupportChatMessage>>();
|
||||||
|
|
||||||
|
var messages = msgRepo
|
||||||
|
.Get()
|
||||||
|
.Include(x => x.Recipient)
|
||||||
|
.Include(x => x.Sender)
|
||||||
|
.Where(x => x.Recipient.Id == recipient.Id)
|
||||||
|
.OrderByDescending(x => x.CreatedAt)
|
||||||
|
.AsEnumerable()
|
||||||
|
.Take(50)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
return Task.FromResult(messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendMessage(User recipient, string content, User? sender, string? attachment = null)
|
||||||
|
{
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
var msgRepo = scope.ServiceProvider.GetRequiredService<Repository<SupportChatMessage>>();
|
||||||
|
var userRepo = scope.ServiceProvider.GetRequiredService<Repository<User>>();
|
||||||
|
|
||||||
|
var message = new SupportChatMessage()
|
||||||
|
{
|
||||||
|
CreatedAt = DateTimeService.GetCurrent(),
|
||||||
|
IsQuestion = false,
|
||||||
|
Sender = sender == null ? null : userRepo.Get().First(x => x.Id == sender.Id),
|
||||||
|
Recipient = userRepo.Get().First(x => x.Id == recipient.Id),
|
||||||
|
Answer = "",
|
||||||
|
Attachment = attachment ?? "",
|
||||||
|
Content = content,
|
||||||
|
UpdatedAt = DateTimeService.GetCurrent()
|
||||||
|
};
|
||||||
|
|
||||||
|
var finalMessage = msgRepo.Add(message);
|
||||||
|
|
||||||
|
await Event.Emit($"supportChat.{recipient.Id}.message", finalMessage);
|
||||||
|
await Event.Emit("supportChat.message", finalMessage);
|
||||||
|
|
||||||
|
if (!userRepo.Get().First(x => x.Id == recipient.Id).SupportPending)
|
||||||
|
{
|
||||||
|
var ticketStart = new SupportChatMessage()
|
||||||
|
{
|
||||||
|
CreatedAt = DateTimeService.GetCurrent(),
|
||||||
|
IsQuestion = false,
|
||||||
|
Sender = null,
|
||||||
|
Recipient = userRepo.Get().First(x => x.Id == recipient.Id),
|
||||||
|
Answer = "",
|
||||||
|
Attachment = "",
|
||||||
|
Content = "Support ticket open", //TODO: Config
|
||||||
|
UpdatedAt = DateTimeService.GetCurrent()
|
||||||
|
};
|
||||||
|
|
||||||
|
var ticketStartFinal = msgRepo.Add(ticketStart);
|
||||||
|
|
||||||
|
var user = userRepo.Get().First(x => x.Id == recipient.Id);
|
||||||
|
user.SupportPending = true;
|
||||||
|
userRepo.Update(user);
|
||||||
|
|
||||||
|
await Event.Emit($"supportChat.{recipient.Id}.message", ticketStartFinal);
|
||||||
|
await Event.Emit("supportChat.message", ticketStartFinal);
|
||||||
|
await Event.Emit("supportChat.new", recipient);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Dictionary<User, SupportChatMessage?>> GetOpenChats()
|
||||||
|
{
|
||||||
|
var result = new Dictionary<User, SupportChatMessage?>();
|
||||||
|
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
var userRepo = scope.ServiceProvider.GetRequiredService<Repository<User>>();
|
||||||
|
var msgRepo = scope.ServiceProvider.GetRequiredService<Repository<SupportChatMessage>>();
|
||||||
|
|
||||||
|
foreach (var user in userRepo.Get().Where(x => x.SupportPending).ToArray())
|
||||||
|
{
|
||||||
|
var lastMessage = msgRepo
|
||||||
|
.Get()
|
||||||
|
.Include(x => x.Recipient)
|
||||||
|
.Include(x => x.Sender)
|
||||||
|
.Where(x => x.Recipient.Id == user.Id)
|
||||||
|
.OrderByDescending(x => x.CreatedAt)
|
||||||
|
.AsEnumerable()
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
result.Add(user, lastMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CloseChat(User recipient)
|
||||||
|
{
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
var msgRepo = scope.ServiceProvider.GetRequiredService<Repository<SupportChatMessage>>();
|
||||||
|
var userRepo = scope.ServiceProvider.GetRequiredService<Repository<User>>();
|
||||||
|
|
||||||
|
var ticketEnd = new SupportChatMessage()
|
||||||
|
{
|
||||||
|
CreatedAt = DateTimeService.GetCurrent(),
|
||||||
|
IsQuestion = false,
|
||||||
|
Sender = null,
|
||||||
|
Recipient = userRepo.Get().First(x => x.Id == recipient.Id),
|
||||||
|
Answer = "",
|
||||||
|
Attachment = "",
|
||||||
|
Content = "Support ticket closed", //TODO: Config
|
||||||
|
UpdatedAt = DateTimeService.GetCurrent()
|
||||||
|
};
|
||||||
|
|
||||||
|
var ticketEndFinal = msgRepo.Add(ticketEnd);
|
||||||
|
|
||||||
|
var user = userRepo.Get().First(x => x.Id == recipient.Id);
|
||||||
|
user.SupportPending = false;
|
||||||
|
userRepo.Update(user);
|
||||||
|
|
||||||
|
await Event.Emit($"supportChat.{recipient.Id}.message", ticketEndFinal);
|
||||||
|
await Event.Emit("supportChat.message", ticketEndFinal);
|
||||||
|
await Event.Emit("supportChat.close", recipient);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,8 +18,8 @@
|
|||||||
<PackageReference Include="BlazorTable" Version="1.17.0" />
|
<PackageReference Include="BlazorTable" Version="1.17.0" />
|
||||||
<PackageReference Include="CloudFlare.Client" Version="6.1.4" />
|
<PackageReference Include="CloudFlare.Client" Version="6.1.4" />
|
||||||
<PackageReference Include="CurrieTechnologies.Razor.SweetAlert2" Version="5.4.0" />
|
<PackageReference Include="CurrieTechnologies.Razor.SweetAlert2" Version="5.4.0" />
|
||||||
<PackageReference Include="Discord.Net" Version="3.9.0" />
|
<PackageReference Include="Discord.Net" Version="3.10.0" />
|
||||||
<PackageReference Include="DiscordWebhooks" Version="1.0.4" />
|
<PackageReference Include="Discord.Net.Webhook" Version="3.10.0" />
|
||||||
<PackageReference Include="FluentFTP" Version="46.0.2" />
|
<PackageReference Include="FluentFTP" Version="46.0.2" />
|
||||||
<PackageReference Include="GravatarSharp.Core" Version="1.0.1.2" />
|
<PackageReference Include="GravatarSharp.Core" Version="1.0.1.2" />
|
||||||
<PackageReference Include="JWT" Version="10.0.2" />
|
<PackageReference Include="JWT" Version="10.0.2" />
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ using Moonlight.App.Services.OAuth2;
|
|||||||
using Moonlight.App.Services.Sessions;
|
using Moonlight.App.Services.Sessions;
|
||||||
using Moonlight.App.Services.Statistics;
|
using Moonlight.App.Services.Statistics;
|
||||||
using Moonlight.App.Services.Support;
|
using Moonlight.App.Services.Support;
|
||||||
|
using Moonlight.App.Services.SupportChat;
|
||||||
|
|
||||||
namespace Moonlight
|
namespace Moonlight
|
||||||
{
|
{
|
||||||
@@ -124,10 +125,15 @@ namespace Moonlight
|
|||||||
builder.Services.AddScoped<MailService>();
|
builder.Services.AddScoped<MailService>();
|
||||||
builder.Services.AddSingleton<TrashMailDetectorService>();
|
builder.Services.AddSingleton<TrashMailDetectorService>();
|
||||||
|
|
||||||
// Support
|
// Support TODO: Remove
|
||||||
builder.Services.AddSingleton<SupportServerService>();
|
builder.Services.AddSingleton<SupportServerService>();
|
||||||
builder.Services.AddScoped<SupportAdminService>();
|
builder.Services.AddScoped<SupportAdminService>();
|
||||||
builder.Services.AddScoped<SupportClientService>();
|
builder.Services.AddScoped<SupportClientService>();
|
||||||
|
|
||||||
|
// Support chat
|
||||||
|
builder.Services.AddSingleton<SupportChatServerService>();
|
||||||
|
builder.Services.AddScoped<SupportChatClientService>();
|
||||||
|
builder.Services.AddScoped<SupportChatAdminService>();
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
builder.Services.AddSingleton<SmartTranslateHelper>();
|
builder.Services.AddSingleton<SmartTranslateHelper>();
|
||||||
@@ -143,6 +149,7 @@ namespace Moonlight
|
|||||||
// Background services
|
// Background services
|
||||||
builder.Services.AddSingleton<DiscordBotService>();
|
builder.Services.AddSingleton<DiscordBotService>();
|
||||||
builder.Services.AddSingleton<StatisticsCaptureService>();
|
builder.Services.AddSingleton<StatisticsCaptureService>();
|
||||||
|
builder.Services.AddSingleton<DiscordNotificationService>();
|
||||||
|
|
||||||
// Third party services
|
// Third party services
|
||||||
builder.Services.AddBlazorTable();
|
builder.Services.AddBlazorTable();
|
||||||
@@ -176,6 +183,7 @@ namespace Moonlight
|
|||||||
_ = app.Services.GetRequiredService<CleanupService>();
|
_ = app.Services.GetRequiredService<CleanupService>();
|
||||||
_ = app.Services.GetRequiredService<DiscordBotService>();
|
_ = app.Services.GetRequiredService<DiscordBotService>();
|
||||||
_ = app.Services.GetRequiredService<StatisticsCaptureService>();
|
_ = app.Services.GetRequiredService<StatisticsCaptureService>();
|
||||||
|
_ = app.Services.GetRequiredService<DiscordNotificationService>();
|
||||||
|
|
||||||
// Discord bot service
|
// Discord bot service
|
||||||
//var discordBotService = app.Services.GetRequiredService<DiscordBotService>();
|
//var discordBotService = app.Services.GetRequiredService<DiscordBotService>();
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
@page "/admin/support"
|
@page "/admin/support"
|
||||||
@using Moonlight.App.Repositories
|
|
||||||
@using Moonlight.App.Database.Entities
|
@using Moonlight.App.Database.Entities
|
||||||
@using Microsoft.EntityFrameworkCore
|
@using Moonlight.App.Events
|
||||||
@using Moonlight.App.Database
|
@using Moonlight.App.Services.SupportChat
|
||||||
@using Moonlight.App.Services
|
|
||||||
|
@inject SupportChatServerService ServerService
|
||||||
|
@inject EventSystem Event
|
||||||
|
|
||||||
@inject SupportMessageRepository SupportMessageRepository
|
|
||||||
@inject ConfigService ConfigService
|
|
||||||
@inject MessageService MessageService
|
|
||||||
@implements IDisposable
|
@implements IDisposable
|
||||||
|
|
||||||
<OnlyAdmin>
|
<OnlyAdmin>
|
||||||
@@ -18,13 +16,13 @@
|
|||||||
<div class="flex-lg-row-fluid me-xl-15 mb-20 mb-xl-0">
|
<div class="flex-lg-row-fluid me-xl-15 mb-20 mb-xl-0">
|
||||||
<div class="mb-0">
|
<div class="mb-0">
|
||||||
<h1 class="text-dark mb-6">
|
<h1 class="text-dark mb-6">
|
||||||
<TL>Open tickets</TL>
|
<TL>Open chats</TL>
|
||||||
</h1>
|
</h1>
|
||||||
<div class="separator"></div>
|
<div class="separator"></div>
|
||||||
<div class="mb-5">
|
<div class="mb-5">
|
||||||
@if (Users.Any())
|
@if (OpenChats.Any())
|
||||||
{
|
{
|
||||||
foreach (var user in Users)
|
foreach (var chat in OpenChats)
|
||||||
{
|
{
|
||||||
<div class="d-flex mt-3 mb-3 ms-2 me-2">
|
<div class="d-flex mt-3 mb-3 ms-2 me-2">
|
||||||
<table>
|
<table>
|
||||||
@@ -35,25 +33,21 @@
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="/admin/support/view/@(user.Id)" class="text-dark text-hover-primary fs-4 me-3 fw-semibold">
|
<a href="/admin/support/view/@(chat.Key.Id)" class="text-dark text-hover-primary fs-4 me-3 fw-semibold">
|
||||||
@(user.FirstName) @(user.LastName)
|
@(chat.Key.FirstName) @(chat.Key.LastName)
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<span class="text-muted fw-semibold fs-6">
|
<span class="text-muted fw-semibold fs-6">
|
||||||
@{
|
@if (chat.Value == null)
|
||||||
var lastMessage = MessageCache.ContainsKey(user) ? MessageCache[user] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (lastMessage == null)
|
|
||||||
{
|
{
|
||||||
<TL>No message sent yet</TL>
|
<TL>No message sent yet</TL>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@(lastMessage.Message)
|
@(chat.Value.Content)
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
@@ -66,7 +60,7 @@
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
<TL>No support ticket is currently open</TL>
|
<TL>No support chat is currently open</TL>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -80,53 +74,28 @@
|
|||||||
|
|
||||||
@code
|
@code
|
||||||
{
|
{
|
||||||
private User[] Users;
|
|
||||||
private Dictionary<User, SupportMessage?> MessageCache;
|
|
||||||
|
|
||||||
private LazyLoader? LazyLoader;
|
private LazyLoader? LazyLoader;
|
||||||
|
private Dictionary<User, SupportChatMessage?> OpenChats = new();
|
||||||
|
|
||||||
protected override void OnInitialized()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
MessageCache = new();
|
await Event.On<User>("supportChat.new", this, async user =>
|
||||||
|
|
||||||
MessageService.Subscribe<Index, User>("support.new", this, async user =>
|
|
||||||
{
|
{
|
||||||
if (LazyLoader != null)
|
//TODO: Play sound or smth. Add a config option
|
||||||
await LazyLoader.Reload();
|
|
||||||
});
|
OpenChats = await ServerService.GetOpenChats();
|
||||||
|
|
||||||
MessageService.Subscribe<Index, User>("support.close", this, async user =>
|
await InvokeAsync(StateHasChanged);
|
||||||
{
|
|
||||||
if (LazyLoader != null)
|
|
||||||
await LazyLoader.Reload();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task Load(LazyLoader arg)
|
private async Task Load(LazyLoader arg) // Only for initial load
|
||||||
{
|
{
|
||||||
// We dont want cache here
|
OpenChats = await ServerService.GetOpenChats();
|
||||||
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()
|
public async void Dispose()
|
||||||
{
|
{
|
||||||
MessageService.Unsubscribe("support.new", this);
|
await Event.Off("supportChat.new", this);
|
||||||
MessageService.Unsubscribe("support.close", this);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
@page "/admin/support/view/{Id:int}"
|
@page "/admin/support/view/{Id:int}"
|
||||||
@using Moonlight.App.Services.Support
|
|
||||||
@using Moonlight.App.Database.Entities
|
@using Moonlight.App.Database.Entities
|
||||||
@using Moonlight.App.Helpers
|
@using Moonlight.App.Helpers
|
||||||
@using Moonlight.App.Repositories
|
@using Moonlight.App.Repositories
|
||||||
@using Moonlight.App.Services
|
@using Moonlight.App.Services
|
||||||
|
@using Moonlight.App.Services.SupportChat
|
||||||
|
|
||||||
@inject SupportAdminService SupportAdminService
|
@inject SupportChatAdminService AdminService
|
||||||
@inject UserRepository UserRepository
|
@inject UserRepository UserRepository
|
||||||
@inject SmartTranslateService SmartTranslateService
|
@inject SmartTranslateService SmartTranslateService
|
||||||
@inject ResourceService ResourceService
|
@inject ResourceService ResourceService
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
<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)
|
||||||
{
|
{
|
||||||
if (message.IsSystem || message.IsSupport)
|
if (message.Sender == null || message.Sender.Id != User.Id)
|
||||||
{
|
{
|
||||||
<div class="d-flex justify-content-end mb-10 ">
|
<div class="d-flex justify-content-end mb-10 ">
|
||||||
<div class="d-flex flex-column align-items-end">
|
<div class="d-flex flex-column align-items-end">
|
||||||
@@ -37,29 +37,29 @@
|
|||||||
<div class="me-3">
|
<div class="me-3">
|
||||||
<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>
|
||||||
<a class="fs-5 fw-bold text-gray-900 text-hover-primary ms-1">
|
<a class="fs-5 fw-bold text-gray-900 text-hover-primary ms-1">
|
||||||
@if (message.IsSupport && !message.IsSystem)
|
@if (message.Sender != null)
|
||||||
{
|
{
|
||||||
<span>@(message.Sender!.FirstName) @(message.Sender!.LastName)</span>
|
<span>@(message.Sender.FirstName) @(message.Sender.LastName)</span>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<span>System</span>
|
<span><TL>System</TL></span>
|
||||||
}
|
}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="symbol symbol-35px symbol-circle ">
|
<div class="symbol symbol-35px symbol-circle ">
|
||||||
<img alt="Logo" src="@(ResourceService.Image("logo.svg"))">
|
<img alt="Logo" src="@(ResourceService.Image("logo.svg"))">
|
||||||
</div>
|
</div>
|
||||||
</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">
|
||||||
@if (message.IsSystem)
|
@if (message.Sender == null)
|
||||||
{
|
{
|
||||||
<TL>@(message.Message)</TL>
|
<TL>@(message.Content)</TL>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@(message.Message)
|
@(message.Content)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -75,14 +75,14 @@
|
|||||||
</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>@(message.Sender!.FirstName) @(message.Sender!.LastName)</span>
|
<span>@(message.Sender.FirstName) @(message.Sender.LastName)</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>
|
||||||
</div>
|
</div>
|
||||||
</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.Message)
|
@(message.Content)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -92,11 +92,7 @@
|
|||||||
</LazyLoader>
|
</LazyLoader>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
@{
|
@if (Typing.Any())
|
||||||
var typingUsers = SupportAdminService.GetTypingUsers();
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (typingUsers.Any())
|
|
||||||
{
|
{
|
||||||
<span class="mb-5 fs-5 d-flex flex-row">
|
<span class="mb-5 fs-5 d-flex flex-row">
|
||||||
<div class="wave me-1">
|
<div class="wave me-1">
|
||||||
@@ -104,16 +100,16 @@
|
|||||||
<div class="dot h-5px w-5px" style="margin-right: 1px;"></div>
|
<div class="dot h-5px w-5px" style="margin-right: 1px;"></div>
|
||||||
<div class="dot h-5px w-5px"></div>
|
<div class="dot h-5px w-5px"></div>
|
||||||
</div>
|
</div>
|
||||||
@if (typingUsers.Length > 1)
|
@if (Typing.Length > 1)
|
||||||
{
|
{
|
||||||
<span>
|
<span>
|
||||||
@(typingUsers.Aggregate((current, next) => current + ", " + next)) <TL>are typing</TL>
|
@(Typing.Aggregate((current, next) => current + ", " + next)) <TL>are typing</TL>
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<span>
|
<span>
|
||||||
@(typingUsers.First()) <TL>is typing</TL>
|
@(Typing.First()) <TL>is typing</TL>
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
@@ -157,7 +153,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="d-flex align-items-center mb-2">
|
<div class="d-flex align-items-center mb-2">
|
||||||
<span class="fw-semibold text-gray-800 fs-5 m-0">
|
<span class="fw-semibold text-gray-800 fs-5 m-0">
|
||||||
<TL>Email</TL>: <a href="/admin/users/view/@User.Id">@(User.Email)</a>
|
<TL>Email</TL>: <a href="/admin/users/view/@User.Id">@(User.Email)</a>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="align-items-center mt-3">
|
<div class="align-items-center mt-3">
|
||||||
@@ -183,9 +179,11 @@
|
|||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
|
||||||
private User? User;
|
private User? User;
|
||||||
private SupportMessage[] Messages;
|
|
||||||
private string Content = "";
|
|
||||||
|
|
||||||
|
private SupportChatMessage[] Messages = Array.Empty<SupportChatMessage>();
|
||||||
|
private string[] Typing = Array.Empty<string>();
|
||||||
|
|
||||||
|
private string Content = "";
|
||||||
private DateTime LastTypingTimestamp = DateTime.UtcNow.AddMinutes(-10);
|
private DateTime LastTypingTimestamp = DateTime.UtcNow.AddMinutes(-10);
|
||||||
|
|
||||||
private async Task Load(LazyLoader arg)
|
private async Task Load(LazyLoader arg)
|
||||||
@@ -196,55 +194,54 @@
|
|||||||
|
|
||||||
if (User != null)
|
if (User != null)
|
||||||
{
|
{
|
||||||
SupportAdminService.OnNewMessage += OnNewMessage;
|
AdminService.OnMessage += OnMessage;
|
||||||
SupportAdminService.OnUpdateTyping += OnUpdateTyping;
|
AdminService.OnTypingChanged += OnTypingChanged;
|
||||||
|
|
||||||
await SupportAdminService.Start(User);
|
await AdminService.Start(User);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Message handling
|
private async Task LoadMessages(LazyLoader arg)
|
||||||
|
|
||||||
private async void OnNewMessage(object? sender, SupportMessage e)
|
|
||||||
{
|
{
|
||||||
Messages = (await SupportAdminService.GetMessages()).Reverse().ToArray();
|
Messages = await AdminService.GetMessages();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnTypingChanged(string[] typing)
|
||||||
|
{
|
||||||
|
Typing = typing;
|
||||||
|
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadMessages(LazyLoader arg)
|
private async Task OnMessage(SupportChatMessage arg)
|
||||||
{
|
{
|
||||||
Messages = (await SupportAdminService.GetMessages()).Reverse().ToArray();
|
Messages = await AdminService.GetMessages();
|
||||||
|
|
||||||
|
//TODO: Sound when message from system or admin
|
||||||
|
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
private async Task Send()
|
private async Task Send()
|
||||||
{
|
{
|
||||||
await SupportAdminService.SendMessage(Content);
|
await AdminService.SendMessage(Content);
|
||||||
Content = "";
|
Content = "";
|
||||||
|
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CloseTicket()
|
private async Task CloseTicket()
|
||||||
{
|
{
|
||||||
await SupportAdminService.Close();
|
await AdminService.Close();
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Typing
|
|
||||||
|
|
||||||
private async Task OnTyping()
|
private async Task OnTyping()
|
||||||
{
|
{
|
||||||
if ((DateTime.UtcNow - LastTypingTimestamp).TotalSeconds > 5)
|
if ((DateTime.UtcNow - LastTypingTimestamp).TotalSeconds > 5)
|
||||||
{
|
{
|
||||||
LastTypingTimestamp = DateTime.UtcNow;
|
LastTypingTimestamp = DateTime.UtcNow;
|
||||||
|
|
||||||
await SupportAdminService.TriggerTyping();
|
await AdminService.SendTyping();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnUpdateTyping(object? sender, EventArgs e)
|
|
||||||
{
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
}
|
||||||
@@ -2,10 +2,10 @@
|
|||||||
@using Moonlight.App.Services
|
@using Moonlight.App.Services
|
||||||
@using Moonlight.App.Database.Entities
|
@using Moonlight.App.Database.Entities
|
||||||
@using Moonlight.App.Helpers
|
@using Moonlight.App.Helpers
|
||||||
@using Moonlight.App.Services.Support
|
@using Moonlight.App.Services.SupportChat
|
||||||
|
|
||||||
@inject ResourceService ResourceService
|
@inject ResourceService ResourceService
|
||||||
@inject SupportClientService SupportClientService
|
@inject SupportChatClientService ClientService
|
||||||
@inject SmartTranslateService SmartTranslateService
|
@inject SmartTranslateService SmartTranslateService
|
||||||
|
|
||||||
<LazyLoader Load="Load">
|
<LazyLoader Load="Load">
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
<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)
|
||||||
{
|
{
|
||||||
if (message.IsSystem || message.IsSupport)
|
if (message.Sender == null || message.Sender.Id != User.Id)
|
||||||
{
|
{
|
||||||
<div class="d-flex justify-content-start mb-10 ">
|
<div class="d-flex justify-content-start mb-10 ">
|
||||||
<div class="d-flex flex-column align-items-start">
|
<div class="d-flex flex-column align-items-start">
|
||||||
@@ -26,13 +26,13 @@
|
|||||||
</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">
|
||||||
@if (message.IsSupport && !message.IsSystem)
|
@if (message.Sender != null)
|
||||||
{
|
{
|
||||||
<span>@(message.Sender!.FirstName) @(message.Sender!.LastName)</span>
|
<span>@(message.Sender.FirstName) @(message.Sender.LastName)</span>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<span>System</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>
|
||||||
@@ -40,13 +40,13 @@
|
|||||||
</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">
|
||||||
@if (message.IsSystem)
|
@if (message.Sender == null)
|
||||||
{
|
{
|
||||||
<TL>@(message.Message)</TL>
|
<TL>@(message.Content)</TL>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@(message.Message)
|
@(message.Content)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
<div class="me-3">
|
<div class="me-3">
|
||||||
<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>
|
||||||
<a class="fs-5 fw-bold text-gray-900 text-hover-primary ms-1">
|
<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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="symbol symbol-35px symbol-circle ">
|
<div class="symbol symbol-35px symbol-circle ">
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
</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.Message)
|
@(message.Content)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
</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>System</span>
|
<span><TL>System</TL></span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -97,11 +97,7 @@
|
|||||||
</LazyLoader>
|
</LazyLoader>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
@{
|
@if (Typing.Any())
|
||||||
var typingUsers = SupportClientService.GetTypingUsers();
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (typingUsers.Any())
|
|
||||||
{
|
{
|
||||||
<span class="mb-5 fs-5 d-flex flex-row">
|
<span class="mb-5 fs-5 d-flex flex-row">
|
||||||
<div class="wave me-1">
|
<div class="wave me-1">
|
||||||
@@ -109,13 +105,13 @@
|
|||||||
<div class="dot h-5px w-5px" style="margin-right: 1px;"></div>
|
<div class="dot h-5px w-5px" style="margin-right: 1px;"></div>
|
||||||
<div class="dot h-5px w-5px"></div>
|
<div class="dot h-5px w-5px"></div>
|
||||||
</div>
|
</div>
|
||||||
@if (typingUsers.Length > 1)
|
@if (Typing.Length > 1)
|
||||||
{
|
{
|
||||||
<span>@(typingUsers.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>@(typingUsers.First()) <TL>is typing</TL></span>
|
<span>@(Typing.First()) <TL>is typing</TL></span>
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
@@ -149,59 +145,60 @@
|
|||||||
|
|
||||||
@code
|
@code
|
||||||
{
|
{
|
||||||
private SupportMessage[] Messages;
|
[CascadingParameter]
|
||||||
private string Content = "";
|
public User User { get; set; }
|
||||||
|
|
||||||
|
private SupportChatMessage[] Messages = Array.Empty<SupportChatMessage>();
|
||||||
|
private string[] Typing = Array.Empty<string>();
|
||||||
|
|
||||||
|
private string Content = "";
|
||||||
private DateTime LastTypingTimestamp = DateTime.UtcNow.AddMinutes(-10);
|
private DateTime LastTypingTimestamp = DateTime.UtcNow.AddMinutes(-10);
|
||||||
|
|
||||||
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.OnTypingChanged += OnTypingChanged;
|
||||||
|
|
||||||
SupportClientService.OnNewMessage += OnNewMessage;
|
await ClientService.Start();
|
||||||
SupportClientService.OnUpdateTyping += OnUpdateTyping;
|
}
|
||||||
|
|
||||||
|
private async Task LoadMessages(LazyLoader arg)
|
||||||
|
{
|
||||||
|
Messages = await ClientService.GetMessages();
|
||||||
|
}
|
||||||
|
|
||||||
await SupportClientService.Start();
|
private async Task OnTypingChanged(string[] typing)
|
||||||
|
{
|
||||||
|
Typing = typing;
|
||||||
|
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnMessage(SupportChatMessage message)
|
||||||
|
{
|
||||||
|
Messages = await ClientService.GetMessages();
|
||||||
|
|
||||||
|
//TODO: Sound when message from system or admin
|
||||||
|
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task Send()
|
private async Task Send()
|
||||||
{
|
{
|
||||||
await SupportClientService.SendMessage(Content);
|
await ClientService.SendMessage(Content);
|
||||||
Content = "";
|
Content = "";
|
||||||
|
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Message handling
|
|
||||||
|
|
||||||
private async void OnNewMessage(object? sender, SupportMessage e)
|
|
||||||
{
|
|
||||||
Messages = (await SupportClientService.GetMessages()).Reverse().ToArray();
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task LoadMessages(LazyLoader arg)
|
|
||||||
{
|
|
||||||
Messages = (await SupportClientService.GetMessages()).Reverse().ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Typing
|
|
||||||
|
|
||||||
private async void OnUpdateTyping(object? sender, EventArgs e)
|
|
||||||
{
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void OnTyping()
|
private async void OnTyping()
|
||||||
{
|
{
|
||||||
if ((DateTime.UtcNow - LastTypingTimestamp).TotalSeconds > 5)
|
if ((DateTime.UtcNow - LastTypingTimestamp).TotalSeconds > 5)
|
||||||
{
|
{
|
||||||
LastTypingTimestamp = DateTime.UtcNow;
|
LastTypingTimestamp = DateTime.UtcNow;
|
||||||
|
await ClientService.SendTyping();
|
||||||
await SupportClientService.TriggerTyping();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user