diff --git a/Application/Alerts/AlertExtensions.cs b/Application/Alerts/AlertExtensions.cs new file mode 100644 index 000000000..b90db8263 --- /dev/null +++ b/Application/Alerts/AlertExtensions.cs @@ -0,0 +1,55 @@ +using System; +using SharedLibraryCore; +using SharedLibraryCore.Alerts; +using SharedLibraryCore.Database.Models; + +namespace IW4MAdmin.Application.Alerts; + +public static class AlertExtensions +{ + public static Alert.AlertState BuildAlert(this EFClient client, Alert.AlertCategory? type = null) + { + return new Alert.AlertState + { + RecipientId = client.ClientId, + Category = type ?? Alert.AlertCategory.Information + }; + } + + public static Alert.AlertState WithCategory(this Alert.AlertState state, Alert.AlertCategory category) + { + state.Category = category; + return state; + } + + public static Alert.AlertState OfType(this Alert.AlertState state, string type) + { + state.Type = type; + return state; + } + + public static Alert.AlertState WithMessage(this Alert.AlertState state, string message) + { + state.Message = message; + return state; + } + + public static Alert.AlertState ExpiresIn(this Alert.AlertState state, TimeSpan expiration) + { + state.ExpiresAt = DateTime.Now.Add(expiration); + return state; + } + + public static Alert.AlertState FromSource(this Alert.AlertState state, string source) + { + state.Source = source; + return state; + } + + public static Alert.AlertState FromClient(this Alert.AlertState state, EFClient client) + { + state.Source = client.Name.StripColors(); + state.SourceId = client.ClientId; + return state; + } +} diff --git a/Application/Alerts/AlertManager.cs b/Application/Alerts/AlertManager.cs new file mode 100644 index 000000000..cca38616f --- /dev/null +++ b/Application/Alerts/AlertManager.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using SharedLibraryCore.Alerts; +using SharedLibraryCore.Configuration; +using SharedLibraryCore.Database.Models; +using SharedLibraryCore.Interfaces; + +namespace IW4MAdmin.Application.Alerts; + +public class AlertManager : IAlertManager +{ + private readonly ApplicationConfiguration _appConfig; + private readonly ConcurrentDictionary> _states = new(); + private readonly List>>> _staticSources = new(); + + public AlertManager(ApplicationConfiguration appConfig) + { + _appConfig = appConfig; + _states.TryAdd(0, new List()); + } + + public EventHandler OnAlertConsumed { get; set; } + + public async Task Initialize() + { + foreach (var source in _staticSources) + { + var alerts = await source(); + foreach (var alert in alerts) + { + AddAlert(alert); + } + } + } + + public IEnumerable RetrieveAlerts(EFClient client) + { + lock (_states) + { + var alerts = Enumerable.Empty(); + if (client.Level > Data.Models.Client.EFClient.Permission.Trusted) + { + alerts = alerts.Concat(_states[0].Where(alert => + alert.MinimumPermission is null || alert.MinimumPermission <= client.Level)); + } + + if (_states.ContainsKey(client.ClientId)) + { + alerts = alerts.Concat(_states[client.ClientId].AsReadOnly()); + } + + return alerts.OrderByDescending(alert => alert.OccuredAt); + } + } + + public void MarkAlertAsRead(Guid alertId) + { + lock (_states) + { + foreach (var items in _states.Values) + { + var matchingEvent = items.FirstOrDefault(item => item.AlertId == alertId); + + if (matchingEvent is null) + { + continue; + } + + items.Remove(matchingEvent); + OnAlertConsumed?.Invoke(this, matchingEvent); + } + } + } + + public void MarkAllAlertsAsRead(int recipientId) + { + lock (_states) + { + foreach (var items in _states.Values) + { + items.RemoveAll(item => + { + if (item.RecipientId != null && item.RecipientId != recipientId) + { + return false; + } + + OnAlertConsumed?.Invoke(this, item); + return true; + }); + } + } + } + + public void AddAlert(Alert.AlertState alert) + { + lock (_states) + { + if (alert.RecipientId is null) + { + _states[0].Add(alert); + return; + } + + if (!_states.ContainsKey(alert.RecipientId.Value)) + { + _states[alert.RecipientId.Value] = new List(); + } + + if (_appConfig.MinimumAlertPermissions.ContainsKey(alert.Type)) + { + alert.MinimumPermission = _appConfig.MinimumAlertPermissions[alert.Type]; + } + + _states[alert.RecipientId.Value].Add(alert); + + PruneOldAlerts(); + } + } + + public void RegisterStaticAlertSource(Func>> alertSource) + { + _staticSources.Add(alertSource); + } + + + private void PruneOldAlerts() + { + foreach (var value in _states.Values) + { + value.RemoveAll(item => item.ExpiresAt < DateTime.UtcNow); + } + } +} diff --git a/Application/ApplicationManager.cs b/Application/ApplicationManager.cs index 6c17f6b66..12f82c7f8 100644 --- a/Application/ApplicationManager.cs +++ b/Application/ApplicationManager.cs @@ -57,6 +57,7 @@ namespace IW4MAdmin.Application private readonly List MessageTokens; private readonly ClientService ClientSvc; readonly PenaltyService PenaltySvc; + private readonly IAlertManager _alertManager; public IConfigurationHandler ConfigHandler; readonly IPageList PageList; private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0); @@ -82,13 +83,14 @@ namespace IW4MAdmin.Application IEnumerable plugins, IParserRegexFactory parserRegexFactory, IEnumerable customParserEvents, IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory, IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver, ClientService clientService, IServiceProvider serviceProvider, - ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService) + ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService, IAlertManager alertManager) { MiddlewareActionHandler = actionHandler; _servers = new ConcurrentBag(); MessageTokens = new List(); ClientSvc = clientService; PenaltySvc = penaltyService; + _alertManager = alertManager; ConfigHandler = appConfigHandler; StartTime = DateTime.UtcNow; PageList = new PageList(); @@ -508,6 +510,7 @@ namespace IW4MAdmin.Application #endregion _metaRegistration.Register(); + await _alertManager.Initialize(); #region CUSTOM_EVENTS foreach (var customEvent in _customParserEvents.SelectMany(_events => _events.Events)) @@ -697,5 +700,6 @@ namespace IW4MAdmin.Application } public void RemoveCommandByName(string commandName) => _commands.RemoveAll(_command => _command.Name == commandName); + public IAlertManager AlertManager => _alertManager; } } diff --git a/Application/Commands/OfflineMessageCommand.cs b/Application/Commands/OfflineMessageCommand.cs index f2ab7284f..d615c8c2c 100644 --- a/Application/Commands/OfflineMessageCommand.cs +++ b/Application/Commands/OfflineMessageCommand.cs @@ -1,11 +1,14 @@ using System; +using System.Linq; using System.Threading.Tasks; using Data.Abstractions; using Data.Models.Client; using Data.Models.Misc; +using IW4MAdmin.Application.Alerts; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using SharedLibraryCore; +using SharedLibraryCore.Alerts; using SharedLibraryCore.Configuration; using SharedLibraryCore.Interfaces; using ILogger = Microsoft.Extensions.Logging.ILogger; @@ -16,19 +19,66 @@ namespace IW4MAdmin.Application.Commands { private readonly IDatabaseContextFactory _contextFactory; private readonly ILogger _logger; + private readonly IAlertManager _alertManager; private const short MaxLength = 1024; - + public OfflineMessageCommand(CommandConfiguration config, ITranslationLookup layout, - IDatabaseContextFactory contextFactory, ILogger logger) : base(config, layout) + IDatabaseContextFactory contextFactory, ILogger logger, IAlertManager alertManager) + : base(config, layout) { Name = "offlinemessage"; Description = _translationLookup["COMMANDS_OFFLINE_MESSAGE_DESC"]; Alias = "om"; Permission = EFClient.Permission.Moderator; RequiresTarget = true; - + _contextFactory = contextFactory; _logger = logger; + _alertManager = alertManager; + + _alertManager.RegisterStaticAlertSource(async () => + { + var context = contextFactory.CreateContext(false); + return await context.InboxMessages.Where(message => !message.IsDelivered) + .Where(message => message.CreatedDateTime >= DateTime.UtcNow.AddDays(-7)) + .Where(message => message.DestinationClient.Level > EFClient.Permission.User) + .Select(message => new Alert.AlertState + { + OccuredAt = message.CreatedDateTime, + Message = message.Message, + ExpiresAt = DateTime.UtcNow.AddDays(7), + Category = Alert.AlertCategory.Message, + Source = message.SourceClient.CurrentAlias.Name.StripColors(), + SourceId = message.SourceClientId, + RecipientId = message.DestinationClientId, + ReferenceId = message.InboxMessageId, + Type = nameof(EFInboxMessage) + }).ToListAsync(); + }); + + _alertManager.OnAlertConsumed += (_, state) => + { + if (state.Category != Alert.AlertCategory.Message || state.ReferenceId is null) + { + return; + } + + try + { + var context = contextFactory.CreateContext(true); + foreach (var message in context.InboxMessages + .Where(message => message.InboxMessageId == state.ReferenceId.Value).ToList()) + { + message.IsDelivered = true; + } + + context.SaveChanges(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Could not update message state for alert {@Alert}", state); + } + }; } public override async Task ExecuteAsync(GameEvent gameEvent) @@ -38,23 +88,24 @@ namespace IW4MAdmin.Application.Commands gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_TOO_LONG"].FormatExt(MaxLength)); return; } - + if (gameEvent.Target.ClientId == gameEvent.Origin.ClientId) { gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_SELF"].FormatExt(MaxLength)); return; } - + if (gameEvent.Target.IsIngame) { - gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_INGAME"].FormatExt(gameEvent.Target.Name)); + gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_INGAME"] + .FormatExt(gameEvent.Target.Name)); return; } - + await using var context = _contextFactory.CreateContext(enableTracking: false); var server = await context.Servers.FirstAsync(srv => srv.EndPoint == gameEvent.Owner.ToString()); - var newMessage = new EFInboxMessage() + var newMessage = new EFInboxMessage { SourceClientId = gameEvent.Origin.ClientId, DestinationClientId = gameEvent.Target.ClientId, @@ -62,6 +113,12 @@ namespace IW4MAdmin.Application.Commands Message = gameEvent.Data, }; + _alertManager.AddAlert(gameEvent.Target.BuildAlert(Alert.AlertCategory.Message) + .WithMessage(gameEvent.Data.Trim()) + .FromClient(gameEvent.Origin) + .OfType(nameof(EFInboxMessage)) + .ExpiresIn(TimeSpan.FromDays(7))); + try { context.Set().Add(newMessage); @@ -75,4 +132,4 @@ namespace IW4MAdmin.Application.Commands } } } -} \ No newline at end of file +} diff --git a/Application/IW4MServer.cs b/Application/IW4MServer.cs index 264f244f0..e57f4df4f 100644 --- a/Application/IW4MServer.cs +++ b/Application/IW4MServer.cs @@ -24,8 +24,10 @@ using Serilog.Context; using static SharedLibraryCore.Database.Models.EFClient; using Data.Models; using Data.Models.Server; +using IW4MAdmin.Application.Alerts; using IW4MAdmin.Application.Commands; using Microsoft.EntityFrameworkCore; +using SharedLibraryCore.Alerts; using static Data.Models.Client.EFClient; namespace IW4MAdmin @@ -306,8 +308,16 @@ namespace IW4MAdmin if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost) { Console.WriteLine(loc["SERVER_ERROR_COMMUNICATION"].FormatExt($"{IP}:{Port}")); + + var alert = Alert.AlertState.Build().OfType(E.Type.ToString()) + .WithCategory(Alert.AlertCategory.Error) + .FromSource("System") + .WithMessage(loc["SERVER_ERROR_COMMUNICATION"].FormatExt($"{IP}:{Port}")) + .ExpiresIn(TimeSpan.FromDays(1)); + + Manager.AlertManager.AddAlert(alert); } - + Throttled = true; } @@ -318,7 +328,15 @@ namespace IW4MAdmin if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost) { - Console.WriteLine(loc["MANAGER_CONNECTION_REST"].FormatExt($"[{IP}:{Port}]")); + Console.WriteLine(loc["MANAGER_CONNECTION_REST"].FormatExt($"{IP}:{Port}")); + + var alert = Alert.AlertState.Build().OfType(E.Type.ToString()) + .WithCategory(Alert.AlertCategory.Information) + .FromSource("System") + .WithMessage(loc["MANAGER_CONNECTION_REST"].FormatExt($"{IP}:{Port}")) + .ExpiresIn(TimeSpan.FromDays(1)); + + Manager.AlertManager.AddAlert(alert); } if (!string.IsNullOrEmpty(CustomSayName)) diff --git a/Application/Main.cs b/Application/Main.cs index b3f3107e2..32269109a 100644 --- a/Application/Main.cs +++ b/Application/Main.cs @@ -27,6 +27,7 @@ using System.Threading.Tasks; using Data.Abstractions; using Data.Helpers; using Integrations.Source.Extensions; +using IW4MAdmin.Application.Alerts; using IW4MAdmin.Application.Extensions; using IW4MAdmin.Application.Localization; using Microsoft.Extensions.Logging; @@ -448,6 +449,7 @@ namespace IW4MAdmin.Application .AddSingleton() .AddSingleton() .AddSingleton(new GeoLocationService(Path.Join(".", "Resources", "GeoLite2-Country.mmdb"))) + .AddSingleton() .AddTransient() .AddSingleton(translationLookup) .AddDatabaseContextOptions(appConfig); diff --git a/SharedLibraryCore/Alerts/Alert.cs b/SharedLibraryCore/Alerts/Alert.cs new file mode 100644 index 000000000..8e3f1375d --- /dev/null +++ b/SharedLibraryCore/Alerts/Alert.cs @@ -0,0 +1,33 @@ +using System; +using Data.Models.Client; + +namespace SharedLibraryCore.Alerts; + +public class Alert +{ + public enum AlertCategory + { + Information, + Warning, + Error, + Message, + } + + public class AlertState + { + public Guid AlertId { get; } = Guid.NewGuid(); + public AlertCategory Category { get; set; } + public DateTime OccuredAt { get; set; } = DateTime.UtcNow; + public DateTime? ExpiresAt { get; set; } + public string Message { get; set; } + public string Source { get; set; } + public int? RecipientId { get; set; } + public int? SourceId { get; set; } + public int? ReferenceId { get; set; } + public bool? Delivered { get; set; } + public bool? Consumed { get; set; } + public EFClient.Permission? MinimumPermission { get; set; } + public string Type { get; set; } + public static AlertState Build() => new(); + } +} diff --git a/SharedLibraryCore/BaseController.cs b/SharedLibraryCore/BaseController.cs index 6bc33a4ab..cbea583fc 100644 --- a/SharedLibraryCore/BaseController.cs +++ b/SharedLibraryCore/BaseController.cs @@ -20,6 +20,8 @@ namespace SharedLibraryCore { public class BaseController : Controller { + protected readonly IAlertManager AlertManager; + /// /// life span in months /// @@ -34,6 +36,7 @@ namespace SharedLibraryCore public BaseController(IManager manager) { + AlertManager = manager.AlertManager; Manager = manager; Localization ??= Utilities.CurrentLocalization.LocalizationIndex; AppConfig = Manager.GetApplicationSettings().Configuration(); @@ -169,6 +172,7 @@ namespace SharedLibraryCore ViewBag.ReportCount = Manager.GetServers().Sum(server => server.Reports.Count(report => DateTime.UtcNow - report.ReportedOn <= TimeSpan.FromHours(24))); ViewBag.PermissionsSet = PermissionsSet; + ViewBag.Alerts = AlertManager.RetrieveAlerts(Client).ToList(); base.OnActionExecuting(context); } diff --git a/SharedLibraryCore/Configuration/ApplicationConfiguration.cs b/SharedLibraryCore/Configuration/ApplicationConfiguration.cs index 5775c03db..da052c9d8 100644 --- a/SharedLibraryCore/Configuration/ApplicationConfiguration.cs +++ b/SharedLibraryCore/Configuration/ApplicationConfiguration.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; +using Data.Models.Misc; using Newtonsoft.Json; using SharedLibraryCore.Configuration.Attributes; using SharedLibraryCore.Interfaces; @@ -154,6 +155,13 @@ namespace SharedLibraryCore.Configuration { Permission.Console.ToString(), new List { "*" } } }; + public Dictionary MinimumAlertPermissions { get; set; } = new() + { + { nameof(EFInboxMessage), Permission.Trusted }, + { GameEvent.EventType.ConnectionLost.ToString(), Permission.Administrator }, + { GameEvent.EventType.ConnectionRestored.ToString(), Permission.Administrator } + }; + [ConfigurationIgnore] [LocalizedDisplayName("WEBFRONT_CONFIGURATION_PRESET_BAN_REASONS")] public Dictionary PresetPenaltyReasons { get; set; } = new() diff --git a/SharedLibraryCore/Interfaces/IAlertManager.cs b/SharedLibraryCore/Interfaces/IAlertManager.cs new file mode 100644 index 000000000..9ec3f00d1 --- /dev/null +++ b/SharedLibraryCore/Interfaces/IAlertManager.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using SharedLibraryCore.Alerts; +using SharedLibraryCore.Database.Models; + +namespace SharedLibraryCore.Interfaces; + +public interface IAlertManager +{ + /// + /// Initializes the manager + /// + /// + Task Initialize(); + + /// + /// Get all the alerts for given client + /// + /// client to retrieve alerts for + /// + IEnumerable RetrieveAlerts(EFClient client); + + /// + /// Trigger a new alert + /// + /// Alert to trigger + void AddAlert(Alert.AlertState alert); + + /// + /// Marks an alert as read and removes it from the manager + /// + /// Id of the alert to mark as read + void MarkAlertAsRead(Guid alertId); + + /// + /// Mark all alerts intended for the given recipientId as read + /// + /// Identifier of the recipient + void MarkAllAlertsAsRead(int recipientId); + + /// + /// Registers a static (persistent) event source eg datastore that + /// gets initialized at startup + /// + /// Source action + void RegisterStaticAlertSource(Func>> alertSource); + + /// + /// Fires when an alert has been consumed (dimissed) + /// + EventHandler OnAlertConsumed { get; set; } + +} diff --git a/SharedLibraryCore/Interfaces/IManager.cs b/SharedLibraryCore/Interfaces/IManager.cs index 4b4985404..65c524005 100644 --- a/SharedLibraryCore/Interfaces/IManager.cs +++ b/SharedLibraryCore/Interfaces/IManager.cs @@ -102,5 +102,7 @@ namespace SharedLibraryCore.Interfaces /// event executed when event has finished executing /// event EventHandler OnGameEventExecuted; + + IAlertManager AlertManager { get; } } } diff --git a/WebfrontCore/Controllers/ActionController.cs b/WebfrontCore/Controllers/ActionController.cs index 352211670..a2d23b38e 100644 --- a/WebfrontCore/Controllers/ActionController.cs +++ b/WebfrontCore/Controllers/ActionController.cs @@ -24,6 +24,7 @@ namespace WebfrontCore.Controllers private readonly string _unbanCommandName; private readonly string _sayCommandName; private readonly string _kickCommandName; + private readonly string _offlineMessageCommandName; private readonly string _flagCommandName; private readonly string _unflagCommandName; private readonly string _setLevelCommandName; @@ -64,6 +65,9 @@ namespace WebfrontCore.Controllers case nameof(SetLevelCommand): _setLevelCommandName = cmd.Name; break; + case "OfflineMessageCommand": + _offlineMessageCommandName = cmd.Name; + break; } } } @@ -213,7 +217,7 @@ namespace WebfrontCore.Controllers public async Task Login(int clientId, string password) { - return await Task.FromResult(RedirectToAction("Login", "Account", new {clientId, password})); + return await Task.FromResult(RedirectToAction("Login", "Account", new { clientId, password })); } public IActionResult EditForm() @@ -327,26 +331,27 @@ namespace WebfrontCore.Controllers public async Task RecentClientsForm(PaginationRequest request) { ViewBag.First = request.Offset == 0; - + if (request.Count > 20) { request.Count = 20; } - + var clients = await Manager.GetClientService().GetRecentClients(request); return request.Offset == 0 ? View("~/Views/Shared/Components/Client/_RecentClientsContainer.cshtml", clients) : View("~/Views/Shared/Components/Client/_RecentClients.cshtml", clients); } - + public IActionResult RecentReportsForm() { var serverInfo = Manager.GetServers().Select(server => new ServerInfo { Name = server.Hostname, - Reports = server.Reports.Where(report => (DateTime.UtcNow - report.ReportedOn).TotalHours <= 24).ToList() + Reports = server.Reports.Where(report => (DateTime.UtcNow - report.ReportedOn).TotalHours <= 24) + .ToList() }); return View("Partials/_Reports", serverInfo); @@ -473,6 +478,105 @@ namespace WebfrontCore.Controllers })); } + public IActionResult DismissAlertForm(Guid id) + { + var info = new ActionInfo + { + ActionButtonLabel = "Dismiss", + Name = "Dismiss Alert?", + Inputs = new List + { + new() + { + Name = "alertId", + Type = "hidden", + Value = id.ToString() + } + }, + Action = nameof(DismissAlert), + ShouldRefresh = true + }; + + return View("_ActionForm", info); + } + + public IActionResult DismissAlert(Guid alertId) + { + AlertManager.MarkAlertAsRead(alertId); + return Json(new[] + { + new CommandResponseInfo + { + Response = "Alert dismissed" + } + }); + } + + public IActionResult DismissAllAlertsForm() + { + var info = new ActionInfo + { + ActionButtonLabel = "Dismiss", + Name = "Dismiss All Alerts?", + Inputs = new List + { + new() + { + Name = "targetId", + Type = "hidden", + Value = Client.ClientId.ToString() + } + }, + Action = nameof(DismissAllAlerts), + ShouldRefresh = true + }; + + return View("_ActionForm", info); + } + + public IActionResult DismissAllAlerts(int targetId) + { + AlertManager.MarkAllAlertsAsRead(targetId); + return Json(new[] + { + new CommandResponseInfo + { + Response = "Alerts dismissed" + } + }); + } + + public IActionResult OfflineMessageForm() + { + var info = new ActionInfo + { + ActionButtonLabel = "Send", + Name = "Compose Message", + Inputs = new List + { + new() + { + Name = "message", + Label = "Message Content", + }, + }, + Action = "OfflineMessage", + ShouldRefresh = true + }; + return View("_ActionForm", info); + } + + public async Task OfflineMessage(int targetId, string message) + { + var server = Manager.GetServers().First(); + return await Task.FromResult(RedirectToAction("Execute", "Console", new + { + serverId = server.EndPoint, + command = + $"{_appConfig.CommandPrefix}{_offlineMessageCommandName} @{targetId} {message.CapClientName(500)}" + })); + } + private Dictionary GetPresetPenaltyReasons() => _appConfig.PresetPenaltyReasons.Values .Concat(_appConfig.GlobalRules) .Concat(_appConfig.Servers.SelectMany(server => server.Rules ?? Array.Empty())) diff --git a/WebfrontCore/Views/Action/_ActionForm.cshtml b/WebfrontCore/Views/Action/_ActionForm.cshtml index c2dd3fd3f..6eaca9c0e 100644 --- a/WebfrontCore/Views/Action/_ActionForm.cshtml +++ b/WebfrontCore/Views/Action/_ActionForm.cshtml @@ -4,7 +4,7 @@ Layout = null; } -@if (Model.Inputs.Any()) +@if (Model.Inputs.Any(input => input.Type != "hidden")) {
} @@ -55,12 +55,12 @@ } } - @if (Model.Inputs.Any()) + @if (Model.Inputs.Any(input => input.Type != "hidden")) {
}
- Close + Close
diff --git a/WebfrontCore/Views/Client/Profile/Index.cshtml b/WebfrontCore/Views/Client/Profile/Index.cshtml index 68a9ce2a2..e8546ed7f 100644 --- a/WebfrontCore/Views/Client/Profile/Index.cshtml +++ b/WebfrontCore/Views/Client/Profile/Index.cshtml @@ -279,6 +279,18 @@ EntityId = Model.ClientId }); } + + if (ViewBag.Authorized) + { + menuItems.Items.Add(new SideContextMenuItem + { + Title = "Message", + IsButton = true, + Reference = "OfflineMessage", + Icon = "oi oi-envelope-closed", + EntityId = Model.ClientId + }); + } menuItems.Items.Add(new SideContextMenuItem { @@ -336,6 +348,7 @@ EntityId = Model.ClientId }); } + } diff --git a/WebfrontCore/Views/Shared/Partials/_Notifications.cshtml b/WebfrontCore/Views/Shared/Partials/_Notifications.cshtml new file mode 100644 index 000000000..4c5b86abd --- /dev/null +++ b/WebfrontCore/Views/Shared/Partials/_Notifications.cshtml @@ -0,0 +1,65 @@ +@using SharedLibraryCore.Alerts +@using Humanizer +@model IEnumerable +@{ + Layout = null; +} + diff --git a/WebfrontCore/Views/Shared/_Layout.cshtml b/WebfrontCore/Views/Shared/_Layout.cshtml index 6f05ae819..1a192e370 100644 --- a/WebfrontCore/Views/Shared/_Layout.cshtml +++ b/WebfrontCore/Views/Shared/_Layout.cshtml @@ -119,11 +119,13 @@
+
+ @await Html.PartialAsync("Partials/_Notifications", (object)ViewBag.Alerts) +
-