diff --git a/WowsKarma.Api/Services/NotificationService.cs b/WowsKarma.Api/Services/NotificationService.cs index 786080c4..dec9b6dd 100644 --- a/WowsKarma.Api/Services/NotificationService.cs +++ b/WowsKarma.Api/Services/NotificationService.cs @@ -1,6 +1,9 @@ -using Microsoft.AspNetCore.SignalR; +using System.Runtime.CompilerServices; +using JetBrains.Annotations; +using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Query; using Microsoft.Extensions.Logging; using WowsKarma.Api.Data; using WowsKarma.Api.Data.Models.Notifications; @@ -94,23 +97,41 @@ public static class NotificationServiceExtensions public static IQueryable IncludeAllNotificationsChildNavs(this IQueryable query) { //PlatformBanNotification - query = query.Include(n => (n as PlatformBanNotification).Ban); + query = query.Include(static n => (n as PlatformBanNotification).Ban); + // PostAddedNotification - query = query.Include(n => (n as PostAddedNotification).Post); + query = query.IncludeAllPostNotificationsChildNavs(); //PostEditedNotification - query = query.Include(n => (n as PostEditedNotification).Post); + query = query.IncludeAllPostNotificationsChildNavs(); //PostDeletedNotification - query = query.Include(n => (n as PostDeletedNotification).Post); + query = query.IncludeAllPostNotificationsChildNavs(); // PostModEditedNotification - query = query.Include(n => (n as PostModEditedNotification).ModAction); - + query = query.Include(static n => (n as PostModEditedNotification).ModAction); + // PostModDeletedNotification - query = query.Include(n => (n as PostModDeletedNotification).ModAction); + query = query.Include(static n => (n as PostModDeletedNotification).ModAction); return query; } + + /// + /// Returns a queryable of type that includes all child navigation properties. + /// + /// An included queryable of type that includes all child navigation properties. + [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IQueryable IncludeAllPostNotificationsChildNavs(this IQueryable query) + where TNotification : PostNotificationBase + { + query = query.Include(static n => (n as TNotification).Post) + .ThenInclude(static p => p.Author); + + query = query.Include(static n => (n as TNotification).Post) + .ThenInclude(static p => p.Player); + + return query; + } } diff --git a/WowsKarma.Api/Startup.cs b/WowsKarma.Api/Startup.cs index d9f8cfbe..f663da37 100644 --- a/WowsKarma.Api/Startup.cs +++ b/WowsKarma.Api/Startup.cs @@ -211,7 +211,8 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton(s => new PublicApiOptions { - AppId = s.GetRequiredService()[$"Api:{ApiRegion.ToRegionString()}:AppId"] + AppId = s.GetRequiredService()[$"Api:{ApiRegion.ToRegionString()}:AppId"] + ?? throw new InvalidOperationException("AppId not found in configuration"), }); services.AddWowsReplayUnpacker(builder => diff --git a/WowsKarma.Api/WowsKarma.Api.csproj b/WowsKarma.Api/WowsKarma.Api.csproj index 26a6a897..e4eb9872 100644 --- a/WowsKarma.Api/WowsKarma.Api.csproj +++ b/WowsKarma.Api/WowsKarma.Api.csproj @@ -3,7 +3,7 @@ net7.0 preview - 0.16.3 + 0.16.3.1 Sakura Akeno Isayeki Nodsoft Systems WOWS Karma (API) @@ -33,41 +33,38 @@ - - - + + - + - + - - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - + + + + + - + diff --git a/wowskarma.app/src/app/services/api/models/notification.ts b/wowskarma.app/src/app/services/api/models/notification.ts index fd188eec..f093d8bf 100644 --- a/wowskarma.app/src/app/services/api/models/notification.ts +++ b/wowskarma.app/src/app/services/api/models/notification.ts @@ -23,4 +23,5 @@ export enum NotificationType { export type PostNotification = Notification & { postId: string, post: PlayerPostDto } export type PostModDeletedNotification = Notification & { modActionId: string, modAction: PostModActionDto } -export type PlatformBanNotification = Notification & { reason: string, until?: Date } \ No newline at end of file +export type PostModEditedNotification = Notification & { modActionId: string, modAction: PostModActionDto } +export type PlatformBanNotification = Notification & { reason: string, until?: Date } diff --git a/wowskarma.app/src/app/shared/notifications/notification/notification.component.ts b/wowskarma.app/src/app/shared/notifications/notification/notification.component.ts index a03d4c11..0cdd4172 100644 --- a/wowskarma.app/src/app/shared/notifications/notification/notification.component.ts +++ b/wowskarma.app/src/app/shared/notifications/notification/notification.component.ts @@ -1,90 +1,193 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, TemplateRef, ViewChild } from "@angular/core"; -import { Notification, NotificationType, PlatformBanNotification, PostModDeletedNotification, PostNotification } from "../../../services/api/models/notification"; +import {ChangeDetectionStrategy, Component, EventEmitter, Input, Output, TemplateRef, ViewChild} from "@angular/core"; +import { + Notification, + NotificationType, + PlatformBanNotification, + PostModDeletedNotification, + PostModEditedNotification, + PostNotification +} from "../../../services/api/models/notification"; +import {PlayerPostDto} from "../../../services/api/models/player-post-dto"; +import {PostModActionDto} from "../../../services/api/models/post-mod-action-dto"; @Component({ - selector: "app-notification", - styleUrls: ["./notification.component.scss"], - templateUrl: "./notification.component.html", - changeDetection: ChangeDetectionStrategy.OnPush, + selector: "app-notification", + styleUrls: ["./notification.component.scss"], + templateUrl: "./notification.component.html", + changeDetection: ChangeDetectionStrategy.OnPush, }) export class NotificationComponent { - @Input() notification!: Notification; - notificationContent!: { title: string, body: string, link?: string, critical?: boolean }; + @Input() notification!: Notification; + notificationContent!: { title: string, body: string, link?: string, critical?: boolean }; - @Output() onClick = new EventEmitter(); - @Output() onDismiss = new EventEmitter(); + @Output() onClick = new EventEmitter(); + @Output() onDismiss = new EventEmitter(); - _onClick(e: Event) { - e.stopPropagation() - this.onClick.emit(); - } + _onClick(e: Event) { + e.stopPropagation() + this.onClick.emit(); + } + + _onDismiss(e: Event) { + e.stopPropagation(); + e.preventDefault(); + this.onDismiss.emit(); + } + + constructor() { + } + + buildNotificationContent(notification: Notification): { title: string, body: string, link?: string, critical?: boolean } { + switch (notification.type) { + case NotificationType.postAdded: + const postNotification = notification as PostNotification; + + return { + title: "Post added", + body: ` +

A new post has been added by ${postNotification.post.author?.username ?? "unknown"} to your profile.

+ + ${generateTable([ + ...fromPostInfo(postNotification.post, ["title", "content", "createdAt"]) + ] + )}`, + link: `/posts/${postNotification.postId}`, + }; + + case NotificationType.postEdited: + const postEditedNotification = notification as PostNotification; + + return { + title: "Post edited", + body: ` +

A post by ${postEditedNotification.post.author?.username ?? "unknown"} has been edited on your profile.

+ + ${generateTable([ + ...fromPostInfo(postEditedNotification.post, ["title", "content", "updatedAt"]) + ] + )}`, + link: `/posts/${postEditedNotification.postId}`, + }; + + case NotificationType.postDeleted: + const postDeletedNotification = notification as PostNotification; + + return { + title: "Post deleted", + body: ` +

A post by ${postDeletedNotification.post.author?.username ?? "unknown"} has been deleted from your profile.

+ + ${generateTable([ + ...fromPostInfo(postDeletedNotification.post, []) + ] + )}`, + link: `/posts/${postDeletedNotification.postId}`, + }; + + case NotificationType.postModEdited: + const postModEditedNotification = notification as PostModEditedNotification; + return { + title: "Post mod edited", + body: ` +

Your post has been edited by our Community Managers, and is locked from further editing.

+ ${generateTable([ + ["Post ID", `${postModEditedNotification.modAction.postId}`], + ...fromModActionInfo(postModEditedNotification.modAction, ["reason"]) + ]) + }`, + link: `/posts/${postModEditedNotification.modAction.postId}`, + critical: true, + }; + + case NotificationType.postModDeleted: + const postModDeletedNotification = notification as PostModDeletedNotification; + + return { + title: "Post mod deleted", + body: ` +

Your post was removed by our Community Managers, and is no longer visible on the platform.

+ ${generateTable([ + ["Post ID", `${postModDeletedNotification.modAction.postId}`], + ...fromModActionInfo(postModDeletedNotification.modAction, ["reason"]) + ]) + }`, + link: `/posts/${postModDeletedNotification.modAction.postId}`, + critical: true, + }; + + case NotificationType.platformBan: + const banNotification = notification as PlatformBanNotification; - _onDismiss(e: Event) { - e.stopPropagation(); - e.preventDefault(); - this.onDismiss.emit(); + return { + title: "Platform ban", + body: ` +

You have been banned from the platform ${banNotification.until ? `until ${banNotification.until.toLocaleDateString()}` : "permanently"}.

+

Reason: ${banNotification.reason}

`, + critical: true, + }; + + default: + return { + title: "Unknown notification", + body: "", + }; } + } +} + +type tableRow = string | [string, string]; + +function generateTable(rows: tableRow[]): string { + return ` + + + ${rows.map(row => { + if (typeof row === "string") { + return `${row}`; + } else { + return ``; + } + }).join("")} + +
${row[0]}${row[1]}
+ `; +} + +function fromPostInfo(post: PlayerPostDto, includeFields: (keyof PlayerPostDto)[]): tableRow[] { + let rows: tableRow[] = [`Post ID${post.id}`]; + + // Loop through the fields and add them programmatically to the table + for (const field of includeFields) { + // Display the field name as the header (from camelCase to Title Case). + const fieldName = field.replace(/([A-Z])/g, " $1").replace(/^./, str => str.toUpperCase()).toString(); + + // ...and the value as the body (formatted accordingly to type). + const value = post[field]; - constructor() {} - - buildNotificationContent(notification: Notification): { title: string, body: string, link?: string, critical?: boolean } { - switch (notification.type) { - case NotificationType.postAdded: - const postNotification = notification as PostNotification; - return { - title: "Post added", - body: ` -

A new post has been added by ${postNotification.post.author?.username ?? "unknown"} to your profile.

-

Post ID: ${postNotification.postId}

`, - link: `/posts/${postNotification.postId}`, - }; - - case NotificationType.postEdited: - const postEditedNotification = notification as PostNotification; - return { - title: "Post edited", - body: ` -

A post by ${postEditedNotification.post.author?.username ?? "unknown"} has been edited on your profile.

-

Post ID: ${postEditedNotification.postId}

`, - link: `/posts/${postEditedNotification.postId}`, - }; - - case NotificationType.postDeleted: - const postDeletedNotification = notification as PostNotification; - return { - title: "Post deleted", - body: ` -

A post by ${postDeletedNotification.post.author?.username ?? "unknown"} has been deleted from your profile.

-

Post ID: ${postDeletedNotification.postId}

`, - link: `/posts/${postDeletedNotification.postId}`, - }; - - case NotificationType.postModDeleted: - const postModDeletedNotification = notification as PostModDeletedNotification; - return { - title: "Post mod deleted", - body: ` -

Your post was removed by our Community Managers, and is no longer visible on the platform.

-

Post ID: ${postModDeletedNotification.modAction.postId}

`, - link: `/posts/${postModDeletedNotification.modAction.postId}`, - critical: true, - }; - - case NotificationType.platformBan: - const banNotification = notification as PlatformBanNotification; - return { - title: "Platform ban", - body: ` -

You have been banned from the platform ${banNotification.until ? `until ${banNotification.until.toLocaleDateString()}` : "permanently"}.

-

Reason: ${banNotification.reason}

`, - critical: true, - }; - - default: - return { - title: "Unknown notification", - body: "", - }; - } + if (field === "createdAt" || field === "updatedAt") { + // Format the date to a more readable format + rows.push([fieldName, new Date(value as string).toLocaleString()]); + } else { + rows.push([fieldName, value?.toLocaleString() ?? "N/A"]); } + } + + return rows; +} + +function fromModActionInfo(modAction: PostModActionDto, includeFields: (keyof PostModActionDto)[]): tableRow[] { + let rows: tableRow[] = [`Mod Action ID${modAction.id}`]; + + // Loop through the fields and add them programmatically to the table + for (const field of includeFields) { + // Display the field name as the header (from camelCase to Title Case). + const fieldName = field.replace(/([A-Z])/g, " $1").replace(/^./, str => str.toUpperCase()).toString(); + + // ...and the value as the body (formatted accordingly to type). + const value = modAction[field]; + + rows.push([fieldName, value?.toLocaleString() ?? "N/A"]); + } + + return rows; }