From 78089a9fc7f1fe209f592db8f308ea84d256f8f1 Mon Sep 17 00:00:00 2001 From: Keda Date: Sat, 20 Sep 2025 12:39:18 +0200 Subject: [PATCH] Update 0.1.8 - Fix interface & ajout syncshell perma/temp --- MareAPI | 2 +- MareSynchronos/MareSynchronos.csproj | 2 +- .../PlayerData/Pairs/PairManager.cs | 15 +- MareSynchronos/Plugin.cs | 2 + .../TemporarySyncshellNotificationService.cs | 225 ++++++++++++++++++ MareSynchronos/UI/Components/GroupPanel.cs | 128 +++++++++- MareSynchronos/UI/DtrEntry.cs | 7 +- MareSynchronos/UI/SettingsUi.cs | 47 +++- MareSynchronos/UmbraSync.json | 6 +- .../SignalR/ApiController.Functions.Groups.cs | 9 +- 10 files changed, 408 insertions(+), 35 deletions(-) create mode 100644 MareSynchronos/Services/TemporarySyncshellNotificationService.cs 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();