diff --git a/MareAPI b/MareAPI
index 7a48ca9..fa9b7bc 160000
--- a/MareAPI
+++ b/MareAPI
@@ -1 +1 @@
-Subproject commit 7a48ca98234d82a46dc291ae030cc1d7f544b903
+Subproject commit fa9b7bce43b8baf9ba17d9e1df221fafa20fd6d7
diff --git a/MareSynchronos/MareSynchronos.csproj b/MareSynchronos/MareSynchronos.csproj
index 081b0ce..29c5657 100644
--- a/MareSynchronos/MareSynchronos.csproj
+++ b/MareSynchronos/MareSynchronos.csproj
@@ -3,7 +3,7 @@
UmbraSync
UmbraSync
- 0.1.7.0
+ 0.1.8.0
diff --git a/MareSynchronos/PlayerData/Pairs/PairManager.cs b/MareSynchronos/PlayerData/Pairs/PairManager.cs
index 1b5d89a..5268c35 100644
--- a/MareSynchronos/PlayerData/Pairs/PairManager.cs
+++ b/MareSynchronos/PlayerData/Pairs/PairManager.cs
@@ -210,9 +210,16 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
public void SetGroupInfo(GroupInfoDto dto)
{
- _allGroups[dto.Group].Group = dto.Group;
- _allGroups[dto.Group].Owner = dto.Owner;
- _allGroups[dto.Group].GroupPermissions = dto.GroupPermissions;
+ if (!_allGroups.TryGetValue(dto.Group, out var groupInfo))
+ {
+ return;
+ }
+
+ groupInfo.Group = dto.Group;
+ groupInfo.Owner = dto.Owner;
+ groupInfo.GroupPermissions = dto.GroupPermissions;
+ groupInfo.IsTemporary = dto.IsTemporary;
+ groupInfo.ExpiresAt = dto.ExpiresAt;
RecreateLazy();
}
@@ -400,4 +407,4 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
_directPairsInternal = DirectPairsLazy();
_groupPairsInternal = GroupPairsLazy();
}
-}
\ No newline at end of file
+}
diff --git a/MareSynchronos/Plugin.cs b/MareSynchronos/Plugin.cs
index d357122..2335d9b 100644
--- a/MareSynchronos/Plugin.cs
+++ b/MareSynchronos/Plugin.cs
@@ -145,6 +145,7 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton();
collection.AddSingleton();
collection.AddSingleton();
+ collection.AddSingleton();
collection.AddSingleton((s) => new MareConfigService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton((s) => new ServerConfigService(pluginInterface.ConfigDirectory.FullName));
@@ -203,6 +204,7 @@ public sealed class Plugin : IDalamudPlugin
collection.AddHostedService(p => p.GetRequiredService());
collection.AddHostedService(p => p.GetRequiredService());
collection.AddHostedService(p => p.GetRequiredService());
+ collection.AddHostedService(p => p.GetRequiredService());
collection.AddHostedService(p => p.GetRequiredService());
collection.AddHostedService(p => p.GetRequiredService());
collection.AddHostedService(p => p.GetRequiredService());
diff --git a/MareSynchronos/Services/TemporarySyncshellNotificationService.cs b/MareSynchronos/Services/TemporarySyncshellNotificationService.cs
new file mode 100644
index 0000000..e7062b7
--- /dev/null
+++ b/MareSynchronos/Services/TemporarySyncshellNotificationService.cs
@@ -0,0 +1,225 @@
+using System.Globalization;
+using System.Threading;
+using MareSynchronos.API.Dto.Group;
+using MareSynchronos.MareConfiguration.Models;
+using MareSynchronos.PlayerData.Pairs;
+using MareSynchronos.Services.Mediator;
+using MareSynchronos.WebAPI;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace MareSynchronos.Services;
+
+public sealed class TemporarySyncshellNotificationService : MediatorSubscriberBase, IHostedService
+{
+ private static readonly int[] NotificationThresholdMinutes = [30, 15, 5, 1];
+ private readonly ApiController _apiController;
+ private readonly PairManager _pairManager;
+ private readonly Lock _stateLock = new();
+ private readonly Dictionary _trackedGroups = new(StringComparer.Ordinal);
+ private CancellationTokenSource? _loopCts;
+ private Task? _loopTask;
+
+ public TemporarySyncshellNotificationService(ILogger logger, MareMediator mediator, PairManager pairManager, ApiController apiController)
+ : base(logger, mediator)
+ {
+ _pairManager = pairManager;
+ _apiController = apiController;
+ }
+
+ public Task StartAsync(CancellationToken cancellationToken)
+ {
+ _loopCts = new CancellationTokenSource();
+ Mediator.Subscribe(this, _ => ResetTrackedGroups());
+ Mediator.Subscribe(this, _ => ResetTrackedGroups());
+ _loopTask = Task.Run(() => MonitorLoopAsync(_loopCts.Token), _loopCts.Token);
+ return Task.CompletedTask;
+ }
+
+ public async Task StopAsync(CancellationToken cancellationToken)
+ {
+ Mediator.UnsubscribeAll(this);
+ if (_loopCts == null)
+ {
+ return;
+ }
+
+ try
+ {
+ _loopCts.Cancel();
+ if (_loopTask != null)
+ {
+ await _loopTask.ConfigureAwait(false);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ }
+ finally
+ {
+ _loopTask = null;
+ _loopCts.Dispose();
+ _loopCts = null;
+ }
+ }
+
+ private async Task MonitorLoopAsync(CancellationToken ct)
+ {
+ var delay = TimeSpan.FromSeconds(30);
+ while (!ct.IsCancellationRequested)
+ {
+ try
+ {
+ CheckGroups();
+ }
+ catch (Exception ex)
+ {
+ Logger.LogDebug(ex, "Failed to check temporary syncshell expirations");
+ }
+
+ try
+ {
+ await Task.Delay(delay, ct).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ break;
+ }
+ }
+ }
+
+ private void CheckGroups()
+ {
+ var nowUtc = DateTime.UtcNow;
+ var groupsSnapshot = _pairManager.Groups.Values.ToList();
+ var notifications = new List();
+ var expiredGroups = new List();
+ var seenTemporaryGids = new HashSet(StringComparer.Ordinal);
+
+ using (var guard = _stateLock.EnterScope())
+ {
+ foreach (var group in groupsSnapshot)
+ {
+ if (!group.IsTemporary || group.ExpiresAt == null)
+ {
+ continue;
+ }
+
+ if (string.IsNullOrEmpty(_apiController.UID) || !string.Equals(group.OwnerUID, _apiController.UID, StringComparison.Ordinal))
+ {
+ continue;
+ }
+
+ var gid = group.Group.GID;
+ seenTemporaryGids.Add(gid);
+ var expiresAtUtc = NormalizeToUtc(group.ExpiresAt.Value);
+ var remaining = expiresAtUtc - nowUtc;
+
+ if (!_trackedGroups.TryGetValue(gid, out var state))
+ {
+ state = new TrackedGroup(expiresAtUtc);
+ _trackedGroups[gid] = state;
+ }
+ else if (state.ExpiresAtUtc != expiresAtUtc)
+ {
+ state.UpdateExpiresAt(expiresAtUtc);
+ }
+
+ if (remaining <= TimeSpan.Zero)
+ {
+ _trackedGroups.Remove(gid);
+ expiredGroups.Add(group);
+ continue;
+ }
+
+ if (!state.LastRemaining.HasValue)
+ {
+ state.UpdateRemaining(remaining);
+ continue;
+ }
+
+ var previousRemaining = state.LastRemaining.Value;
+
+ foreach (var thresholdMinutes in NotificationThresholdMinutes)
+ {
+ var threshold = TimeSpan.FromMinutes(thresholdMinutes);
+ if (previousRemaining > threshold && remaining <= threshold)
+ {
+ notifications.Add(new NotificationPayload(group, thresholdMinutes, expiresAtUtc));
+ }
+ }
+
+ state.UpdateRemaining(remaining);
+ }
+
+ var toRemove = _trackedGroups.Keys.Where(k => !seenTemporaryGids.Contains(k)).ToList();
+ foreach (var gid in toRemove)
+ {
+ _trackedGroups.Remove(gid);
+ }
+ }
+
+ foreach (var expiredGroup in expiredGroups)
+ {
+ Logger.LogInformation("Temporary syncshell {gid} expired locally; removing", expiredGroup.Group.GID);
+ _pairManager.RemoveGroup(expiredGroup.Group);
+ }
+
+ foreach (var notification in notifications)
+ {
+ PublishNotification(notification.Group, notification.ThresholdMinutes, notification.ExpiresAtUtc);
+ }
+ }
+
+ private void PublishNotification(GroupFullInfoDto group, int thresholdMinutes, DateTime expiresAtUtc)
+ {
+ string displayName = string.IsNullOrWhiteSpace(group.GroupAlias) ? group.Group.GID : group.GroupAlias!;
+ string threshold = thresholdMinutes == 1 ? "1 minute" : $"{thresholdMinutes} minutes";
+ string expiresLocal = expiresAtUtc.ToLocalTime().ToString("t", CultureInfo.CurrentCulture);
+
+ string message = $"La Syncshell temporaire \"{displayName}\" sera supprimee dans {threshold} (a {expiresLocal}).";
+ Mediator.Publish(new NotificationMessage("Syncshell temporaire", message, NotificationType.Warning, TimeSpan.FromSeconds(6)));
+ }
+
+ private static DateTime NormalizeToUtc(DateTime expiresAt)
+ {
+ return expiresAt.Kind switch
+ {
+ DateTimeKind.Utc => expiresAt,
+ DateTimeKind.Local => expiresAt.ToUniversalTime(),
+ _ => DateTime.SpecifyKind(expiresAt, DateTimeKind.Utc)
+ };
+ }
+
+ private void ResetTrackedGroups()
+ {
+ using (var guard = _stateLock.EnterScope())
+ {
+ _trackedGroups.Clear();
+ }
+ }
+
+ private sealed class TrackedGroup
+ {
+ public TrackedGroup(DateTime expiresAtUtc)
+ {
+ ExpiresAtUtc = expiresAtUtc;
+ }
+
+ public DateTime ExpiresAtUtc { get; private set; }
+ public TimeSpan? LastRemaining { get; private set; }
+
+ public void UpdateExpiresAt(DateTime expiresAtUtc)
+ {
+ ExpiresAtUtc = expiresAtUtc;
+ LastRemaining = null;
+ }
+
+ public void UpdateRemaining(TimeSpan remaining)
+ {
+ LastRemaining = remaining;
+ }
+ }
+
+ private sealed record NotificationPayload(GroupFullInfoDto Group, int ThresholdMinutes, DateTime ExpiresAtUtc);
+}
diff --git a/MareSynchronos/UI/Components/GroupPanel.cs b/MareSynchronos/UI/Components/GroupPanel.cs
index c9e3f33..134d05f 100644
--- a/MareSynchronos/UI/Components/GroupPanel.cs
+++ b/MareSynchronos/UI/Components/GroupPanel.cs
@@ -17,6 +17,7 @@ using MareSynchronos.Services.ServerConfiguration;
using MareSynchronos.UI.Components;
using MareSynchronos.UI.Handlers;
using MareSynchronos.WebAPI;
+using System;
using System.Globalization;
using System.Numerics;
@@ -54,6 +55,20 @@ internal sealed class GroupPanel
private bool _showModalCreateGroup;
private bool _showModalEnterPassword;
private string _newSyncShellAlias = string.Empty;
+ private bool _createIsTemporary = false;
+ private int _tempSyncshellDurationHours = 24;
+ private readonly int[] _temporaryDurationOptions = new[]
+ {
+ 1,
+ 12,
+ 24,
+ 48,
+ 72,
+ 96,
+ 120,
+ 144,
+ 168
+ };
private string _syncShellPassword = string.Empty;
private string _syncShellToJoin = string.Empty;
@@ -111,6 +126,8 @@ internal sealed class GroupPanel
_lastCreatedGroup = null;
_errorGroupCreate = false;
_newSyncShellAlias = string.Empty;
+ _createIsTemporary = false;
+ _tempSyncshellDurationHours = 24;
_errorGroupCreateMessage = string.Empty;
_showModalCreateGroup = true;
ImGui.OpenPopup("Create Syncshell");
@@ -153,28 +170,98 @@ internal sealed class GroupPanel
}
if (ImGui.BeginPopupModal("Create Syncshell", ref _showModalCreateGroup, UiSharedService.PopupWindowFlags))
+ {
+ UiSharedService.TextWrapped("Choisissez le type de Syncshell à créer.");
+ bool showPermanent = !_createIsTemporary;
+ if (ImGui.RadioButton("Permanente", showPermanent))
+ {
+ _createIsTemporary = false;
+ }
+ ImGui.SameLine();
+ if (ImGui.RadioButton("Temporaire", _createIsTemporary))
+ {
+ _createIsTemporary = true;
+ _newSyncShellAlias = string.Empty;
+ }
+
+ if (!_createIsTemporary)
{
UiSharedService.TextWrapped("Donnez un nom à votre Syncshell (optionnel) puis créez-la.");
ImGui.SetNextItemWidth(-1);
ImGui.InputTextWithHint("##syncshellalias", "Nom du Syncshell", ref _newSyncShellAlias, 50);
+ }
+ else
+ {
+ _newSyncShellAlias = string.Empty;
+ }
+
+ if (_createIsTemporary)
+ {
+ UiSharedService.TextWrapped("Durée maximale d'une Syncshell temporaire : 7 jours.");
+ if (_tempSyncshellDurationHours > 168) _tempSyncshellDurationHours = 168;
+ for (int i = 0; i < _temporaryDurationOptions.Length; i++)
+ {
+ var option = _temporaryDurationOptions[i];
+ var isSelected = _tempSyncshellDurationHours == option;
+ string label = option switch
+ {
+ >= 24 when option % 24 == 0 => option == 24 ? "24h" : $"{option / 24}j",
+ _ => option + "h"
+ };
+
+ if (ImGui.RadioButton(label, isSelected))
+ {
+ _tempSyncshellDurationHours = option;
+ }
+
+ // Start a new line after every 3 buttons
+ if ((i + 1) % 3 == 0)
+ {
+ ImGui.NewLine();
+ }
+ else
+ {
+ ImGui.SameLine();
+ }
+ }
+
+ var expiresLocal = DateTime.Now.AddHours(_tempSyncshellDurationHours);
+ UiSharedService.TextWrapped($"Expiration le {expiresLocal:g} (heure locale).");
+ }
+
UiSharedService.TextWrapped("Appuyez sur le bouton ci-dessous pour créer une nouvelle Syncshell.");
ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale);
if (ImGui.Button("Create Syncshell"))
{
try
{
- var aliasInput = string.IsNullOrWhiteSpace(_newSyncShellAlias) ? null : _newSyncShellAlias.Trim();
- _lastCreatedGroup = ApiController.GroupCreate(aliasInput).Result;
- if (_lastCreatedGroup != null)
+ if (_createIsTemporary)
{
- _newSyncShellAlias = string.Empty;
+ var expiresAtUtc = DateTime.UtcNow.AddHours(_tempSyncshellDurationHours);
+ _lastCreatedGroup = ApiController.GroupCreateTemporary(expiresAtUtc).Result;
+ }
+ else
+ {
+ var aliasInput = string.IsNullOrWhiteSpace(_newSyncShellAlias) ? null : _newSyncShellAlias.Trim();
+ _lastCreatedGroup = ApiController.GroupCreate(aliasInput).Result;
+ if (_lastCreatedGroup != null)
+ {
+ _newSyncShellAlias = string.Empty;
+ }
}
}
catch (Exception ex)
{
_lastCreatedGroup = null;
_errorGroupCreate = true;
- _errorGroupCreateMessage = ex.Message;
+ if (ex.Message.Contains("name is already in use", StringComparison.OrdinalIgnoreCase))
+ {
+ _errorGroupCreateMessage = "Le nom de la Syncshell est déjà utilisé.";
+ }
+ else
+ {
+ _errorGroupCreateMessage = ex.Message;
+ }
}
}
@@ -196,6 +283,11 @@ internal sealed class GroupPanel
ImGui.SetClipboardText(_lastCreatedGroup.Password);
}
UiSharedService.TextWrapped("You can change the Syncshell password later at any time.");
+ if (_lastCreatedGroup.IsTemporary && _lastCreatedGroup.ExpiresAt != null)
+ {
+ var expiresLocal = _lastCreatedGroup.ExpiresAt.Value.ToLocalTime();
+ UiSharedService.TextWrapped($"Cette Syncshell expirera le {expiresLocal:g} (heure locale).");
+ }
}
if (_errorGroupCreate)
@@ -263,11 +355,13 @@ internal sealed class GroupPanel
if (!string.Equals(_editGroupEntry, groupDto.GID, StringComparison.Ordinal))
{
var shellConfig = _serverConfigurationManager.GetShellConfigForGid(groupDto.GID);
- if (!_mareConfig.Current.DisableSyncshellChat && shellConfig.Enabled)
- {
- ImGui.TextUnformatted($"[{shellNumber}]");
- UiSharedService.AttachToolTip("Chat command prefix: /ss" + shellNumber);
- }
+ var totalMembers = pairsInGroup.Count + 1;
+ var connectedMembers = pairsInGroup.Count(p => p.IsOnline) + 1;
+ var maxCapacity = ApiController.ServerInfo.MaxGroupUserCount;
+ ImGui.TextUnformatted($"{connectedMembers}/{totalMembers}");
+ UiSharedService.AttachToolTip("Membres connectés / membres totaux" + Environment.NewLine +
+ $"Capacité maximale : {maxCapacity}" + Environment.NewLine +
+ "Syncshell ID: " + groupDto.Group.GID);
if (textIsGid) ImGui.PushFont(UiBuilder.MonoFont);
ImGui.SameLine();
ImGui.TextUnformatted(groupName);
@@ -275,6 +369,20 @@ internal sealed class GroupPanel
UiSharedService.AttachToolTip("Left click to switch between GID display and comment" + Environment.NewLine +
"Right click to change comment for " + groupName + Environment.NewLine + Environment.NewLine
+ "Users: " + (pairsInGroup.Count + 1) + ", Owner: " + groupDto.OwnerAliasOrUID);
+ if (groupDto.IsTemporary)
+ {
+ ImGui.SameLine();
+ UiSharedService.ColorText("(Temp)", ImGuiColors.DalamudOrange);
+ if (groupDto.ExpiresAt != null)
+ {
+ var tempExpireLocal = groupDto.ExpiresAt.Value.ToLocalTime();
+ UiSharedService.AttachToolTip($"Expire le {tempExpireLocal:g}");
+ }
+ else
+ {
+ UiSharedService.AttachToolTip("Syncshell temporaire");
+ }
+ }
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
{
var prevState = textIsGid;
diff --git a/MareSynchronos/UI/DtrEntry.cs b/MareSynchronos/UI/DtrEntry.cs
index e75194c..0c2f943 100644
--- a/MareSynchronos/UI/DtrEntry.cs
+++ b/MareSynchronos/UI/DtrEntry.cs
@@ -2,6 +2,7 @@
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Plugin.Services;
+using Dalamud.Interface;
using MareSynchronos.MareConfiguration;
using MareSynchronos.MareConfiguration.Configurations;
using MareSynchronos.PlayerData.Pairs;
@@ -15,6 +16,7 @@ namespace MareSynchronos.UI;
public sealed class DtrEntry : IDisposable, IHostedService
{
+ public const string DefaultGlyph = "\u25CB";
private enum DtrStyle
{
Default,
@@ -196,7 +198,8 @@ public sealed class DtrEntry : IDisposable, IHostedService
{
var style = (DtrStyle)styleNum;
- return style switch {
+ return style switch
+ {
DtrStyle.Style1 => $"\xE039 {text}",
DtrStyle.Style2 => $"\xE0BC {text}",
DtrStyle.Style3 => $"\xE0BD {text}",
@@ -206,7 +209,7 @@ public sealed class DtrEntry : IDisposable, IHostedService
DtrStyle.Style7 => $"\xE05D {text}",
DtrStyle.Style8 => $"\xE03C{text}",
DtrStyle.Style9 => $"\xE040 {text} \xE041",
- _ => $"\uE044 {text}"
+ _ => DefaultGlyph + " " + text
};
}
diff --git a/MareSynchronos/UI/SettingsUi.cs b/MareSynchronos/UI/SettingsUi.cs
index 8afc981..a3a0a8e 100644
--- a/MareSynchronos/UI/SettingsUi.cs
+++ b/MareSynchronos/UI/SettingsUi.cs
@@ -50,6 +50,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
private readonly AccountRegistrationService _registerService;
private readonly ServerConfigurationManager _serverConfigurationManager;
private readonly UiSharedService _uiShared;
+ private static readonly string DtrDefaultPreviewText = DtrEntry.DefaultGlyph + " 123";
private bool _deleteAccountPopupModalShown = false;
private string _lastTab = string.Empty;
private bool? _notesSuccessfullyApplied = null;
@@ -1049,13 +1050,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
_configService.Save();
}
- ImGui.SetNextItemWidth(250 * ImGuiHelpers.GlobalScale);
- _uiShared.DrawCombo("Server Info Bar style", Enumerable.Range(0, DtrEntry.NumStyles), (i) => DtrEntry.RenderDtrStyle(i, "123"),
- (i) =>
- {
- _configService.Current.DtrStyle = i;
- _configService.Save();
- }, _configService.Current.DtrStyle);
+ DrawDtrStyleCombo();
if (ImGui.Checkbox("Color-code the Server Info Bar entry according to status", ref useColorsInDtr))
{
@@ -1941,12 +1936,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.EndDisabled();
}
- if (ImGui.BeginTabItem("Chat"))
- {
- DrawChatConfig();
- ImGui.EndTabItem();
- }
-
if (ImGui.BeginTabItem("Advanced"))
{
DrawAdvanced();
@@ -1957,6 +1946,38 @@ public class SettingsUi : WindowMediatorSubscriberBase
}
}
+ private void DrawDtrStyleCombo()
+ {
+ var styleIndex = _configService.Current.DtrStyle;
+ string previewText = styleIndex == 0 ? DtrDefaultPreviewText : DtrEntry.RenderDtrStyle(styleIndex, "123");
+
+ ImGui.SetNextItemWidth(250 * ImGuiHelpers.GlobalScale);
+ bool comboOpen = ImGui.BeginCombo("Server Info Bar style", previewText);
+
+ if (comboOpen)
+ {
+ for (int i = 0; i < DtrEntry.NumStyles; i++)
+ {
+ string label = i == 0 ? DtrDefaultPreviewText : DtrEntry.RenderDtrStyle(i, "123");
+ bool isSelected = i == styleIndex;
+ if (ImGui.Selectable(label, isSelected))
+ {
+ _configService.Current.DtrStyle = i;
+ _configService.Save();
+ }
+
+ if (isSelected)
+ {
+ ImGui.SetItemDefaultFocus();
+ }
+
+ }
+
+ ImGui.EndCombo();
+ }
+
+ }
+
private void UiSharedService_GposeEnd()
{
IsOpen = _wasOpen;
diff --git a/MareSynchronos/UmbraSync.json b/MareSynchronos/UmbraSync.json
index da34e8d..f9f9e8e 100644
--- a/MareSynchronos/UmbraSync.json
+++ b/MareSynchronos/UmbraSync.json
@@ -1,8 +1,8 @@
{
- "Author": "SirConstance",
+ "Author": "Keda",
"Name": "UmbraSync",
- "Punchline": "Share your true self.",
- "Description": "This plugin will synchronize your Penumbra mods and current Glamourer state with other paired clients automatically.",
+ "Punchline": "Parce que nous le valons bien.",
+ "Description": "Ce plugin synchronisera automatiquement vos mods Penumbra et l'état actuel de Glamourer avec les autres clients appairés.",
"InternalName": "UmbraSync",
"ApplicableVersion": "any",
"Tags": [
diff --git a/MareSynchronos/WebAPI/SignalR/ApiController.Functions.Groups.cs b/MareSynchronos/WebAPI/SignalR/ApiController.Functions.Groups.cs
index b0e1398..e1ac34d 100644
--- a/MareSynchronos/WebAPI/SignalR/ApiController.Functions.Groups.cs
+++ b/MareSynchronos/WebAPI/SignalR/ApiController.Functions.Groups.cs
@@ -1,4 +1,5 @@
-using MareSynchronos.API.Data;
+using System;
+using MareSynchronos.API.Data;
using MareSynchronos.API.Dto.Group;
using MareSynchronos.WebAPI.SignalR.Utils;
using Microsoft.AspNetCore.SignalR.Client;
@@ -60,6 +61,12 @@ public partial class ApiController
return await _mareHub!.InvokeAsync(nameof(GroupCreate), string.IsNullOrWhiteSpace(alias) ? null : alias.Trim()).ConfigureAwait(false);
}
+ public async Task GroupCreateTemporary(DateTime expiresAtUtc)
+ {
+ CheckConnection();
+ return await _mareHub!.InvokeAsync(nameof(GroupCreateTemporary), expiresAtUtc).ConfigureAwait(false);
+ }
+
public async Task> GroupCreateTempInvite(GroupDto group, int amount)
{
CheckConnection();