Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions FlexibleContactsSort/ContactsSortingConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,30 +12,44 @@ namespace FlexibleContactsSort
{
internal sealed class ContactsSortingConfig : ConfigSection
{
private static readonly ConfigKeyQuantity<int, Time> _cooldownTimeFormat = new(new UnitConfiguration("s", "0", " ", ["m", "s"]), null, 0, int.MaxValue);

private readonly DefiningConfigKey<int> _alphabeticPriorityKey = new("AlphabeticPriority", "Priority of the contact's name. Set 0 to ignore; negative to invert.", () => 1);
private readonly DefiningConfigKey<int> _headlessPriorityKey = new("HeadlessPriority", "Priority of the contact being an active headless host. Set 0 to ignore; negative to invert.", () => 100_000);
private readonly DefiningConfigKey<bool> _hideOffline = new("HideOffline", "Hide offline contacts completely.", () => false);
private readonly DefiningConfigKey<int> _incomingContactRequestPriorityKey = new("IncomingContactRequestPriority", "Priority of the contact being an incoming request. Set 0 to ignore; negative to invert.", () => -1_000_000);
private readonly DefiningConfigKey<int> _joinablePriorityKey = new("JoinablePriority", "Priority of the contact being in a session you can join. Set 0 to ignore; negative to invert.", () => -10_000);

private readonly DefiningConfigKey<bool> _keepPinnedOffline = new("KeepPinnedOffline", "Do not hide pinned contacts, even if they're offline.", () => true);

private readonly DefiningConfigKey<int> _offlineCooldown = new("OfflineCooldown", "Delay before a contact that has just gone offline is counted as such. Set to 0 to disable.", () => 120)
{
_cooldownTimeFormat
};

private readonly DefiningConfigKey<int> _onlineStatusPriorityKey = new("OnlineStatusPriority", "Priority of the contact's online status. Set 0 to ignore; negative to invert.", () => 1_000);
private readonly DefiningConfigKey<int> _outgoingContactRequestPriorityKey = new("OutgoingContactRequestPriority", "Priority of the contact being an outgoing request. Set 0 to ignore; negative to invert.", () => 1_000_000);
private readonly DefiningConfigKey<HashSet<string>> _pinnedContactsKey = new("PinnedContacts", "List of Contacts to always keep at the top.", () => new(), internalAccessOnly: true);
private readonly DefiningConfigKey<HashSet<string>> _pinnedContactsKey = new("PinnedContacts", "List of Contacts to always keep at the top.", () => [], internalAccessOnly: true);

private readonly DefiningConfigKey<int> _readMessageCooldownKey = new("ReadMessageCooldown", "Delay before a contact with freshly-read messages is counted as such. Set 0 to disable.", () => 120)
{
new ConfigKeyQuantity<int, Time>(new UnitConfiguration("s", "0", " ", new[] { "m", "s" }), null, 0, int.MaxValue )
_cooldownTimeFormat
};

public int AlphabeticPriority => _alphabeticPriorityKey.GetValue();
public override string Description => "Contains options for how to sort the Contacts list.";
public int HeadlessPriority => _headlessPriorityKey.GetValue();
public bool HideOffline => _hideOffline;
public override string Id => "ContactsSorting";
public int IncomingContactRequestPriority => _incomingContactRequestPriorityKey.GetValue();
public int JoinablePriority => _joinablePriorityKey.GetValue();
public bool KeepPinnedOffline => _keepPinnedOffline;
public override string Name => "Contact Sorting";
public int OfflineCooldown => _offlineCooldown;
public int OnlineStatusPriority => _onlineStatusPriorityKey.GetValue();
public int OutgoingContactRequestPriority => _outgoingContactRequestPriorityKey.GetValue();
public HashSet<string> PinnedContacts => _pinnedContactsKey.GetValue()!;
public int ReadMessageCooldown => _readMessageCooldownKey.GetValue();
public override Version Version { get; } = new(1, 1, 0);
public override Version Version { get; } = new(1, 2, 0);
}
}
72 changes: 60 additions & 12 deletions FlexibleContactsSort/FlexibleContactSorting.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using FrooxEngine;
using FrooxEngine.UIX;
using HarmonyLib;
using MonkeyLoader.Patching;
using MonkeyLoader.Resonite;
using SkyFrost.Base;
using System;
Expand All @@ -16,9 +15,11 @@ namespace FlexibleContactsSort
[HarmonyPatchCategory(nameof(FlexibleContactSorting))]
internal sealed class FlexibleContactSorting : ConfiguredResoniteMonkey<FlexibleContactSorting, ContactsSortingConfig>
{
private static readonly Dictionary<ContactItem, string> _contactIds = new();
private static readonly ConditionalWeakTable<ContactItem, ActivityTracker> _activityTrackersByContact = [];
private static readonly Dictionary<ContactItem, string> _contactIds = [];

private static readonly ConditionalWeakTable<ContactItem, ReadMessageTracker> _contactReadMessageTrackers = new();
private static readonly LocaleString _pinString = Mod.GetLocaleString("Pin");
private static readonly LocaleString _unpinString = Mod.GetLocaleString("Unpin");

public override bool CanBeDisabled => true;

Expand Down Expand Up @@ -73,6 +74,9 @@ private static int CalculateOrderScore((ContactData, bool) contactSortInfo)

private static int Compare(Slot slot1, Slot slot2)
{
if (slot1.ActiveSelf ^ slot2.ActiveSelf)
return slot1.ActiveSelf ? -1 : 1;

var contactItem1 = slot1.GetComponent<ContactItem>();
var contactItem2 = slot2.GetComponent<ContactItem>();

Expand All @@ -94,7 +98,7 @@ private static int GetOnlineStatusOrder(ContactData contactData)

private static bool HasUnreadMessages(ContactItem contactItem)
{
var readMessageTracker = _contactReadMessageTrackers.GetOrCreateValue(contactItem);
var readMessageTracker = _activityTrackersByContact.GetOrCreateValue(contactItem);

if (contactItem.HasMessages)
{
Expand All @@ -114,7 +118,7 @@ private static bool IsIncomingRequest(Contact contact)
=> contact.ContactStatus == ContactStatus.Requested;

private static bool IsInJoinableSession(ContactData contactData)
=> contactData?.CurrentSessionInfo is SessionInfo session
=> contactData?.CurrentSessionInfo is not null
&& !IsHeadlessHost(contactData);

private static bool IsOfflineContact(ContactData contactData)
Expand All @@ -130,9 +134,36 @@ private static bool IsOutgoingRequest(Contact contact)
[HarmonyPatch(nameof(ContactsDialog.OnCommonUpdate))]
private static void OnCommonUpdatePostfix(ContactsDialog __instance, bool __state)
{
if (!__state || !LagFreeContactsLoading.AllowSorting)
return;

if (string.IsNullOrWhiteSpace(__instance._searchBar.Target.Text.Content))
{
if (ConfigSection.HideOffline)
{
foreach (var contactSlot in __instance._listRoot.Target.Children)
{
var contactItem = contactSlot.GetComponent<ContactItem>();

if (contactItem?.Data is null)
continue;

var activityTracker = _activityTrackersByContact.GetOrCreateValue(contactItem);
var isOffline = activityTracker.IsOffline = IsOfflineContact(contactItem.Data);

contactSlot.ActiveSelf = !isOffline || activityTracker.SecondsSinceOffline < ConfigSection.OfflineCooldown
|| (ConfigSection.KeepPinnedOffline && ConfigSection.PinnedContacts.Contains(contactItem.Data.Contact.ContactUserId));
}
}
else
{
foreach (var contactSlot in __instance._listRoot.Target.Children)
contactSlot.ActiveSelf = true;
}
}

// Sort only if Resonite would have sorted (but we prevented it)
if (__state && LagFreeContactsLoading.AllowSorting)
__instance._listRoot.Target.SortChildren(Compare);
__instance._listRoot.Target.SortChildren(Compare);
}

[HarmonyPrefix]
Expand All @@ -151,29 +182,32 @@ private static void OnCommonUpdatePrefix(ContactsDialog __instance, out bool __s
}

[HarmonyPostfix]
[HarmonyPatch("UpdateSelectedContact")]
[HarmonyPatch(nameof(ContactsDialog.UpdateSelectedContactUI))]
private static void UpdateSelectedContactPostfix(ContactsDialog __instance, UIBuilder ___actionsUi)
{
if (__instance.SelectedContact is null || __instance.SelectedContactId == __instance.Cloud.Platform.AppUserId || __instance.SelectedContact.IsSelfContact)
return;

var pinButton = ___actionsUi.Button((ConfigSection.PinnedContacts.Contains(__instance.SelectedContactId) ? "FlexibleContactsSort.Unpin" : "FlexibleContactsSort.Pin").AsLocaleKey());
var pinButton = ___actionsUi.Button(ConfigSection.PinnedContacts.Contains(__instance.SelectedContactId) ? _unpinString : _pinString);
pinButton.LocalPressed += (button, data) =>
{
if (ConfigSection.PinnedContacts.Add(__instance.SelectedContactId))
{
((Text)pinButton.LabelTextField.Parent).LocaleContent = "FlexibleContactsSort.Unpin".AsLocaleKey();
((Text)pinButton.LabelTextField.Parent).LocaleContent = _unpinString;
return;
}

ConfigSection.PinnedContacts.Remove(__instance.SelectedContactId);
((Text)pinButton.LabelTextField.Parent).LocaleContent = "FlexibleContactsSort.Pin".AsLocaleKey();
((Text)pinButton.LabelTextField.Parent).LocaleContent = _pinString;
};
}

private sealed class ReadMessageTracker
private sealed class ActivityTracker
{
private bool _hasMessages;
private bool _isOffline = true;

private DateTime _offlineTime = DateTime.MinValue;
private DateTime _readTime = DateTime.MinValue;

public bool HasMessages
Expand All @@ -188,6 +222,20 @@ public bool HasMessages
}
}

public bool IsOffline
{
get => _isOffline;
set
{
if (_isOffline && !value)
_offlineTime = DateTime.UtcNow;

_isOffline = value;
}
}

public double SecondsSinceOffline => (DateTime.UtcNow - _offlineTime).TotalSeconds;

public double SecondsSinceRead => (DateTime.UtcNow - _readTime).TotalSeconds;
}
}
Expand Down
2 changes: 1 addition & 1 deletion FlexibleContactsSort/FlexibleContactsSort.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<PackageId>FlexibleContactSorting</PackageId>
<Title>Flexible Contact Sorting</Title>
<Authors>Banane9</Authors>
<Version>0.7.0-beta</Version>
<Version>0.8.0-beta</Version>
<Description>This MonkeyLoader mod for Resonite allows sorting contacts flexibly and to your liking, including pinning your favorites to the top. It also adds other Quality of Life features to the Contacts Page, such as a clear button for the search, an extra color for your outgoing conctact requests, capacity display to contacts' sessions, and contacts loading without lag.</Description>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageLicenseExpression>LGPL-3.0-or-later</PackageLicenseExpression>
Expand Down
3 changes: 1 addition & 2 deletions FlexibleContactsSort/LagFreeContactsLoading.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,8 @@
private const int ContactsPerUpdate = 8;

public override bool CanBeDisabled => true;
internal static bool AllowSorting { get; private set; } = true;

protected override IEnumerable<IFeaturePatch> GetFeaturePatches() => Enumerable.Empty<IFeaturePatch>();
internal static bool AllowSorting { get; private set; } = true;

private static void AddContactItems(ContactsDialog contactsDialog)
=> contactsDialog.StartTask(async () =>
Expand Down Expand Up @@ -92,7 +91,7 @@

private static bool RemoveItem(ContactItem contact, string? searchTerm, bool clear)
{
if (clear || !(contact.Username.StartsWith(searchTerm)

Check warning on line 94 in FlexibleContactsSort/LagFreeContactsLoading.cs

View workflow job for this annotation

GitHub Actions / Build

Possible null reference argument for parameter 'value' in 'bool string.StartsWith(string value)'.

Check warning on line 94 in FlexibleContactsSort/LagFreeContactsLoading.cs

View workflow job for this annotation

GitHub Actions / Build

Possible null reference argument for parameter 'value' in 'bool string.StartsWith(string value)'.
|| contact.AlternateNames.Any(name => name.StartsWith(searchTerm, StringComparison.InvariantCulture))))
{
contact.Slot.Destroy();
Expand Down Expand Up @@ -140,7 +139,7 @@
foreach (var contactItem in __instance._contactItems)
{
contactItem.Value.Slot.ActiveSelf = clear
|| contactItem.Value.Username.StartsWith(searchTerm, StringComparison.InvariantCultureIgnoreCase)

Check warning on line 142 in FlexibleContactsSort/LagFreeContactsLoading.cs

View workflow job for this annotation

GitHub Actions / Build

Possible null reference argument for parameter 'value' in 'bool string.StartsWith(string value, StringComparison comparisonType)'.

Check warning on line 142 in FlexibleContactsSort/LagFreeContactsLoading.cs

View workflow job for this annotation

GitHub Actions / Build

Possible null reference argument for parameter 'value' in 'bool string.StartsWith(string value, StringComparison comparisonType)'.
|| contactItem.Value.AlternateNames.Any(name => name.StartsWith(searchTerm, StringComparison.InvariantCulture));
}
});
Expand Down
4 changes: 2 additions & 2 deletions FlexibleContactsSort/Locale/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"localeCode": "de",
"authors": [ "Banane9" ],
"messages": {
"FlexibleContactsSort.Pin": "Kontakt anheften",
"FlexibleContactsSort.Unpin": "Kontakt lösen"
"FlexibleContactSorting.Pin": "Kontakt anheften",
"FlexibleContactSorting.Unpin": "Kontakt lösen"
}
}
4 changes: 2 additions & 2 deletions FlexibleContactsSort/Locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"localeCode": "en",
"authors": [ "Banane9" ],
"messages": {
"FlexibleContactsSort.Pin": "Pin Contact",
"FlexibleContactsSort.Unpin": "Unpin Contact"
"FlexibleContactSorting.Pin": "Pin Contact",
"FlexibleContactSorting.Unpin": "Unpin Contact"
}
}
4 changes: 2 additions & 2 deletions FlexibleContactsSort/Locale/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"localeCode": "es",
"authors": [ "Raptoranim" ],
"messages": {
"FlexibleContactsSort.Pin": "Anclar Contacto",
"FlexibleContactsSort.Unpin": "Desanclar Contacto"
"FlexibleContactSorting.Pin": "Anclar Contacto",
"FlexibleContactSorting.Unpin": "Desanclar Contacto"
}
}
4 changes: 2 additions & 2 deletions FlexibleContactsSort/Locale/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"localeCode": "fr",
"authors": [ "j4" ],
"messages": {
"FlexibleContactsSort.Pin": "Épingler le contact",
"FlexibleContactsSort.Unpin": "Désépingler le contact"
"FlexibleContactSortinging.Pin": "Épingler le contact",
"FlexibleContactSorting.Unpin": "Désépingler le contact"
}
}
4 changes: 2 additions & 2 deletions FlexibleContactsSort/Locale/jp.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"localeCode": "jp",
"authors": [ "Aesc" ],
"messages": {
"FlexibleContactsSort.Pin": "ピン留めする",
"FlexibleContactsSort.Unpin": "ピン留めを外す"
"FlexibleContactSorting.Pin": "ピン留めする",
"FlexibleContactSorting.Unpin": "ピン留めを外す"
}
}
4 changes: 2 additions & 2 deletions FlexibleContactsSort/Locale/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"localeCode": "ko",
"authors": [ "Sinduy" ],
"messages": {
"FlexibleContactsSort.Pin": "연락처 고정",
"FlexibleContactsSort.Unpin": "연락처 고정해제"
"FlexibleContactSorting.Pin": "연락처 고정",
"FlexibleContactSorting.Unpin": "연락처 고정해제"
}
}
4 changes: 2 additions & 2 deletions FlexibleContactsSort/Locale/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"localeCode": "nl",
"authors": [ "zahndy" ],
"messages": {
"FlexibleContactsSort.Pin": "Contact Vastpinnen",
"FlexibleContactsSort.Unpin": "Contact Losmaken"
"FlexibleContactSorting.Pin": "Contact Vastpinnen",
"FlexibleContactSorting.Unpin": "Contact Losmaken"
}
}
4 changes: 2 additions & 2 deletions FlexibleContactsSort/Locale/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"localeCode": "pt",
"authors": [ "LucasRo7" ],
"messages": {
"FlexibleContactsSort.Pin": "Fixar Contato",
"FlexibleContactsSort.Unpin": "Soltar Contato"
"FlexibleContactSorting.Pin": "Fixar Contato",
"FlexibleContactSorting.Unpin": "Soltar Contato"
}
}