mirror of
https://github.com/oxen-io/session-android.git
synced 2025-08-25 10:12:50 +00:00
Order grid by latest speakers and prevent any unnecessary shifts.
This commit is contained in:

committed by
Greyson Parrelli

parent
db3098f633
commit
71be388989
@@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
import org.thoughtcrime.securesms.ringrtc.CameraState;
|
||||
import org.thoughtcrime.securesms.service.webrtc.collections.ParticipantCollection;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
@@ -29,7 +30,7 @@ public final class CallParticipantsState {
|
||||
|
||||
public static final CallParticipantsState STARTING_STATE = new CallParticipantsState(WebRtcViewModel.State.CALL_DISCONNECTED,
|
||||
WebRtcViewModel.GroupCallState.IDLE,
|
||||
Collections.emptyList(),
|
||||
new ParticipantCollection(SMALL_GROUP_MAX),
|
||||
CallParticipant.createLocal(CameraState.UNKNOWN, new BroadcastVideoSink(null), false),
|
||||
null,
|
||||
WebRtcLocalRenderState.GONE,
|
||||
@@ -40,7 +41,7 @@ public final class CallParticipantsState {
|
||||
|
||||
private final WebRtcViewModel.State callState;
|
||||
private final WebRtcViewModel.GroupCallState groupCallState;
|
||||
private final List<CallParticipant> remoteParticipants;
|
||||
private final ParticipantCollection remoteParticipants;
|
||||
private final CallParticipant localParticipant;
|
||||
private final CallParticipant focusedParticipant;
|
||||
private final WebRtcLocalRenderState localRenderState;
|
||||
@@ -51,7 +52,7 @@ public final class CallParticipantsState {
|
||||
|
||||
public CallParticipantsState(@NonNull WebRtcViewModel.State callState,
|
||||
@NonNull WebRtcViewModel.GroupCallState groupCallState,
|
||||
@NonNull List<CallParticipant> remoteParticipants,
|
||||
@NonNull ParticipantCollection remoteParticipants,
|
||||
@NonNull CallParticipant localParticipant,
|
||||
@Nullable CallParticipant focusedParticipant,
|
||||
@NonNull WebRtcLocalRenderState localRenderState,
|
||||
@@ -81,11 +82,7 @@ public final class CallParticipantsState {
|
||||
}
|
||||
|
||||
public @NonNull List<CallParticipant> getGridParticipants() {
|
||||
if (getAllRemoteParticipants().size() > SMALL_GROUP_MAX) {
|
||||
return getAllRemoteParticipants().subList(0, SMALL_GROUP_MAX);
|
||||
} else {
|
||||
return getAllRemoteParticipants();
|
||||
}
|
||||
return remoteParticipants.getGridParticipants();
|
||||
}
|
||||
|
||||
public @NonNull List<CallParticipant> getListParticipants() {
|
||||
@@ -94,14 +91,11 @@ public final class CallParticipantsState {
|
||||
if (isViewingFocusedParticipant && getAllRemoteParticipants().size() > 1) {
|
||||
listParticipants.addAll(getAllRemoteParticipants());
|
||||
listParticipants.remove(focusedParticipant);
|
||||
} else if (getAllRemoteParticipants().size() > SMALL_GROUP_MAX) {
|
||||
listParticipants.addAll(getAllRemoteParticipants().subList(SMALL_GROUP_MAX, getAllRemoteParticipants().size()));
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
listParticipants.addAll(remoteParticipants.getListParticipants());
|
||||
}
|
||||
|
||||
listParticipants.add(CallParticipant.EMPTY);
|
||||
|
||||
Collections.reverse(listParticipants);
|
||||
|
||||
return listParticipants;
|
||||
@@ -132,7 +126,7 @@ public final class CallParticipantsState {
|
||||
}
|
||||
|
||||
public @NonNull List<CallParticipant> getAllRemoteParticipants() {
|
||||
return remoteParticipants;
|
||||
return remoteParticipants.getAllParticipants();
|
||||
}
|
||||
|
||||
public @NonNull CallParticipant getLocalParticipant() {
|
||||
@@ -196,7 +190,7 @@ public final class CallParticipantsState {
|
||||
|
||||
return new CallParticipantsState(webRtcViewModel.getState(),
|
||||
webRtcViewModel.getGroupState(),
|
||||
webRtcViewModel.getRemoteParticipants(),
|
||||
oldState.remoteParticipants.getNext(webRtcViewModel.getRemoteParticipants()),
|
||||
webRtcViewModel.getLocalParticipant(),
|
||||
focused,
|
||||
localRenderState,
|
||||
|
@@ -0,0 +1,100 @@
|
||||
package org.thoughtcrime.securesms.service.webrtc.collections;
|
||||
|
||||
import androidx.annotation.CheckResult;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.annimon.stream.ComparatorCompat;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
import org.thoughtcrime.securesms.events.CallParticipantId;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents the participants to be displayed in the grid at any given time.
|
||||
*/
|
||||
public class ParticipantCollection {
|
||||
|
||||
private static final Comparator<CallParticipant> LEAST_RECENTLY_ADDED = (a, b) -> Long.compare(a.getAddedToCallTime(), b.getAddedToCallTime());
|
||||
private static final Comparator<CallParticipant> MOST_RECENTLY_SPOKEN = (a, b) -> Long.compare(b.getLastSpoke(), a.getLastSpoke());
|
||||
private static final Comparator<CallParticipant> MOST_RECENTLY_SPOKEN_THEN_LEAST_RECENTLY_ADDED = ComparatorCompat.chain(MOST_RECENTLY_SPOKEN).thenComparing(LEAST_RECENTLY_ADDED);
|
||||
|
||||
private final int maxGridCellCount;
|
||||
private final List<CallParticipant> participants;
|
||||
|
||||
public ParticipantCollection(int maxGridCellCount) {
|
||||
this(maxGridCellCount, Collections.emptyList());
|
||||
}
|
||||
|
||||
private ParticipantCollection(int maxGridCellCount, @NonNull List<CallParticipant> callParticipants) {
|
||||
this.maxGridCellCount = maxGridCellCount;
|
||||
this.participants = Collections.unmodifiableList(callParticipants);
|
||||
}
|
||||
|
||||
@CheckResult
|
||||
public @NonNull ParticipantCollection getNext(@NonNull List<CallParticipant> participants) {
|
||||
if (participants.isEmpty()) {
|
||||
return new ParticipantCollection(maxGridCellCount);
|
||||
} else if (this.participants.isEmpty()) {
|
||||
List<CallParticipant> newParticipants = new ArrayList<>(participants);
|
||||
Collections.sort(newParticipants, participants.size() <= maxGridCellCount ? LEAST_RECENTLY_ADDED : MOST_RECENTLY_SPOKEN_THEN_LEAST_RECENTLY_ADDED);
|
||||
|
||||
return new ParticipantCollection(maxGridCellCount, newParticipants);
|
||||
} else {
|
||||
List<CallParticipant> newParticipants = new ArrayList<>(participants);
|
||||
Collections.sort(newParticipants, MOST_RECENTLY_SPOKEN_THEN_LEAST_RECENTLY_ADDED);
|
||||
|
||||
List<CallParticipantId> oldGridParticipantIds = Stream.of(getGridParticipants())
|
||||
.map(CallParticipant::getCallParticipantId)
|
||||
.toList();
|
||||
|
||||
for (int i = 0; i < oldGridParticipantIds.size(); i++) {
|
||||
CallParticipantId oldId = oldGridParticipantIds.get(i);
|
||||
|
||||
int newIndex = Stream.of(newParticipants)
|
||||
.takeUntilIndexed((j, p) -> j >= maxGridCellCount)
|
||||
.map(CallParticipant::getCallParticipantId)
|
||||
.toList()
|
||||
.indexOf(oldId);
|
||||
|
||||
if (newIndex != -1 && newIndex != i) {
|
||||
Collections.swap(newParticipants, newIndex, i);
|
||||
}
|
||||
}
|
||||
|
||||
return new ParticipantCollection(maxGridCellCount, newParticipants);
|
||||
}
|
||||
}
|
||||
|
||||
public List<CallParticipant> getGridParticipants() {
|
||||
return participants.size() > maxGridCellCount
|
||||
? Collections.unmodifiableList(participants.subList(0, maxGridCellCount))
|
||||
: Collections.unmodifiableList(participants);
|
||||
}
|
||||
|
||||
public List<CallParticipant> getListParticipants() {
|
||||
return participants.size() > maxGridCellCount
|
||||
? Collections.unmodifiableList(participants.subList(maxGridCellCount, participants.size()))
|
||||
: Collections.emptyList();
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return participants.isEmpty();
|
||||
}
|
||||
|
||||
public List<CallParticipant> getAllParticipants() {
|
||||
return participants;
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return participants.size();
|
||||
}
|
||||
|
||||
public @NonNull CallParticipant get(int i) {
|
||||
return participants.get(i);
|
||||
}
|
||||
}
|
@@ -0,0 +1,218 @@
|
||||
package org.thoughtcrime.securesms.service.webrtc.collections;
|
||||
|
||||
import org.hamcrest.Matcher;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Test;
|
||||
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
import org.thoughtcrime.securesms.events.CallParticipantId;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
public class ParticipantCollectionTest {
|
||||
|
||||
private final ParticipantCollection testSubject = new ParticipantCollection(3);
|
||||
|
||||
@Test
|
||||
public void givenAnEmptyCollection_whenIAdd3Participants_thenIExpectThemToBeOrderedByAddedToCallTime() {
|
||||
// GIVEN
|
||||
List<CallParticipant> input = Arrays.asList(participant(1, 1, 4), participant(2, 1, 2), participant(3, 1, 3));
|
||||
|
||||
// WHEN
|
||||
ParticipantCollection result = testSubject.getNext(input);
|
||||
|
||||
// THEN
|
||||
assertThat(result.getGridParticipants(), Matchers.contains(id(2), id(3), id(1)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenAnEmptyCollection_whenIAdd3Participants_thenIExpectNoListParticipants() {
|
||||
// GIVEN
|
||||
List<CallParticipant> input = Arrays.asList(participant(1, 1, 4), participant(2, 1, 2), participant(3, 1, 3));
|
||||
|
||||
// WHEN
|
||||
ParticipantCollection result = testSubject.getNext(input);
|
||||
|
||||
// THEN
|
||||
assertEquals(result.getListParticipants().size(), 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenAnEmptyColletion_whenIAdd4Participants_thenIExpectThemToBeOrderedByLastSpokenThenAddedToCallTime() {
|
||||
// GIVEN
|
||||
List<CallParticipant> input = Arrays.asList(participant(1, 1, 2),
|
||||
participant(2, 5, 2),
|
||||
participant(3, 1, 1),
|
||||
participant(4, 1, 0));
|
||||
|
||||
// WHEN
|
||||
ParticipantCollection result = testSubject.getNext(input);
|
||||
|
||||
// THEN
|
||||
assertThat(result.getGridParticipants(), Matchers.contains(id(2), id(4), id(3)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenACollection_whenIUpdateWithEmptyList_thenIExpectEmptyList() {
|
||||
// GIVEN
|
||||
List<CallParticipant> initial = Arrays.asList(participant(1, 1, 2), participant(2, 1, 3), participant(3, 1, 4));
|
||||
ParticipantCollection initialCollection = testSubject.getNext(initial);
|
||||
List<CallParticipant> next = Collections.emptyList();
|
||||
|
||||
// WHEN
|
||||
ParticipantCollection result = initialCollection.getNext(next);
|
||||
|
||||
// THEN
|
||||
assertEquals(0, result.getGridParticipants().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenACollection_whenIUpdateWithLatestSpeakerAndSpeakerIsAlreadyInGridSection_thenIExpectTheSameGridSectionOrder() {
|
||||
// GIVEN
|
||||
List<CallParticipant> initial = Arrays.asList(participant(1, 1, 2), participant(2, 1, 3), participant(3, 1, 4));
|
||||
ParticipantCollection initialCollection = testSubject.getNext(initial);
|
||||
List<CallParticipant> next = Arrays.asList(participant(1, 1, 2), participant(2, 2, 3), participant(3, 1, 4));
|
||||
|
||||
// WHEN
|
||||
ParticipantCollection result = initialCollection.getNext(next);
|
||||
|
||||
// THEN
|
||||
assertThat(result.getGridParticipants(), Matchers.contains(id(1), id(2), id(3)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bigTest() {
|
||||
|
||||
// Welcome to the Thunder dome. 10 people enter...
|
||||
|
||||
ParticipantCollection testSubject = new ParticipantCollection(6);
|
||||
List<CallParticipant> init = Arrays.asList(participant(1, 1, 1), // Alice
|
||||
participant(2, 1, 1), // Bob
|
||||
participant(3, 1, 1), // Charlie
|
||||
participant(4, 1, 1), // Diane
|
||||
participant(5, 1, 1), // Ethel
|
||||
participant(6, 1, 1), // Francis
|
||||
participant(7, 1, 1), // Georgina
|
||||
participant(8, 1, 1), // Henry
|
||||
participant(9, 1, 1), // Ignace
|
||||
participant(10, 1, 1)); // Jericho
|
||||
|
||||
ParticipantCollection initialCollection = testSubject.getNext(init);
|
||||
|
||||
assertThat(initialCollection.getGridParticipants(), Matchers.contains(id(1), id(2), id(3), id(4), id(5), id(6)));
|
||||
assertThat(initialCollection.getListParticipants(), Matchers.contains(id(7), id(8), id(9), id(10)));
|
||||
|
||||
// Bob speaks about his trip to antigua...
|
||||
|
||||
List<CallParticipant> bobSpoke = Arrays.asList(participant(1, 1, 1),
|
||||
participant(2, 2, 1),
|
||||
participant(3, 1, 1),
|
||||
participant(4, 1, 1),
|
||||
participant(5, 1, 1),
|
||||
participant(6, 1, 1),
|
||||
participant(7, 1, 1),
|
||||
participant(8, 1, 1),
|
||||
participant(9, 1, 1),
|
||||
participant(10, 1, 1));
|
||||
|
||||
ParticipantCollection afterBobSpoke = initialCollection.getNext(bobSpoke);
|
||||
|
||||
assertThat(afterBobSpoke.getGridParticipants(), Matchers.contains(id(1), id(2), id(3), id(4), id(5), id(6)));
|
||||
assertThat(afterBobSpoke.getListParticipants(), Matchers.contains(id(7), id(8), id(9), id(10)));
|
||||
|
||||
// Henry interjects and says now is not the time, this is the thunderdome.
|
||||
|
||||
List<CallParticipant> henrySpoke = Arrays.asList(participant(1, 1, 1),
|
||||
participant(2, 2, 1),
|
||||
participant(3, 1, 1),
|
||||
participant(4, 1, 1),
|
||||
participant(5, 1, 1),
|
||||
participant(6, 1, 1),
|
||||
participant(7, 1, 1),
|
||||
participant(8, 3, 1),
|
||||
participant(9, 1, 1),
|
||||
participant(10, 1, 1));
|
||||
|
||||
ParticipantCollection afterHenrySpoke = afterBobSpoke.getNext(henrySpoke);
|
||||
|
||||
assertThat(afterHenrySpoke.getGridParticipants(), Matchers.contains(id(1), id(2), id(3), id(4), id(5), id(8)));
|
||||
assertThat(afterHenrySpoke.getListParticipants(), Matchers.contains(id(6), id(7), id(9), id(10)));
|
||||
|
||||
// Ignace asks how everone's holidays were
|
||||
|
||||
List<CallParticipant> ignaceSpoke = Arrays.asList(participant(1, 1, 1),
|
||||
participant(2, 2, 1),
|
||||
participant(3, 1, 1),
|
||||
participant(4, 1, 1),
|
||||
participant(5, 1, 1),
|
||||
participant(6, 1, 1),
|
||||
participant(7, 1, 1),
|
||||
participant(8, 3, 1),
|
||||
participant(9, 4, 1),
|
||||
participant(10, 1, 1));
|
||||
|
||||
ParticipantCollection afterIgnaceSpoke = afterHenrySpoke.getNext(ignaceSpoke);
|
||||
|
||||
assertThat(afterIgnaceSpoke.getGridParticipants(), Matchers.contains(id(1), id(2), id(3), id(4), id(9), id(8)));
|
||||
assertThat(afterIgnaceSpoke.getListParticipants(), Matchers.contains(id(5), id(6), id(7), id(10)));
|
||||
|
||||
// Alice is the first to fall
|
||||
|
||||
List<CallParticipant> aliceLeft = Arrays.asList(participant(2, 2, 1),
|
||||
participant(3, 1, 1),
|
||||
participant(4, 1, 1),
|
||||
participant(5, 1, 1),
|
||||
participant(6, 1, 1),
|
||||
participant(7, 1, 1),
|
||||
participant(8, 3, 1),
|
||||
participant(9, 4, 1),
|
||||
participant(10, 1, 1));
|
||||
|
||||
ParticipantCollection afterAliceLeft = afterIgnaceSpoke.getNext(aliceLeft);
|
||||
|
||||
assertThat(afterAliceLeft.getGridParticipants(), Matchers.contains(id(5), id(2), id(3), id(4), id(9), id(8)));
|
||||
assertThat(afterAliceLeft.getListParticipants(), Matchers.contains(id(6), id(7), id(10)));
|
||||
|
||||
// Just kidding, Alice is back. Georgina and Charlie gasp!
|
||||
|
||||
List<CallParticipant> mixUp = Arrays.asList(participant(1, 1, 5),
|
||||
participant(2, 2, 1),
|
||||
participant(3, 6, 1),
|
||||
participant(4, 1, 1),
|
||||
participant(5, 1, 1),
|
||||
participant(6, 1, 1),
|
||||
participant(7, 5, 1),
|
||||
participant(8, 3, 1),
|
||||
participant(9, 4, 1),
|
||||
participant(10, 1, 1));
|
||||
|
||||
ParticipantCollection afterMixUp = afterAliceLeft.getNext(mixUp);
|
||||
|
||||
assertThat(afterMixUp.getGridParticipants(), Matchers.contains(id(7), id(2), id(3), id(4), id(9), id(8)));
|
||||
assertThat(afterMixUp.getListParticipants(), Matchers.contains(id(5), id(6), id(10), id(1)));
|
||||
}
|
||||
|
||||
private Matcher<CallParticipant> id(long serializedId) {
|
||||
return Matchers.hasProperty("callParticipantId", Matchers.equalTo(new CallParticipantId(serializedId, RecipientId.from(serializedId))));
|
||||
}
|
||||
|
||||
private static CallParticipant participant(long serializedId,long lastSpoke, long added) {
|
||||
return CallParticipant.createRemote(
|
||||
new CallParticipantId(serializedId, RecipientId.from(serializedId)),
|
||||
Recipient.UNKNOWN,
|
||||
null,
|
||||
new BroadcastVideoSink(null),
|
||||
false,
|
||||
false,
|
||||
lastSpoke,
|
||||
false,
|
||||
added);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user