Improve group update copy and implement speaker indexing.

fork-5.53.8
Cody Henthorne 2020-11-24 13:23:25 -05:00 zatwierdzone przez Alex Hart
rodzic ce68da1613
commit a640d9e298
15 zmienionych plików z 145 dodań i 52 usunięć

Wyświetl plik

@ -5,12 +5,12 @@ import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.annimon.stream.ComparatorCompat;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.ringrtc.CameraState;
import java.util.ArrayList;
@ -171,7 +171,10 @@ public final class CallParticipantsState {
webRtcViewModel.getRemoteParticipants().size(),
oldState.isViewingFocusedParticipant);
CallParticipant focused = oldState.remoteParticipants.isEmpty() ? null : oldState.remoteParticipants.get(0);
List<CallParticipant> participantsByLastSpoke = new ArrayList<>(webRtcViewModel.getRemoteParticipants());
Collections.sort(participantsByLastSpoke, ComparatorCompat.reversed((p1, p2) -> Long.compare(p1.getLastSpoke(), p2.getLastSpoke())));
CallParticipant focused = participantsByLastSpoke.isEmpty() ? null : participantsByLastSpoke.get(0);
return new CallParticipantsState(webRtcViewModel.getState(),
webRtcViewModel.getGroupState(),

Wyświetl plik

@ -183,7 +183,7 @@ public final class ConversationUpdateItem extends LinearLayout
});
} else if (conversationMessage.getMessageRecord().isGroupCall()) {
UpdateDescription updateDescription = MessageRecord.getGroupCallUpdateDescription(getContext(), conversationMessage.getMessageRecord().getBody());
UpdateDescription updateDescription = MessageRecord.getGroupCallUpdateDescription(getContext(), conversationMessage.getMessageRecord().getBody(), true);
Collection<UUID> uuids = updateDescription.getMentioned();
int text = 0;

Wyświetl plik

@ -462,7 +462,7 @@ public final class ConversationListItem extends RelativeLayout
} else if (SmsDatabase.Types.isMissedVideoCall(thread.getType())) {
return emphasisAdded(context, context.getString(R.string.ThreadRecord_missed_video_call), defaultTint);
} else if (MmsSmsColumns.Types.isGroupCall(thread.getType())) {
return emphasisAdded(context, MessageRecord.getGroupCallUpdateDescription(context, thread.getBody()), defaultTint);
return emphasisAdded(context, MessageRecord.getGroupCallUpdateDescription(context, thread.getBody(), false), defaultTint);
} else if (SmsDatabase.Types.isJoinedType(thread.getType())) {
return emphasisAdded(recipientToStringAsync(thread.getRecipient().getId(), r -> new SpannableString(context.getString(R.string.ThreadRecord_s_is_on_signal, r.getDisplayName(context)))));
} else if (SmsDatabase.Types.isExpirationTimerUpdate(thread.getType())) {

Wyświetl plik

@ -687,11 +687,11 @@ public class SmsDatabase extends MessageDatabase {
@Override
public void insertOrUpdateGroupCall(@NonNull RecipientId groupRecipientId,
@NonNull RecipientId sender,
long timestamp,
@Nullable String messageGroupCallEraId,
@Nullable String peekGroupCallEraId,
@NonNull Collection<UUID> peekJoinedUuids)
@NonNull RecipientId sender,
long timestamp,
@Nullable String messageGroupCallEraId,
@Nullable String peekGroupCallEraId,
@NonNull Collection<UUID> peekJoinedUuids)
{
SQLiteDatabase db = databaseHelper.getWritableDatabase();
@ -704,6 +704,9 @@ public class SmsDatabase extends MessageDatabase {
boolean peerEraIdSameAsPrevious = updatePreviousGroupCall(threadId, peekGroupCallEraId, peekJoinedUuids);
if (!peerEraIdSameAsPrevious && !Util.isEmpty(peekGroupCallEraId)) {
Recipient self = Recipient.self();
boolean markRead = peekJoinedUuids.contains(self.requireUuid()) || self.getId().equals(sender);
byte[] updateDetails = GroupCallUpdateDetails.newBuilder()
.setEraId(Util.emptyIfNull(peekGroupCallEraId))
.setStartedCallUuid(Recipient.resolved(sender).requireUuid().toString())
@ -719,7 +722,7 @@ public class SmsDatabase extends MessageDatabase {
values.put(ADDRESS_DEVICE_ID, 1);
values.put(DATE_RECEIVED, timestamp);
values.put(DATE_SENT, timestamp);
values.put(READ, 0);
values.put(READ, markRead ? 1 : 0);
values.put(BODY, body);
values.put(TYPE, Types.GROUP_CALL_TYPE);
values.put(THREAD_ID, threadId);
@ -753,6 +756,7 @@ public class SmsDatabase extends MessageDatabase {
GroupCallUpdateDetails groupCallUpdateDetails = GroupCallUpdateDetailsUtil.parse(record.getBody());
boolean sameEraId = groupCallUpdateDetails.getEraId().equals(peekGroupCallEraId) && !Util.isEmpty(peekGroupCallEraId);
boolean containsSelf = peekJoinedUuids.contains(Recipient.self().requireUuid());
List<String> inCallUuids = sameEraId ? Stream.of(peekJoinedUuids).map(UUID::toString).toList()
: Collections.emptyList();
@ -762,6 +766,10 @@ public class SmsDatabase extends MessageDatabase {
ContentValues contentValues = new ContentValues();
contentValues.put(BODY, body);
if (sameEraId && containsSelf) {
contentValues.put(READ, 1);
}
db.update(TABLE_NAME, contentValues, ID_WHERE, SqlUtil.buildArgs(record.getId()));
return sameEraId;

Wyświetl plik

@ -25,12 +25,18 @@ import java.util.UUID;
public class GroupCallUpdateMessageFactory implements UpdateDescription.StringFactory {
private final Context context;
private final List<UUID> joinedMembers;
private final boolean withTime;
private final GroupCallUpdateDetails groupCallUpdateDetails;
private final UUID selfUuid;
public GroupCallUpdateMessageFactory(@NonNull Context context, @NonNull List<UUID> joinedMembers, @NonNull GroupCallUpdateDetails groupCallUpdateDetails) {
public GroupCallUpdateMessageFactory(@NonNull Context context,
@NonNull List<UUID> joinedMembers,
boolean withTime,
@NonNull GroupCallUpdateDetails groupCallUpdateDetails)
{
this.context = context;
this.joinedMembers = new ArrayList<>(joinedMembers);
this.withTime = withTime;
this.groupCallUpdateDetails = groupCallUpdateDetails;
this.selfUuid = TextSecurePreferences.getLocalUuid(context);
@ -46,28 +52,40 @@ public class GroupCallUpdateMessageFactory implements UpdateDescription.StringFa
switch (joinedMembers.size()) {
case 0:
return context.getString(R.string.MessageRecord_group_call_s, time);
return withTime ? context.getString(R.string.MessageRecord_group_call_s, time)
: context.getString(R.string.MessageRecord_group_call);
case 1:
if (joinedMembers.get(0).toString().equals(groupCallUpdateDetails.getStartedCallUuid())) {
return context.getString(R.string.MessageRecord_s_started_a_group_call_s, describe(joinedMembers.get(0)), time);
return withTime ? context.getString(R.string.MessageRecord_s_started_a_group_call_s, describe(joinedMembers.get(0)), time)
: context.getString(R.string.MessageRecord_s_started_a_group_call, describe(joinedMembers.get(0)));
} else if (Objects.equals(joinedMembers.get(0), selfUuid)) {
return context.getString(R.string.MessageRecord_you_are_in_the_group_call_s, describe(joinedMembers.get(0)), time);
return withTime ? context.getString(R.string.MessageRecord_you_are_in_the_group_call_s1, time)
: context.getString(R.string.MessageRecord_you_are_in_the_group_call);
} else {
return context.getString(R.string.MessageRecord_s_is_in_the_group_call_s, describe(joinedMembers.get(0)), time);
return withTime ? context.getString(R.string.MessageRecord_s_is_in_the_group_call_s, describe(joinedMembers.get(0)), time)
: context.getString(R.string.MessageRecord_s_is_in_the_group_call, describe(joinedMembers.get(0)));
}
case 2:
return context.getString(R.string.MessageRecord_s_and_s_are_in_the_group_call_s,
describe(joinedMembers.get(0)),
describe(joinedMembers.get(1)),
time);
return withTime ? context.getString(R.string.MessageRecord_s_and_s_are_in_the_group_call_s1,
describe(joinedMembers.get(0)),
describe(joinedMembers.get(1)),
time)
: context.getString(R.string.MessageRecord_s_and_s_are_in_the_group_call,
describe(joinedMembers.get(0)),
describe(joinedMembers.get(1)));
default:
int others = joinedMembers.size() - 2;
return context.getResources().getQuantityString(R.plurals.MessageRecord_s_s_and_d_others_are_in_the_group_call_s,
others,
describe(joinedMembers.get(0)),
describe(joinedMembers.get(1)),
others,
time);
return withTime ? context.getResources().getQuantityString(R.plurals.MessageRecord_s_s_and_d_others_are_in_the_group_call_s,
others,
describe(joinedMembers.get(0)),
describe(joinedMembers.get(1)),
others,
time)
: context.getResources().getQuantityString(R.plurals.MessageRecord_s_s_and_d_others_are_in_the_group_call,
others,
describe(joinedMembers.get(0)),
describe(joinedMembers.get(1)),
others);
}
}

Wyświetl plik

@ -162,7 +162,7 @@ public abstract class MessageRecord extends DisplayRecord {
} else if (isMissedVideoCall()) {
return staticUpdateDescription(context.getString(R.string.MessageRecord_missed_video_call_date, getCallDateString(context)), R.drawable.ic_update_video_call_missed_16, ContextCompat.getColor(context, R.color.core_red_shade), ContextCompat.getColor(context, R.color.core_red));
} else if (isGroupCall()) {
return getGroupCallUpdateDescription(context, getBody());
return getGroupCallUpdateDescription(context, getBody(), true);
} else if (isJoined()) {
return staticUpdateDescription(context.getString(R.string.MessageRecord_s_joined_signal, getIndividualRecipient().getDisplayName(context)), R.drawable.ic_update_group_add_16);
} else if (isExpirationTimerUpdate()) {
@ -308,8 +308,7 @@ public abstract class MessageRecord extends DisplayRecord {
}
}
public static @NonNull UpdateDescription getGroupCallUpdateDescription(@NonNull Context context, @NonNull String body) {
public static @NonNull UpdateDescription getGroupCallUpdateDescription(@NonNull Context context, @NonNull String body, boolean withTime) {
GroupCallUpdateDetails groupCallUpdateDetails = GroupCallUpdateDetailsUtil.parse(body);
List<UUID> joinedMembers = Stream.of(groupCallUpdateDetails.getInCallUuidsList())
@ -317,7 +316,7 @@ public abstract class MessageRecord extends DisplayRecord {
.withoutNulls()
.toList();
UpdateDescription.StringFactory stringFactory = new GroupCallUpdateMessageFactory(context, joinedMembers, groupCallUpdateDetails);
UpdateDescription.StringFactory stringFactory = new GroupCallUpdateMessageFactory(context, joinedMembers, withTime, groupCallUpdateDetails);
return UpdateDescription.mentioning(joinedMembers, stringFactory, R.drawable.ic_video_16);
}

Wyświetl plik

@ -12,7 +12,7 @@ import java.util.Objects;
public class CallParticipant {
public static final CallParticipant EMPTY = createRemote(Recipient.UNKNOWN, null, new BroadcastVideoSink(null), false, false);
public static final CallParticipant EMPTY = createRemote(Recipient.UNKNOWN, null, new BroadcastVideoSink(null), false, false, 0);
private final @NonNull CameraState cameraState;
private final @NonNull Recipient recipient;
@ -20,6 +20,7 @@ public class CallParticipant {
private final @NonNull BroadcastVideoSink videoSink;
private final boolean videoEnabled;
private final boolean microphoneEnabled;
private final long lastSpoke;
public static @NonNull CallParticipant createLocal(@NonNull CameraState cameraState,
@NonNull BroadcastVideoSink renderer,
@ -30,16 +31,18 @@ public class CallParticipant {
renderer,
cameraState,
cameraState.isEnabled() && cameraState.getCameraCount() > 0,
microphoneEnabled);
microphoneEnabled,
0);
}
public static @NonNull CallParticipant createRemote(@NonNull Recipient recipient,
@Nullable IdentityKey identityKey,
@NonNull BroadcastVideoSink renderer,
boolean audioEnabled,
boolean videoEnabled)
boolean videoEnabled,
long lastSpoke)
{
return new CallParticipant(recipient, identityKey, renderer, CameraState.UNKNOWN, videoEnabled, audioEnabled);
return new CallParticipant(recipient, identityKey, renderer, CameraState.UNKNOWN, videoEnabled, audioEnabled, lastSpoke);
}
private CallParticipant(@NonNull Recipient recipient,
@ -47,7 +50,8 @@ public class CallParticipant {
@NonNull BroadcastVideoSink videoSink,
@NonNull CameraState cameraState,
boolean videoEnabled,
boolean microphoneEnabled)
boolean microphoneEnabled,
long lastSpoke)
{
this.recipient = recipient;
this.identityKey = identityKey;
@ -55,14 +59,15 @@ public class CallParticipant {
this.cameraState = cameraState;
this.videoEnabled = videoEnabled;
this.microphoneEnabled = microphoneEnabled;
this.lastSpoke = lastSpoke;
}
public @NonNull CallParticipant withIdentityKey(@NonNull IdentityKey identityKey) {
return new CallParticipant(recipient, identityKey, videoSink, cameraState, videoEnabled, microphoneEnabled);
return new CallParticipant(recipient, identityKey, videoSink, cameraState, videoEnabled, microphoneEnabled, lastSpoke);
}
public @NonNull CallParticipant withVideoEnabled(boolean videoEnabled) {
return new CallParticipant(recipient, identityKey, videoSink, cameraState, videoEnabled, microphoneEnabled);
return new CallParticipant(recipient, identityKey, videoSink, cameraState, videoEnabled, microphoneEnabled, lastSpoke);
}
public @NonNull Recipient getRecipient() {
@ -100,22 +105,27 @@ public class CallParticipant {
return cameraState.getCameraCount() > 1;
}
public long getLastSpoke() {
return lastSpoke;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CallParticipant that = (CallParticipant) o;
return videoEnabled == that.videoEnabled &&
microphoneEnabled == that.microphoneEnabled &&
cameraState.equals(that.cameraState) &&
recipient.equals(that.recipient) &&
Objects.equals(identityKey, that.identityKey) &&
Objects.equals(videoSink, that.videoSink);
microphoneEnabled == that.microphoneEnabled &&
lastSpoke == that.lastSpoke &&
cameraState.equals(that.cameraState) &&
recipient.equals(that.recipient) &&
Objects.equals(identityKey, that.identityKey) &&
Objects.equals(videoSink, that.videoSink);
}
@Override
public int hashCode() {
return Objects.hash(cameraState, recipient, identityKey, videoSink, videoEnabled, microphoneEnabled);
return Objects.hash(cameraState, recipient, identityKey, videoSink, videoEnabled, microphoneEnabled, lastSpoke);
}
@Override

Wyświetl plik

@ -31,6 +31,7 @@ import android.net.Uri;
import android.os.Build;
import android.os.TransactionTooLargeException;
import android.service.notification.StatusBarNotification;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
@ -607,6 +608,9 @@ public class DefaultMessageNotifier implements MessageNotifier {
body = ThreadBodyUtil.getFormattedBodyFor(context, record);
slideDeck = ((MmsMessageRecord) record).getSlideDeck();
canReply = true;
} else if (record.isGroupCall()) {
body = new SpannableString(MessageRecord.getGroupCallUpdateDescription(context, record.getBody(), false).getString());
canReply = false;
} else {
canReply = true;
}

Wyświetl plik

@ -76,6 +76,7 @@ import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserExce
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
@ -667,8 +668,6 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
public void sendGroupCallMessage(@NonNull Recipient recipient, @Nullable String groupCallEraId) {
SignalExecutors.BOUNDED.execute(() -> ApplicationDependencies.getJobManager().add(GroupCallUpdateSendJob.create(recipient.getId(), groupCallEraId)));
peekGroupCall(new WebRtcData.GroupCallUpdateMetadata(Recipient.self().getId(), recipient.getId(), groupCallEraId, System.currentTimeMillis()));
}
public void peekGroupCall(@NonNull WebRtcData.GroupCallUpdateMetadata groupCallUpdateMetadata) {
@ -689,6 +688,9 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
groupCallUpdateMetadata.getGroupCallEraId(),
peekInfo.getEraId(),
peekInfo.getJoinedMembers());
long threadId = DatabaseFactory.getThreadDatabase(this).getThreadIdFor(group);
ApplicationDependencies.getMessageNotifier().updateNotification(this, threadId, true);
});
} catch (IOException | VerificationFailedException | CallException e) {
@ -697,6 +699,15 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
});
}
public void updateGroupCallUpdateMessage(@NonNull RecipientId groupId, @Nullable String groupCallEraId, @NonNull Collection<UUID> joinedMembers) {
DatabaseFactory.getSmsDatabase(this).insertOrUpdateGroupCall(groupId,
Recipient.self().getId(),
System.currentTimeMillis(),
null,
groupCallEraId,
joinedMembers);
}
@Override
public void onStartCall(@Nullable Remote remote, @NonNull CallId callId, @NonNull Boolean isOutgoing, @Nullable CallManager.CallMediaType callMediaType) {
Log.i(TAG, "onStartCall(): callId: " + callId + ", outgoing: " + isOutgoing + ", type: " + callMediaType);

Wyświetl plik

@ -45,7 +45,8 @@ public class BeginCallActionProcessorDelegate extends WebRtcActionProcessor {
null,
new BroadcastVideoSink(currentState.getVideoState().getEglBase()),
true,
false
false,
0
))
.build();
@ -84,7 +85,8 @@ public class BeginCallActionProcessorDelegate extends WebRtcActionProcessor {
null,
new BroadcastVideoSink(currentState.getVideoState().getEglBase()),
true,
false
false,
0
))
.build();
}

Wyświetl plik

@ -69,7 +69,8 @@ public class GroupActionProcessor extends DeviceAwareActionProcessor {
null,
videoSink,
Boolean.FALSE.equals(device.getAudioMuted()),
Boolean.FALSE.equals(device.getVideoMuted())));
Boolean.FALSE.equals(device.getVideoMuted()),
device.getSpeakerTime()));
}
return builder.build();

Wyświetl plik

@ -2,14 +2,21 @@ package org.thoughtcrime.securesms.service.webrtc;
import androidx.annotation.NonNull;
import com.annimon.stream.Stream;
import org.signal.ringrtc.CallException;
import org.signal.ringrtc.GroupCall;
import org.signal.ringrtc.PeekInfo;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.ringrtc.Camera;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* Process actions for when the call has at least once been connected and joined.
*/
@ -100,7 +107,14 @@ public class GroupConnectedActionProcessor extends GroupActionProcessor {
return currentState;
}
webRtcInteractor.sendGroupCallMessage(currentState.getCallInfoState().getCallRecipient(), WebRtcUtil.getGroupCallEraId(groupCall));
String eraId = WebRtcUtil.getGroupCallEraId(groupCall);
webRtcInteractor.sendGroupCallMessage(currentState.getCallInfoState().getCallRecipient(), eraId);
List<UUID> members = new ArrayList<>(peekInfo.getJoinedMembers());
if (!members.contains(Recipient.self().requireUuid())) {
members.add(Recipient.self().requireUuid());
}
webRtcInteractor.updateGroupCallUpdateMessage(currentState.getCallInfoState().getCallRecipient().getId(), eraId, members);
return currentState.builder()
.changeCallSetupState()
@ -120,7 +134,11 @@ public class GroupConnectedActionProcessor extends GroupActionProcessor {
return groupCallFailure(currentState, "Unable to disconnect from group call", e);
}
webRtcInteractor.sendGroupCallMessage(currentState.getCallInfoState().getCallRecipient(), WebRtcUtil.getGroupCallEraId(groupCall));
String eraId = WebRtcUtil.getGroupCallEraId(groupCall);
webRtcInteractor.sendGroupCallMessage(currentState.getCallInfoState().getCallRecipient(), eraId);
List<UUID> members = Stream.of(currentState.getCallInfoState().getRemoteCallParticipants()).map(p -> p.getRecipient().requireUuid()).toList();
webRtcInteractor.updateGroupCallUpdateMessage(currentState.getCallInfoState().getCallRecipient().getId(), eraId, members);
currentState = currentState.builder()
.changeCallInfoState()

Wyświetl plik

@ -114,7 +114,7 @@ public class GroupPreJoinActionProcessor extends GroupActionProcessor {
.changeCallInfoState();
for (Recipient recipient : callParticipants) {
builder.putParticipant(recipient, CallParticipant.createRemote(recipient, null, new BroadcastVideoSink(null), true, true));
builder.putParticipant(recipient, CallParticipant.createRemote(recipient, null, new BroadcastVideoSink(null), true, true, 0));
}
return builder.build();

Wyświetl plik

@ -8,6 +8,7 @@ import androidx.annotation.Nullable;
import org.signal.ringrtc.CallManager;
import org.signal.ringrtc.GroupCall;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.ringrtc.CameraEventListener;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.WebRtcCallService;
@ -18,6 +19,7 @@ import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
import org.thoughtcrime.securesms.webrtc.locks.LockManager;
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
import java.util.Collection;
import java.util.UUID;
/**
@ -92,6 +94,10 @@ public class WebRtcInteractor {
webRtcCallService.sendGroupCallMessage(recipient, groupCallEraId);
}
void updateGroupCallUpdateMessage(@NonNull RecipientId groupId, @Nullable String groupCallEraId, @NonNull Collection<UUID> joinedMembers) {
webRtcCallService.updateGroupCallUpdateMessage(groupId, groupCallEraId, joinedMembers);
}
void setCallInProgressNotification(int type, @NonNull RemotePeer remotePeer) {
webRtcCallService.setCallInProgressNotification(type, remotePeer.getRecipient());
}

Wyświetl plik

@ -1215,10 +1215,18 @@
<!-- Group Calling update messages -->
<string name="MessageRecord_s_started_a_group_call_s">%1$s started a group call · %2$s</string>
<string name="MessageRecord_s_is_in_the_group_call_s">%1$s is in the group call · %2$s</string>
<string name="MessageRecord_you_are_in_the_group_call_s">You are in the group call · %2$s</string>
<string name="MessageRecord_s_and_s_are_in_the_group_call_s">%1$s and %2$s are in the group call · %3$s</string>
<string name="MessageRecord_you_are_in_the_group_call_s1">You are in the group call · %1$s</string>
<string name="MessageRecord_s_and_s_are_in_the_group_call_s1">%1$s and %2$s are in the group call · %3$s</string>
<string name="MessageRecord_s_s_and_s_are_in_the_group_call_s">%1$s, %2$s, and %3$s are in the group call · %4$s</string>
<string name="MessageRecord_group_call_s">Group call · %1$s</string>
<string name="MessageRecord_s_started_a_group_call">%1$s started a group call</string>
<string name="MessageRecord_s_is_in_the_group_call">%1$s is in the group call</string>
<string name="MessageRecord_you_are_in_the_group_call">You are in the group call</string>
<string name="MessageRecord_s_and_s_are_in_the_group_call">%1$s and %2$s are in the group call</string>
<string name="MessageRecord_s_s_and_s_are_in_the_group_call">%1$s, %2$s, and %3$s are in the group call</string>
<string name="MessageRecord_group_call">Group call</string>
<string name="MessageRecord_you">You</string>
<plurals name="MessageRecord_s_s_and_d_others_are_in_the_group_call_s">
@ -1226,6 +1234,11 @@
<item quantity="other">%1$s, %2$s, and %3$d others are in the group call · %4$s</item>
</plurals>
<plurals name="MessageRecord_s_s_and_d_others_are_in_the_group_call">
<item quantity="one">%1$s, %2$s, and %3$d other are in the group call</item>
<item quantity="other">%1$s, %2$s, and %3$d others are in the group call</item>
</plurals>
<!-- MessageRequestBottomView -->
<string name="MessageRequestBottomView_accept">Accept</string>
<string name="MessageRequestBottomView_continue">Continue</string>