mirror of
https://github.com/oxen-io/session-android.git
synced 2025-08-12 00:57:55 +00:00
Merge branch 'dev' into hardfork
This commit is contained in:
@@ -11,6 +11,7 @@ import org.session.libsession.messaging.messages.visible.Attachment
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||
import org.session.libsession.messaging.opengroups.OpenGroup
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
|
||||
import org.session.libsession.messaging.sending_receiving.dataextraction.DataExtractionNotificationInfoMessage
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview
|
||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
|
||||
@@ -34,6 +35,7 @@ interface StorageProtocol {
|
||||
fun setUserProfilePictureUrl(newProfilePicture: String)
|
||||
|
||||
fun getProfileKeyForRecipient(recipientPublicKey: String): ByteArray?
|
||||
fun getDisplayNameForRecipient(recipientPublicKey: String): String?
|
||||
fun setProfileKeyForRecipient(recipientPublicKey: String, profileKey: ByteArray)
|
||||
|
||||
// Signal Protocol
|
||||
@@ -116,9 +118,9 @@ interface StorageProtocol {
|
||||
fun removeClosedGroupPublicKey(groupPublicKey: String)
|
||||
fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String)
|
||||
fun removeAllClosedGroupEncryptionKeyPairs(groupPublicKey: String)
|
||||
fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type0: SignalServiceProtos.GroupContext.Type, type1: SignalServiceGroup.Type,
|
||||
fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type: SignalServiceGroup.Type,
|
||||
name: String, members: Collection<String>, admins: Collection<String>, sentTimestamp: Long)
|
||||
fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceProtos.GroupContext.Type, name: String,
|
||||
fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceGroup.Type, name: String,
|
||||
members: Collection<String>, admins: Collection<String>, threadID: Long, sentTimestamp: Long)
|
||||
fun isClosedGroup(publicKey: String): Boolean
|
||||
fun getClosedGroupEncryptionKeyPairs(groupPublicKey: String): MutableList<ECKeyPair>
|
||||
@@ -158,4 +160,7 @@ interface StorageProtocol {
|
||||
// Message Handling
|
||||
/// Returns the ID of the `TSIncomingMessage` that was constructed.
|
||||
fun persist(message: VisibleMessage, quotes: QuoteModel?, linkPreview: List<LinkPreview?>, groupPublicKey: String?, openGroupID: String?, attachments: List<Attachment>): Long?
|
||||
}
|
||||
|
||||
// Data Extraction Notification
|
||||
fun insertDataExtractionNotificationMessage(senderPublicKey: String, message: DataExtractionNotificationInfoMessage, sentTimestamp: Long)
|
||||
}
|
||||
|
@@ -9,7 +9,7 @@ class DataExtractionNotification(): ControlMessage() {
|
||||
// Kind enum
|
||||
sealed class Kind {
|
||||
class Screenshot() : Kind()
|
||||
class MediaSaved(val timestanp: Long) : Kind()
|
||||
class MediaSaved(val timestamp: Long) : Kind()
|
||||
|
||||
val description: String =
|
||||
when(this) {
|
||||
@@ -46,7 +46,7 @@ class DataExtractionNotification(): ControlMessage() {
|
||||
val kind = kind ?: return false
|
||||
return when(kind) {
|
||||
is Kind.Screenshot -> true
|
||||
is Kind.MediaSaved -> kind.timestanp > 0
|
||||
is Kind.MediaSaved -> kind.timestamp > 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ class DataExtractionNotification(): ControlMessage() {
|
||||
is Kind.Screenshot -> dataExtractionNotification.type = SignalServiceProtos.DataExtractionNotification.Type.SCREENSHOT
|
||||
is Kind.MediaSaved -> {
|
||||
dataExtractionNotification.type = SignalServiceProtos.DataExtractionNotification.Type.MEDIA_SAVED
|
||||
dataExtractionNotification.timestamp = kind.timestanp
|
||||
dataExtractionNotification.timestamp = kind.timestamp
|
||||
}
|
||||
}
|
||||
val contentProto = SignalServiceProtos.Content.newBuilder()
|
||||
@@ -73,5 +73,4 @@ class DataExtractionNotification(): ControlMessage() {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -33,6 +33,11 @@ class ExpirationTimerUpdate() : ControlMessage() {
|
||||
this.duration = duration
|
||||
}
|
||||
|
||||
internal constructor(duration: Int) : this() {
|
||||
this.syncTarget = null
|
||||
this.duration = duration
|
||||
}
|
||||
|
||||
// validation
|
||||
override fun isValid(): Boolean {
|
||||
if (!super.isValid()) return false
|
||||
|
@@ -4,11 +4,13 @@ import static org.session.libsignal.service.internal.push.SignalServiceProtos.Gr
|
||||
|
||||
public class IncomingGroupMessage extends IncomingTextMessage {
|
||||
|
||||
private final GroupContext groupContext;
|
||||
private final String groupID;
|
||||
private final boolean updateMessage;
|
||||
|
||||
public IncomingGroupMessage(IncomingTextMessage base, GroupContext groupContext, String body) {
|
||||
public IncomingGroupMessage(IncomingTextMessage base, String groupID, String body, boolean updateMessage) {
|
||||
super(base, body);
|
||||
this.groupContext = groupContext;
|
||||
this.groupID = groupID;
|
||||
this.updateMessage = updateMessage;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -16,12 +18,6 @@ public class IncomingGroupMessage extends IncomingTextMessage {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isUpdate() {
|
||||
return groupContext.getType().getNumber() == GroupContext.Type.UPDATE_VALUE;
|
||||
}
|
||||
|
||||
public boolean isQuit() {
|
||||
return groupContext.getType().getNumber() == GroupContext.Type.QUIT_VALUE;
|
||||
}
|
||||
public boolean isUpdateMessage() { return updateMessage; }
|
||||
|
||||
}
|
||||
|
@@ -3,10 +3,11 @@ package org.session.libsession.messaging.messages.signal;
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment;
|
||||
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview;
|
||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
|
||||
import org.session.libsession.messaging.sending_receiving.dataextraction.DataExtractionNotificationInfoMessage;
|
||||
import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview;
|
||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
|
||||
import org.session.libsession.utilities.GroupUtil;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachment;
|
||||
@@ -26,9 +27,11 @@ public class IncomingMediaMessage {
|
||||
private final int subscriptionId;
|
||||
private final long expiresIn;
|
||||
private final boolean expirationUpdate;
|
||||
private final QuoteModel quote;
|
||||
private final boolean unidentified;
|
||||
|
||||
private final DataExtractionNotificationInfoMessage dataExtractionNotification;
|
||||
private final QuoteModel quote;
|
||||
|
||||
private final List<Attachment> attachments = new LinkedList<>();
|
||||
private final List<Contact> sharedContacts = new LinkedList<>();
|
||||
private final List<LinkPreview> linkPreviews = new LinkedList<>();
|
||||
@@ -44,17 +47,19 @@ public class IncomingMediaMessage {
|
||||
Optional<List<SignalServiceAttachment>> attachments,
|
||||
Optional<QuoteModel> quote,
|
||||
Optional<List<Contact>> sharedContacts,
|
||||
Optional<List<LinkPreview>> linkPreviews)
|
||||
Optional<List<LinkPreview>> linkPreviews,
|
||||
Optional<DataExtractionNotificationInfoMessage> dataExtractionNotification)
|
||||
{
|
||||
this.push = true;
|
||||
this.from = from;
|
||||
this.sentTimeMillis = sentTimeMillis;
|
||||
this.body = body.orNull();
|
||||
this.subscriptionId = subscriptionId;
|
||||
this.expiresIn = expiresIn;
|
||||
this.expirationUpdate = expirationUpdate;
|
||||
this.quote = quote.orNull();
|
||||
this.unidentified = unidentified;
|
||||
this.push = true;
|
||||
this.from = from;
|
||||
this.sentTimeMillis = sentTimeMillis;
|
||||
this.body = body.orNull();
|
||||
this.subscriptionId = subscriptionId;
|
||||
this.expiresIn = expiresIn;
|
||||
this.expirationUpdate = expirationUpdate;
|
||||
this.dataExtractionNotification = dataExtractionNotification.orNull();
|
||||
this.quote = quote.orNull();
|
||||
this.unidentified = unidentified;
|
||||
|
||||
if (group.isPresent()) this.groupId = Address.fromSerialized(GroupUtil.INSTANCE.getEncodedId(group.get()));
|
||||
else this.groupId = null;
|
||||
@@ -73,7 +78,7 @@ public class IncomingMediaMessage {
|
||||
Optional<List<LinkPreview>> linkPreviews)
|
||||
{
|
||||
return new IncomingMediaMessage(from, message.getSentTimestamp(), -1, expiresIn, false,
|
||||
false, Optional.fromNullable(message.getText()), group, Optional.fromNullable(attachments), quote, Optional.absent(), linkPreviews);
|
||||
false, Optional.fromNullable(message.getText()), group, Optional.fromNullable(attachments), quote, Optional.absent(), linkPreviews, Optional.absent());
|
||||
}
|
||||
|
||||
public int getSubscriptionId() {
|
||||
@@ -116,6 +121,20 @@ public class IncomingMediaMessage {
|
||||
return groupId != null;
|
||||
}
|
||||
|
||||
public boolean isScreenshotDataExtraction() {
|
||||
if (dataExtractionNotification == null) return false;
|
||||
else {
|
||||
return dataExtractionNotification.getKind() == DataExtractionNotificationInfoMessage.Kind.SCREENSHOT;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isMediaSavedDataExtraction() {
|
||||
if (dataExtractionNotification == null) return false;
|
||||
else {
|
||||
return dataExtractionNotification.getKind() == DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED;
|
||||
}
|
||||
}
|
||||
|
||||
public QuoteModel getQuote() {
|
||||
return quote;
|
||||
}
|
||||
|
@@ -1,6 +1,5 @@
|
||||
package org.session.libsession.messaging.messages.signal;
|
||||
|
||||
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
import org.session.libsession.messaging.threads.DistributionTypes;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
@@ -10,15 +9,13 @@ import java.util.LinkedList;
|
||||
|
||||
public class OutgoingExpirationUpdateMessage extends OutgoingSecureMediaMessage {
|
||||
|
||||
public OutgoingExpirationUpdateMessage(Recipient recipient, long sentTimeMillis, long expiresIn) {
|
||||
super(recipient, "", new LinkedList<Attachment>(), sentTimeMillis,
|
||||
DistributionTypes.CONVERSATION, expiresIn, true, null, Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
}
|
||||
private final String groupId;
|
||||
|
||||
public static OutgoingExpirationUpdateMessage from(ExpirationTimerUpdate message,
|
||||
Recipient recipient) {
|
||||
return new OutgoingExpirationUpdateMessage(recipient, message.getSentTimestamp(), message.getDuration() * 1000);
|
||||
public OutgoingExpirationUpdateMessage(Recipient recipient, long sentTimeMillis, long expiresIn, String groupId) {
|
||||
super(recipient, "", new LinkedList<Attachment>(), sentTimeMillis,
|
||||
DistributionTypes.CONVERSATION, expiresIn, null, Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -26,4 +23,13 @@ public class OutgoingExpirationUpdateMessage extends OutgoingSecureMediaMessage
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGroup() {
|
||||
return groupId != null;
|
||||
}
|
||||
|
||||
public String getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -19,56 +19,27 @@ import java.util.List;
|
||||
|
||||
public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
|
||||
|
||||
private final GroupContext group;
|
||||
private final String groupID;
|
||||
private final boolean isUpdateMessage;
|
||||
|
||||
public OutgoingGroupMediaMessage(@NonNull Recipient recipient,
|
||||
@NonNull String encodedGroupContext,
|
||||
@NonNull List<Attachment> avatar,
|
||||
long sentTimeMillis,
|
||||
long expiresIn,
|
||||
@Nullable QuoteModel quote,
|
||||
@NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> previews)
|
||||
throws IOException
|
||||
{
|
||||
super(recipient, encodedGroupContext, avatar, sentTimeMillis,
|
||||
DistributionTypes.CONVERSATION, expiresIn, false, quote, contacts, previews);
|
||||
|
||||
this.group = GroupContext.parseFrom(Base64.decode(encodedGroupContext));
|
||||
}
|
||||
|
||||
public OutgoingGroupMediaMessage(@NonNull Recipient recipient,
|
||||
@NonNull GroupContext group,
|
||||
@Nullable final Attachment avatar,
|
||||
long expireIn,
|
||||
@Nullable QuoteModel quote,
|
||||
@NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> previews)
|
||||
{
|
||||
super(recipient, Base64.encodeBytes(group.toByteArray()),
|
||||
new LinkedList<Attachment>() {{if (avatar != null) add(avatar);}},
|
||||
System.currentTimeMillis(),
|
||||
DistributionTypes.CONVERSATION, expireIn, false, quote, contacts, previews);
|
||||
|
||||
this.group = group;
|
||||
}
|
||||
|
||||
public OutgoingGroupMediaMessage(@NonNull Recipient recipient,
|
||||
@NonNull GroupContext group,
|
||||
@NonNull String body,
|
||||
@Nullable String groupId,
|
||||
@Nullable final Attachment avatar,
|
||||
long sentTime,
|
||||
long expireIn,
|
||||
boolean expirationUpdate,
|
||||
boolean updateMessage,
|
||||
@Nullable QuoteModel quote,
|
||||
@NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> previews)
|
||||
{
|
||||
super(recipient, Base64.encodeBytes(group.toByteArray()),
|
||||
super(recipient, body,
|
||||
new LinkedList<Attachment>() {{if (avatar != null) add(avatar);}},
|
||||
sentTime,
|
||||
DistributionTypes.CONVERSATION, expireIn, expirationUpdate, quote, contacts, previews);
|
||||
DistributionTypes.CONVERSATION, expireIn, quote, contacts, previews);
|
||||
|
||||
this.group = group;
|
||||
this.groupID = groupId;
|
||||
this.isUpdateMessage = updateMessage;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -76,15 +47,11 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isGroupUpdate() {
|
||||
return group.getType().getNumber() == GroupContext.Type.UPDATE_VALUE;
|
||||
public String getGroupId() {
|
||||
return groupID;
|
||||
}
|
||||
|
||||
public boolean isGroupQuit() {
|
||||
return group.getType().getNumber() == GroupContext.Type.QUIT_VALUE;
|
||||
}
|
||||
|
||||
public GroupContext getGroupContext() {
|
||||
return group;
|
||||
public boolean isUpdateMessage() {
|
||||
return isUpdateMessage;
|
||||
}
|
||||
}
|
||||
|
@@ -26,7 +26,6 @@ public class OutgoingMediaMessage {
|
||||
private final int distributionType;
|
||||
private final int subscriptionId;
|
||||
private final long expiresIn;
|
||||
private final boolean expirationUpdate;
|
||||
private final QuoteModel outgoingQuote;
|
||||
|
||||
private final List<NetworkFailure> networkFailures = new LinkedList<>();
|
||||
@@ -37,7 +36,6 @@ public class OutgoingMediaMessage {
|
||||
public OutgoingMediaMessage(Recipient recipient, String message,
|
||||
List<Attachment> attachments, long sentTimeMillis,
|
||||
int subscriptionId, long expiresIn,
|
||||
boolean expirationUpdate,
|
||||
int distributionType,
|
||||
@Nullable QuoteModel outgoingQuote,
|
||||
@NonNull List<Contact> contacts,
|
||||
@@ -52,7 +50,6 @@ public class OutgoingMediaMessage {
|
||||
this.attachments = attachments;
|
||||
this.subscriptionId = subscriptionId;
|
||||
this.expiresIn = expiresIn;
|
||||
this.expirationUpdate = expirationUpdate;
|
||||
this.outgoingQuote = outgoingQuote;
|
||||
|
||||
this.contacts.addAll(contacts);
|
||||
@@ -69,7 +66,6 @@ public class OutgoingMediaMessage {
|
||||
this.sentTimeMillis = that.sentTimeMillis;
|
||||
this.subscriptionId = that.subscriptionId;
|
||||
this.expiresIn = that.expiresIn;
|
||||
this.expirationUpdate = that.expirationUpdate;
|
||||
this.outgoingQuote = that.outgoingQuote;
|
||||
|
||||
this.identityKeyMismatches.addAll(that.identityKeyMismatches);
|
||||
@@ -89,7 +85,7 @@ public class OutgoingMediaMessage {
|
||||
previews = Collections.singletonList(linkPreview);
|
||||
}
|
||||
return new OutgoingMediaMessage(recipient, message.getText(), attachments, message.getSentTimestamp(), -1,
|
||||
recipient.getExpireMessages() * 1000, false, DistributionTypes.DEFAULT, outgoingQuote, Collections.emptyList(),
|
||||
recipient.getExpireMessages() * 1000, DistributionTypes.DEFAULT, outgoingQuote, Collections.emptyList(),
|
||||
previews, Collections.emptyList(), Collections.emptyList());
|
||||
}
|
||||
|
||||
@@ -113,7 +109,7 @@ public class OutgoingMediaMessage {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isExpirationUpdate() { return expirationUpdate; }
|
||||
public boolean isExpirationUpdate() { return false; }
|
||||
|
||||
public long getSentTimeMillis() {
|
||||
return sentTimeMillis;
|
||||
|
@@ -19,12 +19,11 @@ public class OutgoingSecureMediaMessage extends OutgoingMediaMessage {
|
||||
long sentTimeMillis,
|
||||
int distributionType,
|
||||
long expiresIn,
|
||||
boolean expirationUpdate,
|
||||
@Nullable QuoteModel quote,
|
||||
@NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> previews)
|
||||
{
|
||||
super(recipient, body, attachments, sentTimeMillis, -1, expiresIn, expirationUpdate, distributionType, quote, contacts, previews, Collections.emptyList(), Collections.emptyList());
|
||||
super(recipient, body, attachments, sentTimeMillis, -1, expiresIn, distributionType, quote, contacts, previews, Collections.emptyList(), Collections.emptyList());
|
||||
}
|
||||
|
||||
public OutgoingSecureMediaMessage(OutgoingMediaMessage base) {
|
||||
|
@@ -9,6 +9,7 @@ import org.session.libsession.messaging.messages.control.*
|
||||
import org.session.libsession.messaging.messages.visible.Attachment
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment
|
||||
import org.session.libsession.messaging.sending_receiving.dataextraction.DataExtractionNotificationInfoMessage
|
||||
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
|
||||
@@ -45,6 +46,7 @@ fun MessageReceiver.handle(message: Message, proto: SignalServiceProtos.Content,
|
||||
is TypingIndicator -> handleTypingIndicator(message)
|
||||
is ClosedGroupControlMessage -> handleClosedGroupControlMessage(message)
|
||||
is ExpirationTimerUpdate -> handleExpirationTimerUpdate(message)
|
||||
is DataExtractionNotification -> handleDataExtractionNotification(message)
|
||||
is ConfigurationMessage -> handleConfigurationMessage(message)
|
||||
is VisibleMessage -> handleVisibleMessage(message, proto, openGroupID)
|
||||
}
|
||||
@@ -91,6 +93,24 @@ private fun MessageReceiver.handleExpirationTimerUpdate(message: ExpirationTimer
|
||||
}
|
||||
}
|
||||
|
||||
// Data Extraction Notification handling
|
||||
|
||||
private fun MessageReceiver.handleDataExtractionNotification(message: DataExtractionNotification) {
|
||||
// we don't handle data extraction messages for groups (they shouldn't be sent, but in case we filter them here too)
|
||||
if (message.groupPublicKey != null) return
|
||||
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val senderPublicKey = message.sender!!
|
||||
val notification: DataExtractionNotificationInfoMessage = when(message.kind) {
|
||||
is DataExtractionNotification.Kind.Screenshot -> DataExtractionNotificationInfoMessage(DataExtractionNotificationInfoMessage.Kind.SCREENSHOT)
|
||||
is DataExtractionNotification.Kind.MediaSaved -> DataExtractionNotificationInfoMessage(DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED)
|
||||
else -> return
|
||||
}
|
||||
storage.insertDataExtractionNotificationMessage(senderPublicKey, notification, message.sentTimestamp!!)
|
||||
}
|
||||
|
||||
// Configuration message handling
|
||||
|
||||
private fun MessageReceiver.handleConfigurationMessage(message: ConfigurationMessage) {
|
||||
val context = MessagingConfiguration.shared.context
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
@@ -153,7 +173,8 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
|
||||
}
|
||||
}
|
||||
// Get or create thread
|
||||
val threadID = storage.getOrCreateThreadIdFor(message.syncTarget ?: message.sender!!, message.groupPublicKey, openGroupID)
|
||||
val threadID = storage.getOrCreateThreadIdFor(message.syncTarget
|
||||
?: message.sender!!, message.groupPublicKey, openGroupID)
|
||||
// Parse quote if needed
|
||||
var quoteModel: QuoteModel? = null
|
||||
if (message.quote != null && proto.dataMessage.hasQuote()) {
|
||||
@@ -242,9 +263,16 @@ private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPubli
|
||||
storage.updateMembers(groupID, members.map { Address.fromSerialized(it) })
|
||||
} else {
|
||||
storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }),
|
||||
null, null, LinkedList(admins.map { Address.fromSerialized(it) }), formationTimestamp)
|
||||
null, null, LinkedList(admins.map { Address.fromSerialized(it) }), formationTimestamp)
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
// Notify the user
|
||||
storage.insertIncomingInfoMessage(context, sender, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp)
|
||||
if (userPublicKey == sender) {
|
||||
// sender is a linked device
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.CREATION, name, members, admins, threadID, sentTimestamp)
|
||||
} else {
|
||||
storage.insertIncomingInfoMessage(context, sender, groupID, SignalServiceGroup.Type.CREATION, name, members, admins, sentTimestamp)
|
||||
}
|
||||
}
|
||||
storage.setProfileSharing(Address.fromSerialized(groupID), true)
|
||||
// Add the group to the user's set of public keys to poll for
|
||||
@@ -304,9 +332,8 @@ private fun MessageReceiver.handleClosedGroupUpdated(message: ClosedGroupControl
|
||||
}
|
||||
// Notify the user
|
||||
val wasSenderRemoved = !members.contains(senderPublicKey)
|
||||
val type0 = if (wasSenderRemoved) SignalServiceProtos.GroupContext.Type.QUIT else SignalServiceProtos.GroupContext.Type.UPDATE
|
||||
val type1 = if (wasSenderRemoved) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.UPDATE
|
||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, type0, type1, name, members, group.admins.map { it.toString() }, message.sentTimestamp!!)
|
||||
val type = if (wasSenderRemoved) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.MEMBER_REMOVED
|
||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, type, name, members, group.admins.map { it.toString() }, message.sentTimestamp!!)
|
||||
}
|
||||
|
||||
private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGroupControlMessage) {
|
||||
@@ -378,9 +405,9 @@ private fun MessageReceiver.handleClosedGroupNameChanged(message: ClosedGroupCon
|
||||
if (userPublicKey == senderPublicKey) {
|
||||
// sender is a linked device
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, name, members, admins, threadID, message.sentTimestamp!!)
|
||||
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.NAME_CHANGE, name, members, admins, threadID, message.sentTimestamp!!)
|
||||
} else {
|
||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, message.sentTimestamp!!)
|
||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceGroup.Type.NAME_CHANGE, name, members, admins, message.sentTimestamp!!)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -413,9 +440,9 @@ private fun MessageReceiver.handleClosedGroupMembersAdded(message: ClosedGroupCo
|
||||
if (userPublicKey == senderPublicKey) {
|
||||
// sender is a linked device
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, name, members, admins, threadID, message.sentTimestamp!!)
|
||||
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.MEMBER_ADDED, name, updateMembers, admins, threadID, message.sentTimestamp!!)
|
||||
} else {
|
||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, message.sentTimestamp!!)
|
||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceGroup.Type.MEMBER_ADDED, name, updateMembers, admins, message.sentTimestamp!!)
|
||||
}
|
||||
if (userPublicKey in admins) {
|
||||
// send current encryption key to the latest added members
|
||||
@@ -477,17 +504,16 @@ private fun MessageReceiver.handleClosedGroupMembersRemoved(message: ClosedGroup
|
||||
MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, newMembers)
|
||||
}
|
||||
}
|
||||
val (contextType, signalType) =
|
||||
if (senderLeft) SignalServiceProtos.GroupContext.Type.QUIT to SignalServiceGroup.Type.QUIT
|
||||
else SignalServiceProtos.GroupContext.Type.UPDATE to SignalServiceGroup.Type.UPDATE
|
||||
val type = if (senderLeft) SignalServiceGroup.Type.QUIT
|
||||
else SignalServiceGroup.Type.MEMBER_REMOVED
|
||||
|
||||
// Notify the user
|
||||
if (userPublicKey == senderPublicKey) {
|
||||
// sender is a linked device
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, contextType, name, members, admins, threadID, message.sentTimestamp!!)
|
||||
storage.insertOutgoingInfoMessage(context, groupID, type, name, updateMembers, admins, threadID, message.sentTimestamp!!)
|
||||
} else {
|
||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, contextType, signalType, name, members, admins, message.sentTimestamp!!)
|
||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, type, name, updateMembers, admins, message.sentTimestamp!!)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -533,9 +559,9 @@ private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupCont
|
||||
if (userLeft) {
|
||||
//sender is a linked device
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceProtos.GroupContext.Type.QUIT, name, members, admins, threadID, message.sentTimestamp!!)
|
||||
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.QUIT, name, members, admins, threadID, message.sentTimestamp!!)
|
||||
} else {
|
||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.QUIT, SignalServiceGroup.Type.QUIT, name, members, admins, message.sentTimestamp!!)
|
||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceGroup.Type.QUIT, name, members, admins, message.sentTimestamp!!)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -15,6 +15,7 @@ import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.libsignal.ecc.Curve
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
||||
import org.session.libsignal.libsignal.util.guava.Optional
|
||||
import org.session.libsignal.service.api.messages.SignalServiceGroup
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey
|
||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
@@ -60,7 +61,7 @@ fun MessageSender.create(name: String, members: Collection<String>): Promise<Str
|
||||
storage.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
|
||||
// Notify the user
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, name, members, admins, threadID, sentTime)
|
||||
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.CREATION, name, members, admins, threadID, sentTime)
|
||||
// Notify the PN server
|
||||
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
|
||||
// Fulfill the promise
|
||||
@@ -107,7 +108,7 @@ fun MessageSender.setName(groupPublicKey: String, newName: String) {
|
||||
// Update the group
|
||||
storage.updateTitle(groupID, newName)
|
||||
// Notify the user
|
||||
val infoType = SignalServiceProtos.GroupContext.Type.UPDATE
|
||||
val infoType = SignalServiceGroup.Type.NAME_CHANGE
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, infoType, newName, members, admins, threadID, sentTime)
|
||||
}
|
||||
@@ -150,9 +151,9 @@ fun MessageSender.addMembers(groupPublicKey: String, membersToAdd: List<String>)
|
||||
send(closedGroupControlMessage, Address.fromSerialized(member))
|
||||
}
|
||||
// Notify the user
|
||||
val infoType = SignalServiceProtos.GroupContext.Type.UPDATE
|
||||
val infoType = SignalServiceGroup.Type.MEMBER_ADDED
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime)
|
||||
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, membersToAdd, admins, threadID, sentTime)
|
||||
}
|
||||
|
||||
fun MessageSender.removeMembers(groupPublicKey: String, membersToRemove: List<String>) {
|
||||
@@ -189,9 +190,9 @@ fun MessageSender.removeMembers(groupPublicKey: String, membersToRemove: List<St
|
||||
generateAndSendNewEncryptionKeyPair(groupPublicKey, updatedMembers)
|
||||
}
|
||||
// Notify the user
|
||||
val infoType = SignalServiceProtos.GroupContext.Type.UPDATE
|
||||
val infoType = SignalServiceGroup.Type.MEMBER_REMOVED
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime)
|
||||
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, membersToRemove, admins, threadID, sentTime)
|
||||
}
|
||||
|
||||
fun MessageSender.leave(groupPublicKey: String, notifyUser: Boolean = true): Promise<Unit, Exception> {
|
||||
@@ -212,7 +213,7 @@ fun MessageSender.leave(groupPublicKey: String, notifyUser: Boolean = true): Pro
|
||||
storage.setActive(groupID, false)
|
||||
sendNonDurably(closedGroupControlMessage, Address.fromSerialized(groupID)).success {
|
||||
// Notify the user
|
||||
val infoType = SignalServiceProtos.GroupContext.Type.QUIT
|
||||
val infoType = SignalServiceGroup.Type.QUIT
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
if (notifyUser) {
|
||||
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime)
|
||||
|
@@ -0,0 +1,16 @@
|
||||
package org.session.libsession.messaging.sending_receiving.dataextraction
|
||||
|
||||
class DataExtractionNotificationInfoMessage {
|
||||
|
||||
enum class Kind {
|
||||
SCREENSHOT,
|
||||
MEDIA_SAVED
|
||||
}
|
||||
|
||||
var kind: Kind? = null
|
||||
|
||||
constructor(kind: Kind?) {
|
||||
this.kind = kind
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,102 @@
|
||||
package org.session.libsession.messaging.utilities
|
||||
|
||||
import android.content.Context
|
||||
import org.session.libsession.R
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.messaging.sending_receiving.dataextraction.DataExtractionNotificationInfoMessage
|
||||
import org.session.libsession.utilities.ExpirationUtil
|
||||
import org.session.libsignal.service.api.messages.SignalServiceGroup
|
||||
|
||||
object UpdateMessageBuilder {
|
||||
|
||||
fun buildGroupUpdateMessage(context: Context, updateMessageData: UpdateMessageData, sender: String? = null, isOutgoing: Boolean = false): String {
|
||||
var message = ""
|
||||
val updateData = updateMessageData.kind ?: return message
|
||||
if (!isOutgoing && sender == null) return message
|
||||
val senderName: String = if (!isOutgoing) {
|
||||
MessagingConfiguration.shared.storage.getDisplayNameForRecipient(sender!!) ?: sender
|
||||
} else { context.getString(R.string.MessageRecord_you) }
|
||||
|
||||
when (updateData) {
|
||||
is UpdateMessageData.Kind.GroupCreation -> {
|
||||
message = if (isOutgoing) {
|
||||
context.getString(R.string.MessageRecord_you_created_a_new_group)
|
||||
} else {
|
||||
context.getString(R.string.MessageRecord_s_added_you_to_the_group, senderName)
|
||||
}
|
||||
}
|
||||
is UpdateMessageData.Kind.GroupNameChange -> {
|
||||
message = if (isOutgoing) {
|
||||
context.getString(R.string.MessageRecord_you_renamed_the_group_to_s, updateData.name)
|
||||
} else {
|
||||
context.getString(R.string.MessageRecord_s_renamed_the_group_to_s, senderName, updateData.name)
|
||||
}
|
||||
}
|
||||
is UpdateMessageData.Kind.GroupMemberAdded -> {
|
||||
val members = updateData.updatedMembers.joinToString(", ") {
|
||||
MessagingConfiguration.shared.storage.getDisplayNameForRecipient(it) ?: it
|
||||
}
|
||||
message = if (isOutgoing) {
|
||||
context.getString(R.string.MessageRecord_you_added_s_to_the_group, members)
|
||||
} else {
|
||||
context.getString(R.string.MessageRecord_s_added_s_to_the_group, senderName, members)
|
||||
}
|
||||
}
|
||||
is UpdateMessageData.Kind.GroupMemberRemoved -> {
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val userPublicKey = storage.getUserPublicKey()!!
|
||||
// 1st case: you are part of the removed members
|
||||
message = if (userPublicKey in updateData.updatedMembers) {
|
||||
if (isOutgoing) {
|
||||
context.getString(R.string.MessageRecord_left_group)
|
||||
} else {
|
||||
context.getString(R.string.MessageRecord_you_were_removed_from_the_group)
|
||||
}
|
||||
} else {
|
||||
// 2nd case: you are not part of the removed members
|
||||
val members = updateData.updatedMembers.joinToString(", ") {
|
||||
storage.getDisplayNameForRecipient(it) ?: it
|
||||
}
|
||||
if (isOutgoing) {
|
||||
context.getString(R.string.MessageRecord_you_removed_s_from_the_group, members)
|
||||
} else {
|
||||
context.getString(R.string.MessageRecord_s_removed_s_from_the_group, senderName, members)
|
||||
}
|
||||
}
|
||||
}
|
||||
is UpdateMessageData.Kind.GroupMemberLeft -> {
|
||||
message = if (isOutgoing) {
|
||||
context.getString(R.string.MessageRecord_left_group)
|
||||
} else {
|
||||
context.getString(R.string.ConversationItem_group_action_left, senderName)
|
||||
}
|
||||
}
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
fun buildExpirationTimerMessage(context: Context, duration: Long, sender: String? = null, isOutgoing: Boolean = false): String {
|
||||
if (!isOutgoing && sender == null) return ""
|
||||
val senderName: String? = if (!isOutgoing) {
|
||||
MessagingConfiguration.shared.storage.getDisplayNameForRecipient(sender!!) ?: sender
|
||||
} else { context.getString(R.string.MessageRecord_you) }
|
||||
return if (duration <= 0) {
|
||||
if (isOutgoing) context.getString(R.string.MessageRecord_you_disabled_disappearing_messages)
|
||||
else context.getString(R.string.MessageRecord_s_disabled_disappearing_messages, senderName)
|
||||
} else {
|
||||
val time = ExpirationUtil.getExpirationDisplayValue(context, duration.toInt())
|
||||
if (isOutgoing)context.getString(R.string.MessageRecord_you_set_disappearing_message_time_to_s, time)
|
||||
else context.getString(R.string.MessageRecord_s_set_disappearing_message_time_to_s, senderName, time)
|
||||
}
|
||||
}
|
||||
|
||||
fun buildDataExtractionMessage(context: Context, kind: DataExtractionNotificationInfoMessage.Kind, sender: String? = null): String {
|
||||
val senderName = MessagingConfiguration.shared.storage.getDisplayNameForRecipient(sender!!) ?: sender
|
||||
return when (kind) {
|
||||
DataExtractionNotificationInfoMessage.Kind.SCREENSHOT ->
|
||||
context.getString(R.string.MessageRecord_s_took_a_screenshot, senderName)
|
||||
DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED ->
|
||||
context.getString(R.string.MessageRecord_media_saved_by_s, senderName)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,70 @@
|
||||
package org.session.libsession.messaging.utilities
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonSubTypes
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo
|
||||
import com.fasterxml.jackson.core.JsonParseException
|
||||
import org.session.libsignal.service.api.messages.SignalServiceGroup
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import java.util.*
|
||||
|
||||
// class used to save update messages details
|
||||
class UpdateMessageData () {
|
||||
|
||||
var kind: Kind? = null
|
||||
|
||||
//the annotations below are required for serialization. Any new Kind class MUST be declared as JsonSubTypes as well
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
|
||||
@JsonSubTypes(
|
||||
JsonSubTypes.Type(Kind.GroupCreation::class, name = "GroupCreation"),
|
||||
JsonSubTypes.Type(Kind.GroupNameChange::class, name = "GroupNameChange"),
|
||||
JsonSubTypes.Type(Kind.GroupMemberAdded::class, name = "GroupMemberAdded"),
|
||||
JsonSubTypes.Type(Kind.GroupMemberRemoved::class, name = "GroupMemberRemoved"),
|
||||
JsonSubTypes.Type(Kind.GroupMemberLeft::class, name = "GroupMemberLeft")
|
||||
)
|
||||
sealed class Kind() {
|
||||
class GroupCreation(): Kind()
|
||||
class GroupNameChange(val name: String): Kind() {
|
||||
constructor(): this("") //default constructor required for json serialization
|
||||
}
|
||||
class GroupMemberAdded(val updatedMembers: Collection<String>): Kind() {
|
||||
constructor(): this(Collections.emptyList())
|
||||
}
|
||||
class GroupMemberRemoved(val updatedMembers: Collection<String>): Kind() {
|
||||
constructor(): this(Collections.emptyList())
|
||||
}
|
||||
class GroupMemberLeft(): Kind()
|
||||
}
|
||||
|
||||
constructor(kind: Kind): this() {
|
||||
this.kind = kind
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TAG = UpdateMessageData::class.simpleName
|
||||
|
||||
fun buildGroupUpdate(type: SignalServiceGroup.Type, name: String, members: Collection<String>): UpdateMessageData? {
|
||||
return when(type) {
|
||||
SignalServiceGroup.Type.CREATION -> UpdateMessageData(Kind.GroupCreation())
|
||||
SignalServiceGroup.Type.NAME_CHANGE -> UpdateMessageData(Kind.GroupNameChange(name))
|
||||
SignalServiceGroup.Type.MEMBER_ADDED -> UpdateMessageData(Kind.GroupMemberAdded(members))
|
||||
SignalServiceGroup.Type.MEMBER_REMOVED -> UpdateMessageData(Kind.GroupMemberRemoved(members))
|
||||
SignalServiceGroup.Type.QUIT -> UpdateMessageData(Kind.GroupMemberLeft())
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun fromJSON(json: String): UpdateMessageData? {
|
||||
return try {
|
||||
JsonUtil.fromJson(json, UpdateMessageData::class.java)
|
||||
} catch (e: JsonParseException) {
|
||||
Log.e(TAG, "${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun toJSON(): String {
|
||||
return JsonUtil.toJson(this)
|
||||
}
|
||||
}
|
@@ -487,6 +487,16 @@
|
||||
<string name="MessageRecord_message_encrypted_with_a_legacy_protocol_version_that_is_no_longer_supported">Received a message encrypted using an old version of Session that is no longer supported. Please ask the sender to update to the most recent version and resend the message.</string>
|
||||
<string name="MessageRecord_left_group">You have left the group.</string>
|
||||
<string name="MessageRecord_you_updated_group">You updated the group.</string>
|
||||
<string name="MessageRecord_you_created_a_new_group">You created a new group.</string>
|
||||
<string name="MessageRecord_s_added_you_to_the_group">%1$s added you to the group.</string>
|
||||
<string name="MessageRecord_you_renamed_the_group_to_s">You renamed the group to %1$s</string>
|
||||
<string name="MessageRecord_s_renamed_the_group_to_s">%1$s renamed the group to: %2$s</string>
|
||||
<string name="MessageRecord_you_added_s_to_the_group">You added %1$s to the group.</string>
|
||||
<string name="MessageRecord_s_added_s_to_the_group">%1$s added %2$s to the group.</string>
|
||||
<string name="MessageRecord_you_removed_s_from_the_group">You removed %1$s from the group.</string>
|
||||
<string name="MessageRecord_s_removed_s_from_the_group">%1$s removed %2$s from the group.</string>
|
||||
<string name="MessageRecord_you_were_removed_from_the_group">You were removed from the group.</string>
|
||||
<string name="MessageRecord_you">You</string>
|
||||
<string name="MessageRecord_you_called">You called</string>
|
||||
<string name="MessageRecord_called_you">Contact called</string>
|
||||
<string name="MessageRecord_missed_call">Missed call</string>
|
||||
@@ -499,6 +509,8 @@
|
||||
<string name="MessageRecord_s_disabled_disappearing_messages">%1$s disabled disappearing messages.</string>
|
||||
<string name="MessageRecord_you_set_disappearing_message_time_to_s">You set the disappearing message timer to %1$s</string>
|
||||
<string name="MessageRecord_s_set_disappearing_message_time_to_s">%1$s set the disappearing message timer to %2$s</string>
|
||||
<string name="MessageRecord_s_took_a_screenshot">%1$s took a screenshot.</string>
|
||||
<string name="MessageRecord_media_saved_by_s">Media saved by %1$s.</string>
|
||||
<string name="MessageRecord_your_safety_number_with_s_has_changed">Your safety number with %s has changed.</string>
|
||||
<string name="MessageRecord_you_marked_your_safety_number_with_s_verified">You marked your safety number with %s verified</string>
|
||||
<string name="MessageRecord_you_marked_your_safety_number_with_s_verified_from_another_device">You marked your safety number with %s verified from another device</string>
|
||||
|
Reference in New Issue
Block a user