diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaOverviewActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MediaOverviewActivity.java index 0056718e3c..66482c9c57 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MediaOverviewActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MediaOverviewActivity.java @@ -52,6 +52,8 @@ import androidx.viewpager.widget.ViewPager; import com.codewaves.stickyheadergrid.StickyHeaderGridLayoutManager; import com.google.android.material.tabs.TabLayout; +import org.session.libsession.messaging.messages.control.DataExtractionNotification; +import org.session.libsession.messaging.sending_receiving.MessageSender; import org.session.libsession.messaging.threads.Address; import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter; import org.thoughtcrime.securesms.database.MediaDatabase; @@ -351,6 +353,12 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity { saveTask.executeOnExecutor(THREAD_POOL_EXECUTOR, attachments.toArray(new SaveAttachmentTask.Attachment[attachments.size()])); actionMode.finish(); + // Sending a Data extraction notification (for incoming attachments only) + boolean containsIncoming = mediaRecords.parallelStream().anyMatch(m -> !m.isOutgoing()); + if (containsIncoming) { + //TODO uncomment line below when Data extraction will be activated + //sendMediaSavedNotificationIfNeeded(); + } } }.execute(); }) @@ -358,6 +366,16 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity { }, mediaRecords.size()); } + /** + * Send a MediaSaved notification to the recipient + */ + private void sendMediaSavedNotificationIfNeeded() { + // we don't send media saved notification for groups + if (recipient.isGroupRecipient()) return; + DataExtractionNotification message = new DataExtractionNotification(new DataExtractionNotification.Kind.MediaSaved(System.currentTimeMillis())); + MessageSender.send(message, recipient.getAddress()); + } + @SuppressLint("StaticFieldLeak") private void handleDeleteMedia(@NonNull Collection mediaRecords) { int recordCount = mediaRecords.size(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java index 908943f5a8..6edfca5e9d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java @@ -39,7 +39,8 @@ import android.view.Window; import android.widget.FrameLayout; import android.widget.TextView; import android.widget.Toast; - +import org.session.libsession.messaging.messages.control.DataExtractionNotification; +import org.session.libsession.messaging.sending_receiving.MessageSender; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; @@ -52,7 +53,6 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.ViewPager; - import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment; import org.session.libsession.messaging.threads.Address; import org.session.libsession.messaging.threads.recipients.Recipient; @@ -352,11 +352,26 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im saveTask.executeOnExecutor( AsyncTask.THREAD_POOL_EXECUTOR, new Attachment(mediaItem.uri, mediaItem.type, saveDate, null)); + // Sending a Data extraction notification (for incoming attachments only) + if(!mediaItem.outgoing) { + //TODO uncomment line below when Data extraction will be activated + //sendMediaSavedNotificationIfNeeded(); + } }) .execute(); }); } + /** + * Send a MediaSaved notification to the recipient + */ + private void sendMediaSavedNotificationIfNeeded() { + // we don't send media saved notification for groups + if (conversationRecipient.isGroupRecipient()) return; + DataExtractionNotification message = new DataExtractionNotification(new DataExtractionNotification.Kind.MediaSaved(System.currentTimeMillis())); + MessageSender.send(message, conversationRecipient.getAddress()); + } + @SuppressLint("StaticFieldLeak") private void deleteMedia() { MediaItem mediaItem = getCurrentMediaItem(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java index 3cceeb089c..1582f36227 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -158,7 +158,6 @@ import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.ImageSlide; import org.thoughtcrime.securesms.mms.MediaConstraints; import org.thoughtcrime.securesms.mms.MmsException; -import org.session.libsession.messaging.messages.signal.OutgoingExpirationUpdateMessage; import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage; import org.session.libsession.messaging.messages.signal.OutgoingSecureMediaMessage; import org.thoughtcrime.securesms.mms.QuoteId; @@ -175,6 +174,7 @@ import org.session.libsession.messaging.threads.recipients.RecipientModifiedList import org.thoughtcrime.securesms.search.model.MessageResult; import org.session.libsession.messaging.sending_receiving.MessageSender; import org.session.libsession.messaging.messages.signal.OutgoingTextMessage; +import org.thoughtcrime.securesms.service.ExpiringMessageManager; import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.MediaUtil; @@ -806,15 +806,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity @Override protected Void doInBackground(Void... params) { DatabaseFactory.getRecipientDatabase(ConversationActivity.this).setExpireMessages(recipient, expirationTime); - ExpirationTimerUpdate message = new ExpirationTimerUpdate(null, expirationTime); + ExpirationTimerUpdate message = new ExpirationTimerUpdate(expirationTime); + message.setRecipient(recipient.getAddress().serialize()); // we need the recipient in ExpiringMessageManager.insertOutgoingExpirationTimerMessage message.setSentTimestamp(System.currentTimeMillis()); - OutgoingExpirationUpdateMessage outgoingMessage = OutgoingExpirationUpdateMessage.from(message, recipient); - try { - message.setId(DatabaseFactory.getMmsDatabase(ConversationActivity.this).insertMessageOutbox(outgoingMessage, getAllocatedThreadId(ConversationActivity.this), false, null)); - MessageSender.send(message, recipient.getAddress()); - } catch (MmsException e) { - Log.w(TAG, e); - } + ExpiringMessageManager expiringMessageManager = ApplicationContext.getInstance(getApplicationContext()).getExpiringMessageManager(); + expiringMessageManager.setExpirationTimer(message); + MessageSender.send(message, recipient.getAddress()); + return null; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java index 0d30b8ce97..2aedad1119 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -57,6 +57,7 @@ import androidx.recyclerview.widget.RecyclerView.OnScrollListener; import com.annimon.stream.Stream; +import org.session.libsession.messaging.messages.control.DataExtractionNotification; import org.session.libsession.messaging.messages.visible.Quote; import org.session.libsession.messaging.messages.visible.VisibleMessage; import org.session.libsession.messaging.opengroups.OpenGroupAPI; @@ -745,6 +746,11 @@ public class ConversationFragment extends Fragment if (!Util.isEmpty(attachments)) { SaveAttachmentTask saveTask = new SaveAttachmentTask(getActivity()); saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, attachments.toArray(new SaveAttachmentTask.Attachment[0])); + // Sending a Data extraction notification (for incoming attachments only) + if(!message.isOutgoing()) { + //TODO uncomment line below when Data extraction will be activated + //sendMediaSavedNotificationIfNeeded(); + } return; } @@ -757,6 +763,16 @@ public class ConversationFragment extends Fragment }); } + /** + * Send a MediaSaved notification to the recipient + */ + private void sendMediaSavedNotificationIfNeeded() { + // we don't send media saved notification for groups + if (recipient.isGroupRecipient()) return; + DataExtractionNotification message = new DataExtractionNotification(new DataExtractionNotification.Kind.MediaSaved(System.currentTimeMillis())); + MessageSender.send(message, recipient.getAddress()); + } + @Override public @NonNull Loader onCreateLoader(int id, Bundle args) { Log.i(TAG, "onCreateLoader"); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java index 6b76c0572a..91d1c4244b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java @@ -14,11 +14,10 @@ import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; - +import org.session.libsession.messaging.sending_receiving.dataextraction.DataExtractionNotificationInfoMessage; import org.thoughtcrime.securesms.BindableConversationItem; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.loki.utilities.GeneralUtilitiesKt; -import org.thoughtcrime.securesms.loki.utilities.GroupDescription; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.util.DateUtils; import org.session.libsignal.libsignal.util.guava.Optional; @@ -106,6 +105,8 @@ public class ConversationUpdateItem extends LinearLayout else if (messageRecord.isCallLog()) setCallRecord(messageRecord); else if (messageRecord.isJoined()) setJoinedRecord(messageRecord); else if (messageRecord.isExpirationTimerUpdate()) setTimerRecord(messageRecord); + else if (messageRecord.isScreenshotExtraction()) setDataExtractionRecord(messageRecord, DataExtractionNotificationInfoMessage.Kind.SCREENSHOT); + else if (messageRecord.isMediaSavedExtraction()) setDataExtractionRecord(messageRecord, DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED); else if (messageRecord.isEndSession()) setEndSessionRecord(messageRecord); else if (messageRecord.isIdentityUpdate()) setIdentityRecord(messageRecord); else if (messageRecord.isIdentityVerified() || @@ -149,6 +150,22 @@ public class ConversationUpdateItem extends LinearLayout date.setVisibility(GONE); } + private void setDataExtractionRecord(final MessageRecord messageRecord, DataExtractionNotificationInfoMessage.Kind kind) { + @ColorInt int color = GeneralUtilitiesKt.getColorWithID(getResources(), R.color.text, getContext().getTheme()); + if (kind == DataExtractionNotificationInfoMessage.Kind.SCREENSHOT) { + icon.setImageResource(R.drawable.quick_camera_dark); + } else if (kind == DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED) { + icon.setImageResource(R.drawable.ic_file_download_white_36dp); + } + icon.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)); + + body.setText(messageRecord.getDisplayBody(getContext())); + + title.setVisibility(VISIBLE); + body.setVisibility(VISIBLE); + date.setVisibility(GONE); + } + private void setIdentityRecord(final MessageRecord messageRecord) { icon.setImageResource(R.drawable.ic_security_white_24dp); icon.setColorFilter(new PorterDuffColorFilter(Color.parseColor("#757575"), PorterDuff.Mode.MULTIPLY)); @@ -174,8 +191,6 @@ public class ConversationUpdateItem extends LinearLayout private void setGroupRecord(MessageRecord messageRecord) { icon.setImageResource(R.drawable.ic_group_grey600_24dp); icon.clearColorFilter(); - - GroupDescription.Companion.getDescription(getContext(), messageRecord.getBody()).addListener(this); body.setText(messageRecord.getDisplayBody(getContext())); title.setVisibility(GONE); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java index 700d64101a..fd3c793d1f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -514,15 +514,7 @@ public class MmsDatabase extends MessagingDatabase { } } - if (body != null && (Types.isGroupQuit(outboxType) || Types.isGroupUpdate(outboxType))) { - return new OutgoingGroupMediaMessage(recipient, body, attachments, timestamp, 0, quote, contacts, previews); - } else if (Types.isExpirationTimerUpdate(outboxType)) { - return new OutgoingExpirationUpdateMessage(recipient, timestamp, expiresIn); - } - - boolean expirationTimer = (outboxType & Types.EXPIRATION_TIMER_UPDATE_BIT) != 0; - - OutgoingMediaMessage message = new OutgoingMediaMessage(recipient, body, attachments, timestamp, subscriptionId, expiresIn, expirationTimer, distributionType, quote, contacts, previews, networkFailures, mismatches); + OutgoingMediaMessage message = new OutgoingMediaMessage(recipient, body, attachments, timestamp, subscriptionId, expiresIn, distributionType, quote, contacts, previews, networkFailures, mismatches); if (Types.isSecureType(outboxType)) { return new OutgoingSecureMediaMessage(message); @@ -532,8 +524,6 @@ public class MmsDatabase extends MessagingDatabase { } throw new NoSuchMessageException("No record found for id: " + messageId); - } catch (IOException e) { - throw new MmsException(e); } finally { if (cursor != null) cursor.close(); @@ -689,8 +679,19 @@ public class MmsDatabase extends MessagingDatabase { { if (threadId == -1) { if(retrieved.isGroup()) { - ByteString decodedGroupId = ((OutgoingGroupMediaMessage)retrieved).getGroupContext().getId(); - String groupId = GroupUtil.doubleEncodeGroupID(decodedGroupId.toByteArray()); + String decodedGroupId; + if (retrieved instanceof OutgoingExpirationUpdateMessage) { + decodedGroupId = ((OutgoingExpirationUpdateMessage)retrieved).getGroupId(); + } else { + decodedGroupId = ((OutgoingGroupMediaMessage)retrieved).getGroupId(); + } + String groupId; + try { + groupId = GroupUtil.doubleEncodeGroupID(decodedGroupId); + } catch (IOException e) { + Log.e(TAG, "Couldn't encrypt group ID"); + throw new MmsException(e); + } Recipient group = Recipient.from(context, Address.fromSerialized(groupId), false); threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(group); } else { @@ -718,6 +719,14 @@ public class MmsDatabase extends MessagingDatabase { type |= Types.EXPIRATION_TIMER_UPDATE_BIT; } + if (retrieved.isScreenshotDataExtraction()) { + type |= Types.SCREENSHOT_EXTRACTION_BIT; + } + + if (retrieved.isMediaSavedDataExtraction()) { + type |= Types.MEDIA_SAVED_EXTRACTION_BIT; + } + return insertMessageInbox(retrieved, "", threadId, type, serverTimestamp); } @@ -745,9 +754,8 @@ public class MmsDatabase extends MessagingDatabase { if (message.isSecure()) type |= (Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT); if (forceSms) type |= Types.MESSAGE_FORCE_SMS_BIT; - if (message.isGroup()) { - if (((OutgoingGroupMediaMessage)message).isGroupUpdate()) type |= Types.GROUP_UPDATE_BIT; - else if (((OutgoingGroupMediaMessage)message).isGroupQuit()) type |= Types.GROUP_QUIT_BIT; + if (message.isGroup() && message instanceof OutgoingGroupMediaMessage) { + if (((OutgoingGroupMediaMessage)message).isUpdateMessage()) type |= Types.GROUP_UPDATE_MESSAGE_BIT; } if (message.isExpirationUpdate()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java index 36b86023e6..024d1f51dd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java @@ -70,6 +70,11 @@ public interface MmsSmsColumns { protected static final long GROUP_UPDATE_BIT = 0x10000; protected static final long GROUP_QUIT_BIT = 0x20000; protected static final long EXPIRATION_TIMER_UPDATE_BIT = 0x40000; + protected static final long GROUP_UPDATE_MESSAGE_BIT = 0x80000; + + // Data Extraction Information + protected static final long MEDIA_SAVED_EXTRACTION_BIT = 0x01000; + protected static final long SCREENSHOT_EXTRACTION_BIT = 0x02000; // Encrypted Storage Information XXX public static final long ENCRYPTION_MASK = 0xFF000000; @@ -197,6 +202,14 @@ public interface MmsSmsColumns { return (type & EXPIRATION_TIMER_UPDATE_BIT) != 0; } + public static boolean isMediaSavedExtraction(long type) { + return (type & MEDIA_SAVED_EXTRACTION_BIT) != 0; + } + + public static boolean isScreenshotExtraction(long type) { + return (type & SCREENSHOT_EXTRACTION_BIT) != 0; + } + public static boolean isIncomingCall(long type) { return type == INCOMING_CALL_TYPE; } @@ -213,6 +226,8 @@ public interface MmsSmsColumns { return (type & GROUP_UPDATE_BIT) != 0; } + public static boolean isGroupUpdateMessage(long type) { return (type & GROUP_UPDATE_MESSAGE_BIT) != 0; } + public static boolean isGroupQuit(long type) { return (type & GROUP_QUIT_BIT) != 0; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java index d5c34c5c54..226645b39c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -347,8 +347,7 @@ public class SmsDatabase extends MessagingDatabase { type |= Types.SECURE_MESSAGE_BIT; } else if (message.isGroup()) { type |= Types.SECURE_MESSAGE_BIT; - if (((IncomingGroupMessage)message).isUpdate()) type |= Types.GROUP_UPDATE_BIT; - else if (((IncomingGroupMessage)message).isQuit()) type |= Types.GROUP_QUIT_BIT; + if (((IncomingGroupMessage)message).isUpdateMessage()) type |= Types.GROUP_UPDATE_MESSAGE_BIT; } if (message.isPush()) type |= Types.PUSH_MESSAGE_BIT; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index 50d7232710..0a58ac42fd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -16,11 +16,15 @@ 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.attachments.DatabaseAttachment +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.quotes.QuoteModel import org.session.libsession.messaging.threads.Address +import org.session.libsession.messaging.threads.Address.Companion.fromSerialized import org.session.libsession.messaging.threads.GroupRecord import org.session.libsession.messaging.threads.recipients.Recipient +import org.session.libsession.messaging.utilities.UpdateMessageBuilder +import org.session.libsession.messaging.utilities.UpdateMessageData import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.IdentityKeyUtil import org.session.libsession.utilities.TextSecurePreferences @@ -40,6 +44,7 @@ import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities import org.thoughtcrime.securesms.loki.utilities.get import org.thoughtcrime.securesms.loki.utilities.getString import org.thoughtcrime.securesms.mms.PartAuthority +import java.util.* class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), StorageProtocol { override fun getUserPublicKey(): String? { @@ -83,6 +88,11 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, return recipient.profileKey } + override fun getDisplayNameForRecipient(recipientPublicKey: String): String? { + val database = DatabaseFactory.getLokiUserDatabase(context) + return database.getDisplayName(recipientPublicKey) + } + override fun setProfileKeyForRecipient(recipientPublicKey: String, profileKey: ByteArray) { val address = Address.fromSerialized(recipientPublicKey) val recipient = Recipient.from(context, address, false) @@ -138,7 +148,6 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, val linkPreviews: Optional> = if (linkPreview.isEmpty()) Optional.absent() else Optional.of(linkPreview.mapNotNull { it!! }) val mmsDatabase = DatabaseFactory.getMmsDatabase(context) val insertResult = if (message.sender == getUserPublicKey()) { - val mediaMessage = OutgoingMediaMessage.from(message, targetRecipient, pointerAttachments, quote.orNull(), linkPreviews.orNull()?.firstOrNull()) mmsDatabase.beginTransaction() mmsDatabase.insertSecureDecryptedMessageOutbox(mediaMessage, message.threadID ?: -1, message.sentTimestamp!!) @@ -394,33 +403,24 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, DatabaseFactory.getGroupDatabase(context).updateMembers(groupID, members) } - override fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type0: SignalServiceProtos.GroupContext.Type, type1: SignalServiceGroup.Type, name: String, members: Collection, admins: Collection, sentTimestamp: Long) { - val groupContextBuilder = SignalServiceProtos.GroupContext.newBuilder() - .setId(ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupID))) - .setType(type0) - .setName(name) - .addAllMembers(members) - .addAllAdmins(admins) - val group = SignalServiceGroup(type1, GroupUtil.getDecodedGroupIDAsData(groupID), SignalServiceGroup.GroupType.SIGNAL, name, members.toList(), null, admins.toList()) + override fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type: SignalServiceGroup.Type, name: String, members: Collection, admins: Collection, sentTimestamp: Long) { + val group = SignalServiceGroup(type, GroupUtil.getDecodedGroupIDAsData(groupID), SignalServiceGroup.GroupType.SIGNAL, name, members.toList(), null, admins.toList()) val m = IncomingTextMessage(Address.fromSerialized(senderPublicKey), 1, sentTimestamp, "", Optional.of(group), 0, true) - val infoMessage = IncomingGroupMessage(m, groupContextBuilder.build(), "") + val updateData = UpdateMessageData.buildGroupUpdate(type, name, members)?.toJSON() + val infoMessage = IncomingGroupMessage(m, groupID, updateData, true) val smsDB = DatabaseFactory.getSmsDatabase(context) smsDB.insertMessageInbox(infoMessage) } - override fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceProtos.GroupContext.Type, name: String, members: Collection, admins: Collection, threadID: Long, sentTimestamp: Long) { + override fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceGroup.Type, name: String, members: Collection, admins: Collection, threadID: Long, sentTimestamp: Long) { val userPublicKey = getUserPublicKey() val recipient = Recipient.from(context, Address.fromSerialized(groupID), false) - val groupContextBuilder = SignalServiceProtos.GroupContext.newBuilder() - .setId(ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupID))) - .setType(type) - .setName(name) - .addAllMembers(members) - .addAllAdmins(admins) - val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, sentTimestamp, 0, false, null, listOf(), listOf()) + + val updateData = UpdateMessageData.buildGroupUpdate(type, name, members)?.toJSON() ?: "" + val infoMessage = OutgoingGroupMediaMessage(recipient, updateData, groupID, null, sentTimestamp, 0, true, null, listOf(), listOf()) val mmsDB = DatabaseFactory.getMmsDatabase(context) val mmsSmsDB = DatabaseFactory.getMmsSmsDatabase(context) - if (mmsSmsDB.getMessageFor(sentTimestamp,userPublicKey) != null) return + if (mmsSmsDB.getMessageFor(sentTimestamp, userPublicKey) != null) return val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null) mmsDB.markAsSent(infoMessageID, true) } @@ -578,4 +578,26 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, override fun getAttachmentThumbnailUri(attachmentId: AttachmentId): Uri { return PartAuthority.getAttachmentThumbnailUri(attachmentId) } + + // Data Extraction Notification + override fun insertDataExtractionNotificationMessage(senderPublicKey: String, message: DataExtractionNotificationInfoMessage, sentTimestamp: Long) { + val database = DatabaseFactory.getMmsDatabase(context) + val address = fromSerialized(senderPublicKey) + val recipient = Recipient.from(context, address, false) + + if (recipient.isBlocked) return + + val mediaMessage = IncomingMediaMessage(address, sentTimestamp, -1, + 0, false, + false, + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.of(message)) + + database.insertSecureDecryptedMessageInbox(mediaMessage, -1) + } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java index ab40f3381c..f98dcda5d9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java @@ -109,6 +109,7 @@ public abstract class DisplayRecord { public boolean isLokiSessionRestoreDone() { return SmsDatabase.Types.isLokiSessionRestoreDoneType(type); } + // TODO isGroupUpdate and isGroupQuit are kept for compatibility with old update messages, they can be removed later on public boolean isGroupUpdate() { return SmsDatabase.Types.isGroupUpdate(type); } @@ -117,14 +118,33 @@ public abstract class DisplayRecord { return SmsDatabase.Types.isGroupQuit(type); } + public boolean isGroupUpdateMessage() { + return SmsDatabase.Types.isGroupUpdateMessage(type); + } + + //TODO isGroupAction can be replaced by isGroupUpdateMessage in the code when the 2 functions above are removed public boolean isGroupAction() { - return isGroupUpdate() || isGroupQuit(); + return isGroupUpdate() || isGroupQuit() || isGroupUpdateMessage(); } public boolean isExpirationTimerUpdate() { return SmsDatabase.Types.isExpirationTimerUpdate(type); } + // Data extraction + + public boolean isMediaSavedExtraction() { + return MmsSmsColumns.Types.isMediaSavedExtraction(type); + } + + public boolean isScreenshotExtraction() { + return MmsSmsColumns.Types.isScreenshotExtraction(type); + } + + public boolean isDataExtraction() { + return isMediaSavedExtraction() || isScreenshotExtraction(); + } + public boolean isCallLog() { return SmsDatabase.Types.isCallLog(type); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java index b41e4ae78e..9ecb9bac2e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java @@ -24,14 +24,16 @@ import android.text.style.RelativeSizeSpan; import android.text.style.StyleSpan; import network.loki.messenger.R; + +import org.session.libsession.messaging.sending_receiving.dataextraction.DataExtractionNotificationInfoMessage; +import org.session.libsession.messaging.utilities.UpdateMessageBuilder; +import org.session.libsession.messaging.utilities.UpdateMessageData; import org.thoughtcrime.securesms.database.MmsSmsColumns; import org.thoughtcrime.securesms.database.SmsDatabase; import org.session.libsession.database.documents.IdentityKeyMismatch; import org.session.libsession.database.documents.NetworkFailure; import org.session.libsession.messaging.threads.recipients.Recipient; -import org.session.libsession.utilities.ExpirationUtil; -import org.thoughtcrime.securesms.loki.utilities.GroupDescription; import java.util.List; @@ -90,39 +92,25 @@ public abstract class MessageRecord extends DisplayRecord { @Override public SpannableString getDisplayBody(@NonNull Context context) { - if (isGroupUpdate() && isOutgoing()) { + if(isGroupUpdateMessage()) { + UpdateMessageData updateMessageData = UpdateMessageData.Companion.fromJSON(getBody()); + return new SpannableString(UpdateMessageBuilder.INSTANCE.buildGroupUpdateMessage(context, updateMessageData, getIndividualRecipient().getAddress().serialize(), isOutgoing())); + } else if (isExpirationTimerUpdate()) { + int seconds = (int) (getExpiresIn() / 1000); + return new SpannableString(UpdateMessageBuilder.INSTANCE.buildExpirationTimerMessage(context, seconds, getIndividualRecipient().getAddress().serialize(), isOutgoing())); + } else if (isDataExtraction()) { + if (isScreenshotExtraction()) return new SpannableString((UpdateMessageBuilder.INSTANCE.buildDataExtractionMessage(context, DataExtractionNotificationInfoMessage.Kind.SCREENSHOT, getIndividualRecipient().getAddress().serialize()))); + else if (isMediaSavedExtraction()) return new SpannableString((UpdateMessageBuilder.INSTANCE.buildDataExtractionMessage(context, DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED, getIndividualRecipient().getAddress().serialize()))); + } + // TODO below lines are left here for compatibility with older group update messages, it can be deleted later on + else if (isGroupUpdate() && isOutgoing()) { return new SpannableString(context.getString(R.string.MessageRecord_you_updated_group)); } else if (isGroupUpdate()) { - return new SpannableString(GroupDescription.Companion.getDescription(context, getBody()).toString(getIndividualRecipient())); + return new SpannableString(context.getString(R.string.MessageRecord_s_updated_group, getIndividualRecipient().toShortString())); } else if (isGroupQuit() && isOutgoing()) { return new SpannableString(context.getString(R.string.MessageRecord_left_group)); } else if (isGroupQuit()) { return new SpannableString(context.getString(R.string.ConversationItem_group_action_left, getIndividualRecipient().toShortString())); - } else if (isIncomingCall()) { - return new SpannableString(context.getString(R.string.MessageRecord_s_called_you, getIndividualRecipient().toShortString())); - } else if (isOutgoingCall()) { - return new SpannableString(context.getString(R.string.MessageRecord_you_called)); - } else if (isMissedCall()) { - return new SpannableString(context.getString(R.string.MessageRecord_missed_call)); - } else if (isJoined()) { - return new SpannableString(context.getString(R.string.MessageRecord_s_joined_signal, getIndividualRecipient().toShortString())); - } else if (isExpirationTimerUpdate()) { - int seconds = (int)(getExpiresIn() / 1000); - if (seconds <= 0) { - return isOutgoing() ? new SpannableString(context.getString(R.string.MessageRecord_you_disabled_disappearing_messages)) - : new SpannableString(context.getString(R.string.MessageRecord_s_disabled_disappearing_messages, getIndividualRecipient().toShortString())); - } - String time = ExpirationUtil.getExpirationDisplayValue(context, seconds); - return isOutgoing() ? new SpannableString(context.getString(R.string.MessageRecord_you_set_disappearing_message_time_to_s, time)) - : new SpannableString(context.getString(R.string.MessageRecord_s_set_disappearing_message_time_to_s, getIndividualRecipient().toShortString(), time)); - } else if (isIdentityUpdate()) { - return new SpannableString(context.getString(R.string.MessageRecord_your_safety_number_with_s_has_changed, getIndividualRecipient().toShortString())); - } else if (isIdentityVerified()) { - if (isOutgoing()) return new SpannableString(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_verified, getIndividualRecipient().toShortString())); - else return new SpannableString(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_verified_from_another_device, getIndividualRecipient().toShortString())); - } else if (isIdentityDefault()) { - if (isOutgoing()) return new SpannableString(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_unverified, getIndividualRecipient().toShortString())); - else return new SpannableString(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_unverified_from_another_device, getIndividualRecipient().toShortString())); } return new SpannableString(getBody()); @@ -175,7 +163,7 @@ public abstract class MessageRecord extends DisplayRecord { } public boolean isUpdate() { - return isGroupAction() || isJoined() || isExpirationTimerUpdate() || isCallLog() || + return isGroupAction() || isJoined() || isExpirationTimerUpdate() || isCallLog() || isDataExtraction() || isEndSession() || isIdentityUpdate() || isIdentityVerified() || isIdentityDefault() || isLokiSessionRestoreSent() || isLokiSessionRestoreDone(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java index e2a286c854..3315ba94f2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java @@ -28,6 +28,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.session.libsession.messaging.threads.recipients.Recipient; +import org.session.libsession.messaging.utilities.UpdateMessageBuilder; +import org.session.libsession.messaging.utilities.UpdateMessageData; import org.session.libsession.utilities.ExpirationUtil; import org.thoughtcrime.securesms.database.MmsSmsColumns; import org.thoughtcrime.securesms.database.SmsDatabase; @@ -73,7 +75,7 @@ public class ThreadRecord extends DisplayRecord { @Override public SpannableString getDisplayBody(@NonNull Context context) { Recipient recipient = getRecipient(); - if (isGroupUpdate()) { + if (isGroupUpdate() || isGroupUpdateMessage()) { return emphasisAdded(context.getString(R.string.ThreadRecord_group_updated)); } else if (isGroupQuit()) { return emphasisAdded(context.getString(R.string.ThreadRecord_left_the_group)); @@ -103,12 +105,16 @@ public class ThreadRecord extends DisplayRecord { } else if (SmsDatabase.Types.isJoinedType(type)) { return emphasisAdded(context.getString(R.string.ThreadRecord_s_is_on_signal, getRecipient().toShortString())); } else if (SmsDatabase.Types.isExpirationTimerUpdate(type)) { - int seconds = (int)(getExpiresIn() / 1000); + int seconds = (int) (getExpiresIn() / 1000); if (seconds <= 0) { return emphasisAdded(context.getString(R.string.ThreadRecord_disappearing_messages_disabled)); } String time = ExpirationUtil.getExpirationDisplayValue(context, seconds); return emphasisAdded(context.getString(R.string.ThreadRecord_disappearing_message_time_updated_to_s, time)); + } else if (MmsSmsColumns.Types.isMediaSavedExtraction(type)) { + return emphasisAdded(context.getString(R.string.ThreadRecord_media_saved_by_s, getRecipient().toShortString())); + } else if (MmsSmsColumns.Types.isScreenshotExtraction(type)) { + return emphasisAdded(context.getString(R.string.ThreadRecord_s_took_a_screenshot, getRecipient().toShortString())); } else if (SmsDatabase.Types.isIdentityUpdate(type)) { if (getRecipient().isGroupRecipient()) return emphasisAdded(context.getString(R.string.ThreadRecord_safety_number_changed)); else return emphasisAdded(context.getString(R.string.ThreadRecord_your_safety_number_with_s_has_changed, getRecipient().toShortString())); diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt index 2ad317bdba..f3a7f94bd4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt @@ -32,6 +32,10 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM set(newValue) { field = newValue; invalidateOptionsMenu() } private var members = listOf() set(value) { field = value; selectContactsAdapter.members = value } + private val publicKey: String + get() { + return TextSecurePreferences.getLocalNumber(this)!! + } private val selectContactsAdapter by lazy { SelectContactsAdapter(this, GlideApp.with(this)) @@ -72,7 +76,8 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM } private fun update(members: List) { - this.members = members + //if there is a Note to self conversation, it loads self in the list, so we need to remove it here + this.members = members.minus(publicKey) mainContentContainer.visibility = if (members.isEmpty()) View.GONE else View.VISIBLE emptyStateContainer.visibility = if (members.isEmpty()) View.VISIBLE else View.GONE invalidateOptionsMenu() diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SelectContactsAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SelectContactsAdapter.kt index 135762f661..e8bbede270 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SelectContactsAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SelectContactsAdapter.kt @@ -36,6 +36,27 @@ class SelectContactsAdapter(private val context: Context, private val glide: Gli isSelected) } + override fun onBindViewHolder(viewHolder: ViewHolder, + position: Int, + payloads: MutableList) { + if (payloads.isNotEmpty()) { + // Because these updates can be batched, + // there can be multiple payloads for a single bind + when (payloads[0]) { + Payload.MEMBER_CLICKED -> { + val member = members[position] + val isSelected = selectedMembers.contains(member) + viewHolder.view.toggleCheckbox(isSelected) + } + } + } else { + // When payload list is empty, + // or we don't have logic to handle a given type, + // default to full bind: + this.onBindViewHolder(viewHolder, position) + } + } + private fun onMemberClick(member: String) { if (selectedMembers.contains(member)) { selectedMembers.remove(member) @@ -43,6 +64,11 @@ class SelectContactsAdapter(private val context: Context, private val glide: Gli selectedMembers.add(member) } val index = members.indexOf(member) - notifyItemChanged(index) + notifyItemChanged(index, Payload.MEMBER_CLICKED) } -} \ No newline at end of file + + // define below the different events used to notify the adapter + enum class Payload { + MEMBER_CLICKED + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt index d2b0ad11aa..df1d7c0b52 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt @@ -9,7 +9,6 @@ import org.session.libsignal.libsignal.ecc.ECKeyPair import org.session.libsignal.service.api.messages.SignalServiceGroup import org.session.libsignal.service.internal.push.SignalServiceProtos import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage -import org.session.libsignal.service.internal.push.SignalServiceProtos.GroupContext import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded import org.session.libsignal.service.loki.utilities.toHexString import org.thoughtcrime.securesms.database.DatabaseFactory @@ -104,11 +103,11 @@ object ClosedGroupsProtocolV2 { apiDB.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey) // Notify the user (if we didn't make the group) if (userPublicKey != senderPublicKey) { - DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp) + DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp) } else if (prevGroup == null) { // only notify if we created this group val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID) - DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp) + DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.UPDATE, name, members, admins, threadID, sentTimestamp) } // Notify the PN server LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey) @@ -156,14 +155,14 @@ object ClosedGroupsProtocolV2 { MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, newMembers) } } - val (contextType, signalType) = - if (senderLeft) GroupContext.Type.QUIT to SignalServiceGroup.Type.QUIT - else GroupContext.Type.UPDATE to SignalServiceGroup.Type.UPDATE + val type = + if (senderLeft) SignalServiceGroup.Type.QUIT + else SignalServiceGroup.Type.UPDATE if (userPublicKey == senderPublicKey) { val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID) - DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, contextType, name, members, admins, threadID, sentTimestamp) + DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, type, name, members, admins, threadID, sentTimestamp) } else { - DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, contextType, signalType, name, members, admins, sentTimestamp) + DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, type, name, members, admins, sentTimestamp) } } @@ -191,9 +190,9 @@ object ClosedGroupsProtocolV2 { groupDB.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) }) if (userPublicKey == senderPublicKey) { val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID) - DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp) + DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.UPDATE, name, members, admins, threadID, sentTimestamp) } else { - DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp) + DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp) } if (userPublicKey in admins) { // send current encryption key to the latest added members @@ -230,9 +229,9 @@ object ClosedGroupsProtocolV2 { // Notify the user if (userPublicKey == senderPublicKey) { val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID) - DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp) + DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.UPDATE, name, members, admins, threadID, sentTimestamp) } else { - DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp) + DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp) } } @@ -272,9 +271,9 @@ object ClosedGroupsProtocolV2 { // Notify user if (userLeft) { val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID) - DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, GroupContext.Type.QUIT, name, members, admins, threadID, sentTimestamp) + DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.QUIT, name, members, admins, threadID, sentTimestamp) } else { - DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.QUIT, SignalServiceGroup.Type.QUIT, name, members, admins, sentTimestamp) + DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceGroup.Type.QUIT, name, members, admins, sentTimestamp) } } @@ -385,14 +384,13 @@ object ClosedGroupsProtocolV2 { } // Notify the user val wasSenderRemoved = !members.contains(senderPublicKey) - val type0 = if (wasSenderRemoved) GroupContext.Type.QUIT else GroupContext.Type.UPDATE - val type1 = if (wasSenderRemoved) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.UPDATE + val type = if (wasSenderRemoved) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.UPDATE val admins = group.admins.map { it.toString() } if (userPublicKey == senderPublicKey) { val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID) - DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, type0, name, members, admins, threadID, sentTimestamp) + DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, type, name, members, admins, threadID, sentTimestamp) } else { - DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, type0, type1, name, members, admins, sentTimestamp) + DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, type, name, members, admins, sentTimestamp) } } // endregion diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/GroupDescription.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/GroupDescription.kt deleted file mode 100644 index 62d48aa524..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/GroupDescription.kt +++ /dev/null @@ -1,103 +0,0 @@ -package org.thoughtcrime.securesms.loki.utilities - -import android.content.Context -import org.session.libsession.messaging.threads.Address -import org.session.libsession.messaging.threads.recipients.Recipient -import org.session.libsession.messaging.threads.recipients.RecipientModifiedListener -import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsignal.utilities.Base64 -import org.session.libsignal.service.internal.push.SignalServiceProtos -import java.util.* - -import network.loki.messenger.R -import org.session.libsignal.utilities.logging.Log -import java.io.IOException - -class GroupDescription(context: Context, groupContext: SignalServiceProtos.GroupContext?) { - private val context: Context - private val groupContext: SignalServiceProtos.GroupContext? - private val newMembers: MutableList - private val removedMembers: MutableList - private var wasCurrentUserRemoved: Boolean = false - private fun toRecipient(hexEncodedPublicKey: String): Recipient { - val address = Address.fromSerialized(hexEncodedPublicKey) - return Recipient.from(context, address, false) - } - - fun toString(sender: Recipient): String { - if (wasCurrentUserRemoved) { - return context.getString(R.string.GroupUtil_you_were_removed_from_group) - } - val description = StringBuilder() - description.append(context.getString(R.string.MessageRecord_s_updated_group, sender.toShortString())) - if (groupContext == null) { - return description.toString() - } - val title = groupContext.name - if (!newMembers.isEmpty()) { - description.append("\n") - description.append(context.resources.getQuantityString(R.plurals.GroupUtil_joined_the_group, - newMembers.size, toString(newMembers))) - } - if (!removedMembers.isEmpty()) { - description.append("\n") - description.append(context.resources.getQuantityString(R.plurals.GroupUtil_removed_from_the_group, - removedMembers.size, toString(removedMembers))) - } - if (title != null && !title.trim { it <= ' ' }.isEmpty()) { - val separator = if (!newMembers.isEmpty() || !removedMembers.isEmpty()) " " else "\n" - description.append(separator) - description.append(context.getString(R.string.GroupUtil_group_name_is_now, title)) - } - return description.toString() - } - - fun addListener(listener: RecipientModifiedListener?) { - if (!newMembers.isEmpty()) { - for (member in newMembers) { - member.addListener(listener) - } - } - } - - private fun toString(recipients: List): String { - var result = "" - for (i in recipients.indices) { - result += recipients[i].toShortString() - if (i != recipients.size - 1) result += ", " - } - return result - } - - init { - this.context = context.applicationContext - this.groupContext = groupContext - newMembers = LinkedList() - removedMembers = LinkedList() - if (groupContext != null) { - val newMembers = groupContext.newMembersList - for (member in newMembers) { - this.newMembers.add(toRecipient(member)) - } - val removedMembers = groupContext.removedMembersList - for (member in removedMembers) { - this.removedMembers.add(toRecipient(member)) - } - wasCurrentUserRemoved = removedMembers.contains(TextSecurePreferences.getLocalNumber(context)) - } - } - - companion object { - fun getDescription(context: Context, encodedGroup: String?): GroupDescription { - return if (encodedGroup == null) { - GroupDescription(context, null) - } else try { - val groupContext = SignalServiceProtos.GroupContext.parseFrom(Base64.decode(encodedGroup)) - GroupDescription(context, groupContext) - } catch (e: IOException) { - Log.w("Loki", e) - GroupDescription(context, null) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/views/UserView.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/views/UserView.kt index 4c6098a710..19642a6c87 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/views/UserView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/views/UserView.kt @@ -82,8 +82,13 @@ class UserView : LinearLayout { } } + fun toggleCheckbox(isSelected: Boolean = false) { + actionIndicatorImageView.visibility = View.VISIBLE + actionIndicatorImageView.setImageResource(if (isSelected) R.drawable.ic_circle_check else R.drawable.ic_circle) + } + fun unbind() { } // endregion -} \ No newline at end of file +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.java b/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.java index 0a7df527fd..05fed4b9d8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.java @@ -2,22 +2,17 @@ package org.thoughtcrime.securesms.service; import android.content.Context; -import com.google.protobuf.ByteString; import org.jetbrains.annotations.NotNull; import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate; -import org.session.libsession.messaging.messages.signal.OutgoingGroupMediaMessage; -import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage; +import org.session.libsession.messaging.messages.signal.OutgoingExpirationUpdateMessage; import org.session.libsession.messaging.threads.Address; -import org.session.libsession.messaging.threads.DistributionTypes; import org.session.libsession.messaging.threads.recipients.Recipient; import org.session.libsession.utilities.GroupUtil; import org.session.libsession.utilities.SSKEnvironment; import org.session.libsession.utilities.TextSecurePreferences; 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.internal.push.SignalServiceProtos.GroupContext; import org.session.libsignal.utilities.logging.Log; import org.thoughtcrime.securesms.database.DatabaseFactory; @@ -28,7 +23,6 @@ import org.session.libsession.messaging.messages.signal.IncomingMediaMessage; import org.thoughtcrime.securesms.mms.MmsException; import java.io.IOException; -import java.util.Collections; import java.util.Comparator; import java.util.TreeSet; import java.util.concurrent.Executor; @@ -79,8 +73,8 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM String senderPublicKey = message.getSender(); // Notify the user - if (userPublicKey.equals(senderPublicKey)) { - // sender is a linked device + if (senderPublicKey == null || userPublicKey.equals(senderPublicKey)) { + // sender is self or a linked device insertOutgoingExpirationTimerMessage(message); } else { insertIncomingExpirationTimerMessage(message); @@ -100,7 +94,7 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM int duration = message.getDuration(); Optional groupInfo = Optional.absent(); - Address address = Address.fromSerialized((message.getSyncTarget() != null && !message.getSyncTarget().isEmpty()) ? message.getSyncTarget() : senderPublicKey); + Address address = Address.fromSerialized(senderPublicKey); Recipient recipient = Recipient.from(context, address, false); // if the sender is blocked, we don't display the update, except if it's in a closed group @@ -123,6 +117,7 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM Optional.absent(), Optional.absent(), Optional.absent(), + Optional.absent(), Optional.absent()); //insert the timer update message database.insertSecureDecryptedMessageInbox(mediaMessage, -1); @@ -138,41 +133,21 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM private void insertOutgoingExpirationTimerMessage(ExpirationTimerUpdate message) { MmsDatabase database = DatabaseFactory.getMmsDatabase(context); - String senderPublicKey = message.getSender(); Long sentTimestamp = message.getSentTimestamp(); String groupId = message.getGroupPublicKey(); int duration = message.getDuration(); - Address address = Address.fromSerialized((message.getSyncTarget() != null && !message.getSyncTarget().isEmpty()) ? message.getSyncTarget() : senderPublicKey); + Address address = Address.fromSerialized((message.getSyncTarget() != null && !message.getSyncTarget().isEmpty()) ? message.getSyncTarget() : message.getRecipient()); Recipient recipient = Recipient.from(context, address, false); try { + OutgoingExpirationUpdateMessage timerUpdateMessage = new OutgoingExpirationUpdateMessage(recipient, sentTimestamp, duration * 1000L, groupId); + database.insertSecureDecryptedMessageOutbox(timerUpdateMessage, -1, sentTimestamp); + if (groupId != null) { - // conversation is a closed group - GroupContext groupContext = SignalServiceProtos.GroupContext.newBuilder() - .setId(ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupId))).build(); - OutgoingGroupMediaMessage infoMessage = new OutgoingGroupMediaMessage(recipient, groupContext, null, sentTimestamp, duration * 1000L, true, null, Collections.emptyList(), Collections.emptyList()); - database.insertSecureDecryptedMessageOutbox(infoMessage, -1, sentTimestamp); // we need the group ID as recipient for setExpireMessages below recipient = Recipient.from(context, Address.fromSerialized(GroupUtil.doubleEncodeGroupID(groupId)), false); - } else { - // conversation is a 1-1 - OutgoingMediaMessage mediaMessage = new OutgoingMediaMessage(recipient, - null, - Collections.emptyList(), - message.getSentTimestamp(), - -1, - duration * 1000L, - true, - DistributionTypes.DEFAULT, - null, - Collections.emptyList(), - Collections.emptyList(), - Collections.emptyList(), - Collections.emptyList()); - database.insertSecureDecryptedMessageOutbox(mediaMessage, -1, sentTimestamp); } - //set the timer to the conversation DatabaseFactory.getRecipientDatabase(context).setExpireMessages(recipient, duration); diff --git a/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/DataExtractionNotificationManager.kt b/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/DataExtractionNotificationManager.kt deleted file mode 100644 index 0c8fa779d4..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/DataExtractionNotificationManager.kt +++ /dev/null @@ -1,4 +0,0 @@ -package org.thoughtcrime.securesms.sskenvironment - -class DataExtractionNotificationManager { -} \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index d562e0c62a..5fbc37a757 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -418,6 +418,16 @@ Vous avez reçu un message chiffré avec une ancienne version de Signal qui n’est plus prise en charge. Veuillez demander à l’expéditeur de mettre Session à jour vers la version la plus récente et de renvoyer son message. Vous avez quitté le groupe Vous avez mis le groupe à jour. + Vous avez créée un nouveau groupe. + %1$s vous a ajouté au groupe. + Vous avez renommé le groupe en \'%1$s\' + %1$s a renommé le groupe en \'%2$s\' + Vous avez ajouté %1$s au groupe. + %1$s a ajouté %2$s au groupe. + Vous avez supprimé %1$s du groupe. + %1$s a supprimé %2$s du groupe. + Vous avez été supprimé du groupe. + Vous Vous avez appelé Contact appelé Appel manqué @@ -430,6 +440,8 @@ %1$s a désactivé les messages éphémères. Vous avez défini l’expiration des messages éphémères à %1$s. %1$s a défini l’expiration des messages éphémères à %2$s. + %1$s a pris une capture d’écran. + %1$s a sauvegardé un media. Votre numéro de sécurité avec %s a changé. Vous avez marqué votre numéro de sécurité avec %s comme vérifié Vous avez marqué votre numéro de sécurité avec %s comme vérifié à partir d’un autre appareil @@ -1483,4 +1495,35 @@ Vous avez reçu un message d’échange de clés pour une version de protocole i Groupes privés Groupes publics + + + Appliquer + Terminé + + Modifier le groupe + Saisissez un nouveau nom de groupe + Membres + Ajouter des membres + Le nom du groupe ne peut pas être vide + Veuillez saisir un nom plus court + Les groupes doivent avoir au moins un membre + Un des membres du group a un Session ID invalide + Êtes vous sûr de vouloir suprimer ce membre du groupe? + Membre supprimé du groupe + + Supprimer le membre du groupe + + Contacts + + Thème + Jour + Nuit + Système + + Copier le Session ID + + Pièce jointe + Message vocal + Détails + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 34ab7d89b4..697b85ff20 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -493,6 +493,16 @@ 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. You have left the group. You updated the group. + You created a new group. + %1$s added you to the group. + You renamed the group to \'%1$s\' + %1$s renamed the group to \'%2$s\' + You added %1$s to the group. + %1$s added %2$s to the group. + You removed %1$s from the group. + %1$s removed %2$s from the group. + You were removed from the group. + You You called Contact called Missed call @@ -505,6 +515,8 @@ %1$s disabled disappearing messages. You set the disappearing message timer to %1$s %1$s set the disappearing message timer to %2$s + %1$s took a screenshot. + Media saved by %1$s. Your safety number with %s has changed. You marked your safety number with %s verified You marked your safety number with %s verified from another device @@ -703,6 +715,8 @@ %s is on Session! Disappearing messages disabled Disappearing message time set to %s + %s took a screenshot. + Media saved by %s. Safety number changed Your safety number with %s has changed. You marked verified diff --git a/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt b/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt index 64c1828d51..da84c8f6e8 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt @@ -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, admins: Collection, 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, admins: Collection, threadID: Long, sentTimestamp: Long) fun isClosedGroup(publicKey: String): Boolean fun getClosedGroupEncryptionKeyPairs(groupPublicKey: String): MutableList @@ -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, groupPublicKey: String?, openGroupID: String?, attachments: List): Long? -} \ No newline at end of file + + // Data Extraction Notification + fun insertDataExtractionNotificationMessage(senderPublicKey: String, message: DataExtractionNotificationInfoMessage, sentTimestamp: Long) +} diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/DataExtractionNotification.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/DataExtractionNotification.kt index 0526fbec0f..538b0b0001 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/DataExtractionNotification.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/DataExtractionNotification.kt @@ -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 } } - -} \ No newline at end of file +} diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ExpirationTimerUpdate.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ExpirationTimerUpdate.kt index 85f34eed51..5dff39072c 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ExpirationTimerUpdate.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ExpirationTimerUpdate.kt @@ -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 diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/signal/IncomingGroupMessage.java b/libsession/src/main/java/org/session/libsession/messaging/messages/signal/IncomingGroupMessage.java index b0a7de08ee..125267afb7 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/signal/IncomingGroupMessage.java +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/signal/IncomingGroupMessage.java @@ -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; } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/signal/IncomingMediaMessage.java b/libsession/src/main/java/org/session/libsession/messaging/messages/signal/IncomingMediaMessage.java index 64b8dcf2b2..f235e84e92 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/signal/IncomingMediaMessage.java +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/signal/IncomingMediaMessage.java @@ -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 attachments = new LinkedList<>(); private final List sharedContacts = new LinkedList<>(); private final List linkPreviews = new LinkedList<>(); @@ -44,17 +47,19 @@ public class IncomingMediaMessage { Optional> attachments, Optional quote, Optional> sharedContacts, - Optional> linkPreviews) + Optional> linkPreviews, + Optional 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> 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; } diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingExpirationUpdateMessage.java b/libsession/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingExpirationUpdateMessage.java index 74f72c4925..b45f33c78f 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingExpirationUpdateMessage.java +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingExpirationUpdateMessage.java @@ -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(), 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(), 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; + } + } diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingGroupMediaMessage.java b/libsession/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingGroupMediaMessage.java index 7f151ae7ee..574c47cd19 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingGroupMediaMessage.java +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingGroupMediaMessage.java @@ -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 avatar, - long sentTimeMillis, - long expiresIn, - @Nullable QuoteModel quote, - @NonNull List contacts, - @NonNull List 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 contacts, - @NonNull List previews) - { - super(recipient, Base64.encodeBytes(group.toByteArray()), - new LinkedList() {{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 contacts, @NonNull List previews) { - super(recipient, Base64.encodeBytes(group.toByteArray()), + super(recipient, body, new LinkedList() {{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; } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingMediaMessage.java b/libsession/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingMediaMessage.java index 9a6618dc4a..d8d4dff3c5 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingMediaMessage.java +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingMediaMessage.java @@ -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 networkFailures = new LinkedList<>(); @@ -37,7 +36,6 @@ public class OutgoingMediaMessage { public OutgoingMediaMessage(Recipient recipient, String message, List attachments, long sentTimeMillis, int subscriptionId, long expiresIn, - boolean expirationUpdate, int distributionType, @Nullable QuoteModel outgoingQuote, @NonNull List 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; diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingSecureMediaMessage.java b/libsession/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingSecureMediaMessage.java index c7822d8b90..8b5e7ddef0 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingSecureMediaMessage.java +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingSecureMediaMessage.java @@ -19,12 +19,11 @@ public class OutgoingSecureMediaMessage extends OutgoingMediaMessage { long sentTimeMillis, int distributionType, long expiresIn, - boolean expirationUpdate, @Nullable QuoteModel quote, @NonNull List contacts, @NonNull List 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) { diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverHandler.kt index bc2546b0a5..fc1b28e1cc 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverHandler.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverHandler.kt @@ -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!!) } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroup.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroup.kt index 58f8ffe64c..143fa7fb3f 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroup.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroup.kt @@ -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): Promise) 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) { @@ -189,9 +190,9 @@ fun MessageSender.removeMembers(groupPublicKey: String, membersToRemove: List { @@ -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) diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/dataextraction/DataExtractionNotificationInfoMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/dataextraction/DataExtractionNotificationInfoMessage.kt new file mode 100644 index 0000000000..aca360eca6 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/dataextraction/DataExtractionNotificationInfoMessage.kt @@ -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 + } + +} diff --git a/libsession/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageBuilder.kt b/libsession/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageBuilder.kt new file mode 100644 index 0000000000..4239c8bdb2 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageBuilder.kt @@ -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) + } + } +} diff --git a/libsession/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageData.kt b/libsession/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageData.kt new file mode 100644 index 0000000000..4285bab78e --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageData.kt @@ -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): Kind() { + constructor(): this(Collections.emptyList()) + } + class GroupMemberRemoved(val updatedMembers: Collection): 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): 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) + } +} diff --git a/libsession/src/main/res/values/strings.xml b/libsession/src/main/res/values/strings.xml index 70b424bd22..ee0510385c 100644 --- a/libsession/src/main/res/values/strings.xml +++ b/libsession/src/main/res/values/strings.xml @@ -487,6 +487,16 @@ 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. You have left the group. You updated the group. + You created a new group. + %1$s added you to the group. + You renamed the group to %1$s + %1$s renamed the group to: %2$s + You added %1$s to the group. + %1$s added %2$s to the group. + You removed %1$s from the group. + %1$s removed %2$s from the group. + You were removed from the group. + You You called Contact called Missed call @@ -499,6 +509,8 @@ %1$s disabled disappearing messages. You set the disappearing message timer to %1$s %1$s set the disappearing message timer to %2$s + %1$s took a screenshot. + Media saved by %1$s. Your safety number with %s has changed. You marked your safety number with %s verified You marked your safety number with %s verified from another device diff --git a/libsignal/src/main/java/org/session/libsignal/service/api/messages/SignalServiceGroup.java b/libsignal/src/main/java/org/session/libsignal/service/api/messages/SignalServiceGroup.java index b503531097..5115844a12 100644 --- a/libsignal/src/main/java/org/session/libsignal/service/api/messages/SignalServiceGroup.java +++ b/libsignal/src/main/java/org/session/libsignal/service/api/messages/SignalServiceGroup.java @@ -37,7 +37,11 @@ public class SignalServiceGroup { UPDATE, DELIVER, QUIT, - REQUEST_INFO + REQUEST_INFO, + CREATION, + NAME_CHANGE, + MEMBER_ADDED, + MEMBER_REMOVED } private final byte[] groupId;