Skip to content
Closed
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
2 changes: 2 additions & 0 deletions Explorer/Assets/DCL/EventsApi/EventDTO.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using UnityEngine;

// ReSharper disable InconsistentNaming
namespace DCL.EventsApi
{
public interface IEventDTO : ISerializationCallbackReceiver
Expand Down Expand Up @@ -34,6 +35,7 @@ public interface IEventDTO : ISerializationCallbackReceiver
int Y {get; set; }
}

// Server schema: decentraland/events src/entities/Event/types.ts#/EventAttributes
[Serializable]
public struct EventDTO : IEventDTO
{
Expand Down
25 changes: 25 additions & 0 deletions Explorer/Assets/DCL/EventsApi/EventDisplayOrderComparer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.Collections.Generic;

namespace DCL.EventsApi
{
/// <summary>
/// Total display order for event lists: live events first, then soonest next occurrence.
/// Ids break the remaining ties, so lists with equal sort keys always render in the same order
/// even under an unstable sort.
/// </summary>
public sealed class EventDisplayOrderComparer : IComparer<EventDTO>
{
public static readonly EventDisplayOrderComparer INSTANCE = new ();

private EventDisplayOrderComparer() { }

public int Compare(EventDTO x, EventDTO y)
{
if (x.live != y.live)
return x.live ? -1 : 1;

int byNextStart = x.NextStartAtProcessed.CompareTo(y.NextStartAtProcessed);
return byNextStart != 0 ? byNextStart : string.CompareOrdinal(x.id, y.id);
}
}
}
11 changes: 11 additions & 0 deletions Explorer/Assets/DCL/EventsApi/EventDisplayOrderComparer.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions Explorer/Assets/DCL/EventsApi/HttpEventsApiService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public async UniTask<IReadOnlyList<EventDTO>> GetEventsAsync(CancellationToken c
return await FetchEventListAsync(urlBuilder.Build(), ct);
}

public async UniTask<IReadOnlyList<EventDTO>> GetEventsByParcelAsync(IEnumerable<Vector2Int> parcels, CancellationToken ct, bool onlyLiveEvents = false)
public async UniTask<EventDTO[]> GetEventsByParcelAsync(IEnumerable<Vector2Int> parcels, CancellationToken ct, bool onlyLiveEvents = false)
{
urlBuilder.Clear();
urlBuilder.AppendDomain(baseUrl);
Expand Down Expand Up @@ -155,7 +155,7 @@ public async UniTask<EventWithPlaceIdDTOListResponse> GetCommunityEventsAsync(st
urlBuilder.AppendDomain(baseUrl)
.AppendParameter(new URLParameter(COMMUNITY_ID_PARAMETER, communityId))
.AppendParameter(new URLParameter(PAGINATION_LIMIT_PARAMETER, elementsPerPage.ToString()))
.AppendParameter(new URLParameter(PAGINATION_OFFSET_PARAMETER, ((pageNumber - 1) * elementsPerPage).ToString()));;
.AppendParameter(new URLParameter(PAGINATION_OFFSET_PARAMETER, ((pageNumber - 1) * elementsPerPage).ToString()));

return await webRequestController
.SignedFetchGetAsync(urlBuilder.Build(), string.Empty, ct)
Expand Down Expand Up @@ -202,7 +202,7 @@ public async UniTask MarkAsNotInterestedAsync(string eventId, CancellationToken
throw new EventsApiException($"Error on trying to create attend intention to event {eventId}");
}

private async UniTask<IReadOnlyList<EventDTO>> FetchEventListAsync(URLAddress url, CancellationToken ct)
private async UniTask<EventDTO[]> FetchEventListAsync(URLAddress url, CancellationToken ct)
{
ulong timestamp = DateTime.UtcNow.UnixTimeAsMilliseconds();

Expand Down
12 changes: 10 additions & 2 deletions Explorer/Assets/DCL/Navmap/PlaceInfoPanelController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -434,17 +434,24 @@ async UniTaskVoid FetchEventsAndShowThemAsync(CancellationToken ct)
{
view.EmptyEventsContainer.SetActive(false);

if (place == null) return;
PlacesData.PlaceInfo placeInfo = place;

SetAsLoadingState();

IReadOnlyList<EventDTO> events = await eventsApiService.GetEventsByParcelAsync(place!.Positions, ct);
EventDTO[] events = await eventsApiService.GetEventsByParcelAsync(placeInfo.Positions, ct);

ClearEventElements();

view.EmptyEventsContainer.SetActive(events.Count == 0);
view.EmptyEventsContainer.SetActive(events.Length == 0);

Array.Sort(events, EventDisplayOrderComparer.INSTANCE);

foreach (EventDTO @event in events)
{
EventElementView element = eventElementPool.Get();
// Pooled views keep their previous sibling slot; re-append so the on-screen order matches the data order.
element.transform.SetAsLastSibling();
element.Init(imageControllerProvider);
eventElements.Add(element);

Expand Down Expand Up @@ -502,6 +509,7 @@ void SetAsLoadingState()
for (var i = 0; i < 8; i++)
{
EventElementView element = eventElementPool.Get();
element.transform.SetAsLastSibling();
eventElements.Add(element);
}
}
Expand Down
129 changes: 129 additions & 0 deletions Explorer/Assets/DCL/Tests/Editor/EventDisplayOrderComparerShould.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
using DCL.EventsApi;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Globalization;

namespace DCL.Tests.Editor
{
// Order contract behind the Events-tab fix for
// https://github.com/decentraland/unity-explorer/issues/9529: the comparer must define a
// total order (id breaks every remaining tie) so that Array.Sort - which is unstable -
// still produces the same sequence for the same content, no matter how the backend
// happened to permute the response.
[TestFixture]
public class EventDisplayOrderComparerShould
{
private static readonly string[] TIMES =
{
"2026-08-10T10:00:00Z",
"2026-08-11T09:30:00Z",
"2026-08-12T18:00:00Z",
};

private static EventDTO Event(string id, bool live, string nextStartAt) =>
new ()
{
id = id,
live = live,
NextStartAtProcessed = DateTime.Parse(nextStartAt, null, DateTimeStyles.RoundtripKind),
};

[Test]
public void PutLiveEventsBeforeUpcomingOnes()
{
EventDTO[] events =
{
Event("upcoming-soon", live: false, TIMES[0]),
Event("live-later", live: true, TIMES[2]),
};

Array.Sort(events, EventDisplayOrderComparer.INSTANCE);

Assert.AreEqual("live-later", events[0].id,
"A live event must come first even when its next occurrence is later than an upcoming event's.");
}

[Test]
public void OrderBySoonestNextOccurrenceWithinTheSameLiveState()
{
EventDTO[] events =
{
Event("c", live: false, TIMES[2]),
Event("a", live: false, TIMES[0]),
Event("b", live: false, TIMES[1]),
};

Array.Sort(events, EventDisplayOrderComparer.INSTANCE);

Assert.AreEqual(new[] { "a", "b", "c" }, Ids(events));
}

[Test]
public void BreakFullKeyTiesByIdSoTheOrderIsTotal()
{
EventDTO[] events =
{
Event("zeta", live: false, TIMES[1]),
Event("alpha", live: false, TIMES[1]),
Event("mid", live: false, TIMES[1]),
};

Array.Sort(events, EventDisplayOrderComparer.INSTANCE);

Assert.AreEqual(new[] { "alpha", "mid", "zeta" }, Ids(events));
}

[Test]
public void ProduceTheSameSequenceForEveryInputPermutation()
{
// Includes same-timestamp pairs in both live states: exactly the inputs where an
// unstable sort without a tiebreaker reshuffles between reopens.
EventDTO[] content =
{
Event("live-tie-1", live: true, TIMES[0]),
Event("live-tie-2", live: true, TIMES[0]),
Event("upcoming-tie-1", live: false, TIMES[1]),
Event("upcoming-tie-2", live: false, TIMES[1]),
Event("upcoming-late", live: false, TIMES[2]),
Event("live-late", live: true, TIMES[2]),
};

string[] expected = { "live-tie-1", "live-tie-2", "live-late", "upcoming-tie-1", "upcoming-tie-2", "upcoming-late" };

// Rotations plus a reversed copy stand in for "whatever order the backend returned this time".
for (var shift = 0; shift < content.Length; shift++)
{
EventDTO[] permutation = Rotate(content, shift);
Array.Sort(permutation, EventDisplayOrderComparer.INSTANCE);

Assert.AreEqual(expected, Ids(permutation), $"Rotation by {shift} sorted to a different sequence.");
}

EventDTO[] reversed = Rotate(content, 0);
Array.Reverse(reversed);
Array.Sort(reversed, EventDisplayOrderComparer.INSTANCE);
Assert.AreEqual(expected, Ids(reversed), "The reversed input sorted to a different sequence.");
}

private static string[] Ids(IReadOnlyList<EventDTO> events)
{
var ids = new string[events.Count];

for (var i = 0; i < events.Count; i++)
ids[i] = events[i].id;

return ids;
}

private static EventDTO[] Rotate(EventDTO[] source, int shift)
{
var rotated = new EventDTO[source.Length];

for (var i = 0; i < source.Length; i++)
rotated[i] = source[(i + shift) % source.Length];

return rotated;
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading