From 694ca79958634038c177f6abb30aae8f2a29c1e5 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 17 Jan 2023 16:30:05 +1100 Subject: [PATCH] Added the unread mention indicator to the conversation list Fixed the unread indicator colours to match correct theming designs Fixed a bug where the unread count could be incorrect when receiving UnsendRequests within the same poll Added a couple missing theme colours --- .../attachments/DatabaseAttachmentProvider.kt | 12 ++- .../securesms/database/MessagingDatabase.java | 2 +- .../securesms/database/MmsDatabase.kt | 19 ++-- .../securesms/database/MmsSmsColumns.java | 2 + .../securesms/database/MmsSmsDatabase.java | 14 ++- .../securesms/database/SmsDatabase.java | 18 +++- .../securesms/database/Storage.kt | 8 +- .../securesms/database/ThreadDatabase.java | 36 +++++-- .../database/helpers/SQLCipherOpenHelper.java | 12 ++- .../database/model/MediaMmsMessageRecord.java | 4 +- .../database/model/MessageRecord.java | 8 +- .../database/model/MmsMessageRecord.java | 4 +- .../model/NotificationMmsMessageRecord.java | 4 +- .../database/model/SmsMessageRecord.java | 4 +- .../database/model/ThreadRecord.java | 28 +++-- .../securesms/home/ConversationView.kt | 17 +-- .../securesms/home/HomeAdapter.kt | 19 ++-- .../service/ExpiringMessageManager.java | 1 + .../securesms/util/MockDataGenerator.kt | 9 +- app/src/main/res/layout/view_conversation.xml | 36 ++++++- app/src/main/res/values/colors.xml | 27 ++--- app/src/main/res/values/themes.xml | 100 +++++++++--------- .../database/MessageDataProvider.kt | 4 +- .../libsession/database/StorageProtocol.kt | 2 +- .../messaging/jobs/BatchMessageReceiveJob.kt | 60 +++++++---- .../messages/signal/IncomingMediaMessage.java | 10 +- .../messages/signal/IncomingTextMessage.java | 23 ++-- .../messages/signal/OutgoingMediaMessage.java | 4 +- .../messages/visible/VisibleMessage.kt | 1 + .../ReceivedMessageHandler.kt | 30 ++++-- 30 files changed, 337 insertions(+), 181 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt index fa0fce7bd3..ef4503b0cb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt @@ -74,10 +74,10 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper) attachmentDatabase.setTransferState(messageID, attachmentId, attachmentState.value) } - override fun getMessageForQuote(timestamp: Long, author: Address): Pair? { + override fun getMessageForQuote(timestamp: Long, author: Address): Triple? { val messagingDatabase = DatabaseComponent.get(context).mmsSmsDatabase() val message = messagingDatabase.getMessageFor(timestamp, author) - return if (message != null) Pair(message.id, message.isMms) else null + return if (message != null) Triple(message.id, message.isMms, message.body) else null } override fun getAttachmentsAndLinkPreviewFor(mmsId: Long): List { @@ -184,16 +184,18 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper) DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHash(messageID) } - override fun updateMessageAsDeleted(timestamp: Long, author: String) { + override fun updateMessageAsDeleted(timestamp: Long, author: String): Long? { val database = DatabaseComponent.get(context).mmsSmsDatabase() val address = Address.fromSerialized(author) - val message = database.getMessageFor(timestamp, address) ?: return + val message = database.getMessageFor(timestamp, address) ?: return null val messagingDatabase: MessagingDatabase = if (message.isMms) DatabaseComponent.get(context).mmsDatabase() else DatabaseComponent.get(context).smsDatabase() - messagingDatabase.markAsDeleted(message.id, message.isRead) + messagingDatabase.markAsDeleted(message.id, message.isRead, message.hasMention) if (message.isOutgoing) { messagingDatabase.deleteMessage(message.id) } + + return message.id } override fun getServerHashForMessage(messageID: Long): String? { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessagingDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MessagingDatabase.java index bc0594df01..04db75a311 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessagingDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessagingDatabase.java @@ -39,7 +39,7 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn public abstract void markAsSent(long messageId, boolean secure); public abstract void markUnidentified(long messageId, boolean unidentified); - public abstract void markAsDeleted(long messageId, boolean read); + public abstract void markAsDeleted(long messageId, boolean read, boolean hasMention); public abstract boolean deleteMessage(long messageId); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt index d82c6bb278..30b84d7902 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt @@ -356,17 +356,19 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa db.update(TABLE_NAME, contentValues, ID_WHERE, arrayOf(messageId.toString())) } - override fun markAsDeleted(messageId: Long, read: Boolean) { + override fun markAsDeleted(messageId: Long, read: Boolean, hasMention: Boolean) { val database = databaseHelper.writableDatabase val contentValues = ContentValues() contentValues.put(READ, 1) contentValues.put(BODY, "") + contentValues.put(HAS_MENTION, 0) database.update(TABLE_NAME, contentValues, ID_WHERE, arrayOf(messageId.toString())) val attachmentDatabase = get(context).attachmentDatabase() queue(Runnable { attachmentDatabase.deleteAttachmentsForMessage(messageId) }) val threadId = getThreadIdForMessage(messageId) if (!read) { - get(context).threadDatabase().decrementUnread(threadId, 1) + val mentionChange = if (hasMention) { 1 } else { 0 } + get(context).threadDatabase().decrementUnread(threadId, 1, mentionChange) } updateMailboxBitmask( messageId, @@ -659,6 +661,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa contentValues.put(EXPIRES_IN, retrieved.expiresIn) contentValues.put(READ, if (retrieved.isExpirationUpdate) 1 else 0) contentValues.put(UNIDENTIFIED, retrieved.isUnidentified) + contentValues.put(HAS_MENTION, retrieved.hasMention()) contentValues.put(MESSAGE_REQUEST_RESPONSE, retrieved.isMessageRequestResponse) if (!contentValues.containsKey(DATE_SENT)) { contentValues.put(DATE_SENT, contentValues.getAsLong(DATE_RECEIVED)) @@ -690,7 +693,8 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa ) if (!MmsSmsColumns.Types.isExpirationTimerUpdate(mailbox)) { if (runIncrement) { - get(context).threadDatabase().incrementUnread(threadId, 1) + val mentionAmount = if (retrieved.hasMention()) { 1 } else { 0 } + get(context).threadDatabase().incrementUnread(threadId, 1, mentionAmount) } if (runThreadUpdate) { get(context).threadDatabase().update(threadId, true) @@ -1272,7 +1276,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa message.outgoingQuote!!.missing, SlideDeck(context, message.outgoingQuote!!.attachments!!) ) else null, - message.sharedContacts, message.linkPreviews, listOf(), false + message.sharedContacts, message.linkPreviews, listOf(), false, false ) } @@ -1316,6 +1320,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa ) var readReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(READ_RECEIPT_COUNT)) val subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(SUBSCRIPTION_ID)) + val hasMention = (cursor.getInt(cursor.getColumnIndexOrThrow(HAS_MENTION)) == 1) if (!isReadReceiptsEnabled(context)) { readReceiptCount = 0 } @@ -1333,7 +1338,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa dateSent, dateReceived, deliveryReceiptCount, threadId, contentLocationBytes, messageSize, expiry, status, transactionIdBytes, mailbox, slideDeck, - readReceiptCount + readReceiptCount, hasMention ) } @@ -1367,6 +1372,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa val expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(EXPIRES_IN)) val expireStarted = cursor.getLong(cursor.getColumnIndexOrThrow(EXPIRE_STARTED)) val unidentified = cursor.getInt(cursor.getColumnIndexOrThrow(UNIDENTIFIED)) == 1 + val hasMention = cursor.getInt(cursor.getColumnIndexOrThrow(HAS_MENTION)) == 1 if (!isReadReceiptsEnabled(context)) { readReceiptCount = 0 } @@ -1403,7 +1409,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa addressDeviceId, dateSent, dateReceived, deliveryReceiptCount, threadId, body, slideDeck!!, partCount, box, mismatches, networkFailures, subscriptionId, expiresIn, expireStarted, - readReceiptCount, quote, contacts, previews, reactions, unidentified + readReceiptCount, quote, contacts, previews, reactions, unidentified, hasMention ) } @@ -1596,5 +1602,6 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa const val CREATE_MESSAGE_REQUEST_RESPONSE_COMMAND = "ALTER TABLE $TABLE_NAME ADD COLUMN $MESSAGE_REQUEST_RESPONSE INTEGER DEFAULT 0;" const val CREATE_REACTIONS_UNREAD_COMMAND = "ALTER TABLE $TABLE_NAME ADD COLUMN $REACTIONS_UNREAD INTEGER DEFAULT 0;" const val CREATE_REACTIONS_LAST_SEEN_COMMAND = "ALTER TABLE $TABLE_NAME ADD COLUMN $REACTIONS_LAST_SEEN INTEGER DEFAULT 0;" + const val CREATE_HAS_MENTION_COMMAND = "ALTER TABLE $TABLE_NAME ADD COLUMN $HAS_MENTION INTEGER DEFAULT 0;" } } \ No newline at end of file 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 c4fe3d2437..f3110a5c79 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java @@ -24,6 +24,8 @@ public interface MmsSmsColumns { public static final String REACTIONS_UNREAD = "reactions_unread"; public static final String REACTIONS_LAST_SEEN = "reactions_last_seen"; + public static final String HAS_MENTION = "has_mention"; + public static class Types { protected static final long TOTAL_MASK = 0xFFFFFFFF; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java index 73534aeb23..447993643c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -75,7 +75,9 @@ public class MmsSmsDatabase extends Database { MmsDatabase.QUOTE_ATTACHMENT, MmsDatabase.SHARED_CONTACTS, MmsDatabase.LINK_PREVIEWS, - ReactionDatabase.REACTION_JSON_ALIAS}; + ReactionDatabase.REACTION_JSON_ALIAS, + MmsSmsColumns.HAS_MENTION + }; public MmsSmsDatabase(Context context, SQLCipherOpenHelper databaseHelper) { super(context, databaseHelper); @@ -337,7 +339,9 @@ public class MmsSmsDatabase extends Database { MmsDatabase.QUOTE_MISSING, MmsDatabase.QUOTE_ATTACHMENT, MmsDatabase.SHARED_CONTACTS, - MmsDatabase.LINK_PREVIEWS}; + MmsDatabase.LINK_PREVIEWS, + MmsSmsColumns.HAS_MENTION + }; String[] smsProjection = {SmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.NORMALIZED_DATE_SENT, SmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED, @@ -364,7 +368,9 @@ public class MmsSmsDatabase extends Database { MmsDatabase.QUOTE_MISSING, MmsDatabase.QUOTE_ATTACHMENT, MmsDatabase.SHARED_CONTACTS, - MmsDatabase.LINK_PREVIEWS}; + MmsDatabase.LINK_PREVIEWS, + MmsSmsColumns.HAS_MENTION + }; SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder(); SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder(); @@ -408,6 +414,7 @@ public class MmsSmsDatabase extends Database { mmsColumnsPresent.add(MmsDatabase.STATUS); mmsColumnsPresent.add(MmsDatabase.UNIDENTIFIED); mmsColumnsPresent.add(MmsDatabase.NETWORK_FAILURE); + mmsColumnsPresent.add(MmsSmsColumns.HAS_MENTION); mmsColumnsPresent.add(AttachmentDatabase.ROW_ID); mmsColumnsPresent.add(AttachmentDatabase.UNIQUE_ID); @@ -470,6 +477,7 @@ public class MmsSmsDatabase extends Database { smsColumnsPresent.add(SmsDatabase.DATE_RECEIVED); smsColumnsPresent.add(SmsDatabase.STATUS); smsColumnsPresent.add(SmsDatabase.UNIDENTIFIED); + smsColumnsPresent.add(MmsSmsColumns.HAS_MENTION); smsColumnsPresent.add(ReactionDatabase.ROW_ID); smsColumnsPresent.add(ReactionDatabase.MESSAGE_ID); smsColumnsPresent.add(ReactionDatabase.IS_MMS); 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 358518deac..4b51cf5340 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -121,6 +121,9 @@ public class SmsDatabase extends MessagingDatabase { public static String CREATE_REACTIONS_UNREAD_COMMAND = "ALTER TABLE "+ TABLE_NAME + " " + "ADD COLUMN " + REACTIONS_UNREAD + " INTEGER DEFAULT 0;"; + public static String CREATE_HAS_MENTION_COMMAND = "ALTER TABLE "+ TABLE_NAME + " " + + "ADD COLUMN " + HAS_MENTION + " INTEGER DEFAULT 0;"; + private static final EarlyReceiptCache earlyDeliveryReceiptCache = new EarlyReceiptCache(); private static final EarlyReceiptCache earlyReadReceiptCache = new EarlyReceiptCache(); @@ -206,14 +209,17 @@ public class SmsDatabase extends MessagingDatabase { } @Override - public void markAsDeleted(long messageId, boolean read) { + public void markAsDeleted(long messageId, boolean read, boolean hasMention) { SQLiteDatabase database = databaseHelper.getWritableDatabase(); ContentValues contentValues = new ContentValues(); contentValues.put(READ, 1); contentValues.put(BODY, ""); + contentValues.put(HAS_MENTION, 0); database.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(messageId)}); long threadId = getThreadIdForMessage(messageId); - if (!read) { DatabaseComponent.get(context).threadDatabase().decrementUnread(threadId, 1); } + if (!read) { + DatabaseComponent.get(context).threadDatabase().decrementUnread(threadId, 1, (hasMention ? 1 : 0)); + } updateTypeBitmask(messageId, Types.BASE_TYPE_MASK, Types.BASE_DELETED_TYPE); } @@ -444,6 +450,7 @@ public class SmsDatabase extends MessagingDatabase { values.put(SUBSCRIPTION_ID, message.getSubscriptionId()); values.put(EXPIRES_IN, message.getExpiresIn()); values.put(UNIDENTIFIED, message.isUnidentified()); + values.put(HAS_MENTION, message.hasMention()); if (!TextUtils.isEmpty(message.getPseudoSubject())) values.put(SUBJECT, message.getPseudoSubject()); @@ -462,7 +469,7 @@ public class SmsDatabase extends MessagingDatabase { long messageId = db.insert(TABLE_NAME, null, values); if (unread && runIncrement) { - DatabaseComponent.get(context).threadDatabase().incrementUnread(threadId, 1); + DatabaseComponent.get(context).threadDatabase().incrementUnread(threadId, 1, (message.hasMention() ? 1 : 0)); } if (runThreadUpdate) { @@ -736,7 +743,7 @@ public class SmsDatabase extends MessagingDatabase { 0, message.isSecureMessage() ? MmsSmsColumns.Types.getOutgoingEncryptedMessageType() : MmsSmsColumns.Types.getOutgoingSmsMessageType(), threadId, 0, new LinkedList(), message.getExpiresIn(), - System.currentTimeMillis(), 0, false, Collections.emptyList()); + System.currentTimeMillis(), 0, false, Collections.emptyList(), false); } } @@ -777,6 +784,7 @@ public class SmsDatabase extends MessagingDatabase { long expireStarted = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.EXPIRE_STARTED)); String body = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.BODY)); boolean unidentified = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.UNIDENTIFIED)) == 1; + boolean hasMention = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.HAS_MENTION)) == 1; if (!TextSecurePreferences.isReadReceiptsEnabled(context)) { readReceiptCount = 0; @@ -790,7 +798,7 @@ public class SmsDatabase extends MessagingDatabase { recipient, dateSent, dateReceived, deliveryReceiptCount, type, threadId, status, mismatches, - expiresIn, expireStarted, readReceiptCount, unidentified, reactions); + expiresIn, expireStarted, readReceiptCount, unidentified, reactions, hasMention); } private List getMismatches(String document) { 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 7daae9ef0c..a4f658eefa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -101,9 +101,9 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, threadDb.setRead(threadId, updateLastSeen) } - override fun incrementUnread(threadId: Long, amount: Int) { + override fun incrementUnread(threadId: Long, amount: Int, unreadMentionAmount: Int) { val threadDb = DatabaseComponent.get(context).threadDatabase() - threadDb.incrementUnread(threadId, amount) + threadDb.incrementUnread(threadId, amount, unreadMentionAmount) } override fun updateThread(threadId: Long, unarchive: Boolean) { @@ -458,7 +458,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, 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(fromSerialized(senderPublicKey), 1, sentTimestamp, "", Optional.of(group), 0, true) + val m = IncomingTextMessage(fromSerialized(senderPublicKey), 1, sentTimestamp, "", Optional.of(group), 0, true, false) val updateData = UpdateMessageData.buildGroupUpdate(type, name, members)?.toJSON() val infoMessage = IncomingGroupMessage(m, groupID, updateData, true) val smsDB = DatabaseComponent.get(context).smsDatabase() @@ -721,6 +721,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, false, false, false, + false, Optional.absent(), Optional.absent(), Optional.absent(), @@ -795,6 +796,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, false, false, true, + false, Optional.absent(), Optional.absent(), Optional.absent(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java index bf42b92ba5..26b20ab00a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -87,6 +87,7 @@ public class ThreadDatabase extends Database { private static final String SNIPPET_CHARSET = "snippet_cs"; public static final String READ = "read"; public static final String UNREAD_COUNT = "unread_count"; + public static final String UNREAD_MENTION_COUNT = "unread_mention_count"; public static final String TYPE = "type"; private static final String ERROR = "error"; public static final String SNIPPET_TYPE = "snippet_type"; @@ -117,7 +118,7 @@ public class ThreadDatabase extends Database { }; private static final String[] THREAD_PROJECTION = { - ID, DATE, MESSAGE_COUNT, ADDRESS, SNIPPET, SNIPPET_CHARSET, READ, UNREAD_COUNT, TYPE, ERROR, SNIPPET_TYPE, + ID, DATE, MESSAGE_COUNT, ADDRESS, SNIPPET, SNIPPET_CHARSET, READ, UNREAD_COUNT, UNREAD_MENTION_COUNT, TYPE, ERROR, SNIPPET_TYPE, SNIPPET_URI, ARCHIVED, STATUS, DELIVERY_RECEIPT_COUNT, EXPIRES_IN, LAST_SEEN, READ_RECEIPT_COUNT, IS_PINNED }; @@ -135,6 +136,11 @@ public class ThreadDatabase extends Database { "ADD COLUMN " + IS_PINNED + " INTEGER DEFAULT 0;"; } + public static String getUnreadMentionCountCommand() { + return "ALTER TABLE "+ TABLE_NAME + " " + + "ADD COLUMN " + UNREAD_MENTION_COUNT + " INTEGER DEFAULT 0;"; + } + public ThreadDatabase(Context context, SQLCipherOpenHelper databaseHelper) { super(context, databaseHelper); } @@ -293,6 +299,7 @@ public class ThreadDatabase extends Database { ContentValues contentValues = new ContentValues(1); contentValues.put(READ, 1); contentValues.put(UNREAD_COUNT, 0); + contentValues.put(UNREAD_MENTION_COUNT, 0); if (lastSeen) { contentValues.put(LAST_SEEN, System.currentTimeMillis()); @@ -312,20 +319,28 @@ public class ThreadDatabase extends Database { }}; } - public void incrementUnread(long threadId, int amount) { + public void incrementUnread(long threadId, int amount, int unreadMentionAmount) { SQLiteDatabase db = databaseHelper.getWritableDatabase(); db.execSQL("UPDATE " + TABLE_NAME + " SET " + READ + " = 0, " + - UNREAD_COUNT + " = " + UNREAD_COUNT + " + ? WHERE " + ID + " = ?", - new String[] {String.valueOf(amount), - String.valueOf(threadId)}); + UNREAD_COUNT + " = " + UNREAD_COUNT + " + ?, " + + UNREAD_MENTION_COUNT + " = " + UNREAD_MENTION_COUNT + " + ? WHERE " + ID + " = ?", + new String[] { + String.valueOf(amount), + String.valueOf(unreadMentionAmount), + String.valueOf(threadId) + }); } - public void decrementUnread(long threadId, int amount) { + public void decrementUnread(long threadId, int amount, int unreadMentionAmount) { SQLiteDatabase db = databaseHelper.getWritableDatabase(); db.execSQL("UPDATE " + TABLE_NAME + " SET " + READ + " = 0, " + - UNREAD_COUNT + " = " + UNREAD_COUNT + " - ? WHERE " + ID + " = ? AND " + UNREAD_COUNT + " > 0", - new String[] {String.valueOf(amount), - String.valueOf(threadId)}); + UNREAD_COUNT + " = " + UNREAD_COUNT + " - ?, " + + UNREAD_MENTION_COUNT + " = " + UNREAD_MENTION_COUNT + " - ? WHERE " + ID + " = ? AND " + UNREAD_COUNT + " > 0", + new String[] { + String.valueOf(amount), + String.valueOf(unreadMentionAmount), + String.valueOf(threadId) + }); } public void setDistributionType(long threadId, int distributionType) { @@ -911,6 +926,7 @@ public class ThreadDatabase extends Database { long date = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.DATE)); long count = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.MESSAGE_COUNT)); int unreadCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.UNREAD_COUNT)); + int unreadMentionCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.UNREAD_MENTION_COUNT)); long type = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET_TYPE)); boolean archived = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.ARCHIVED)) != 0; int status = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.STATUS)); @@ -926,7 +942,7 @@ public class ThreadDatabase extends Database { } return new ThreadRecord(body, snippetUri, recipient, date, count, - unreadCount, threadId, deliveryReceiptCount, status, type, + unreadCount, unreadMentionCount, threadId, deliveryReceiptCount, status, type, distributionType, archived, expiresIn, lastSeen, readReceiptCount, pinned); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index a6ac906980..4a48aa2446 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -85,9 +85,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { private static final int lokiV37 = 58; private static final int lokiV38 = 59; private static final int lokiV39 = 60; + private static final int lokiV40 = 61; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes - private static final int DATABASE_VERSION = lokiV39; + private static final int DATABASE_VERSION = lokiV40; private static final int MIN_DATABASE_VERSION = lokiV7; private static final String CIPHER3_DATABASE_NAME = "signal.db"; public static final String DATABASE_NAME = "signal_v4.db"; @@ -306,6 +307,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { db.execSQL(LokiAPIDatabase.RESET_SEQ_NO); // probably not needed but consistent with all migrations db.execSQL(EmojiSearchDatabase.CREATE_EMOJI_SEARCH_TABLE_COMMAND); db.execSQL(ReactionDatabase.CREATE_REACTION_TABLE_COMMAND); + db.execSQL(ThreadDatabase.getUnreadMentionCountCommand()); + db.execSQL(SmsDatabase.CREATE_HAS_MENTION_COMMAND); + db.execSQL(MmsDatabase.CREATE_HAS_MENTION_COMMAND); executeStatements(db, SmsDatabase.CREATE_INDEXS); executeStatements(db, MmsDatabase.CREATE_INDEXS); @@ -543,6 +547,12 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { executeStatements(db, ReactionDatabase.CREATE_INDEXS); } + if (oldVersion < lokiV40) { + db.execSQL(ThreadDatabase.getUnreadMentionCountCommand()); + db.execSQL(SmsDatabase.CREATE_HAS_MENTION_COMMAND); + db.execSQL(MmsDatabase.CREATE_HAS_MENTION_COMMAND); + } + db.setTransactionSuccessful(); } finally { db.endTransaction(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java index 570cb48bce..1b566169d7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java @@ -57,12 +57,12 @@ public class MediaMmsMessageRecord extends MmsMessageRecord { long expiresIn, long expireStarted, int readReceiptCount, @Nullable Quote quote, @NonNull List contacts, @NonNull List linkPreviews, - @NonNull List reactions, boolean unidentified) + @NonNull List reactions, boolean unidentified, boolean hasMention) { super(id, body, conversationRecipient, individualRecipient, dateSent, dateReceived, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox, mismatches, failures, expiresIn, expireStarted, slideDeck, readReceiptCount, quote, contacts, - linkPreviews, unidentified, reactions); + linkPreviews, unidentified, reactions, hasMention); this.partCount = partCount; } 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 1d78314dea..ba01ffd9c5 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 @@ -51,7 +51,8 @@ public abstract class MessageRecord extends DisplayRecord { private final long expireStarted; private final boolean unidentified; public final long id; - private final List reactions; + private final List reactions; + private final boolean hasMention; public abstract boolean isMms(); public abstract boolean isMmsNotification(); @@ -63,7 +64,7 @@ public abstract class MessageRecord extends DisplayRecord { List mismatches, List networkFailures, long expiresIn, long expireStarted, - int readReceiptCount, boolean unidentified, List reactions) + int readReceiptCount, boolean unidentified, List reactions, boolean hasMention) { super(body, conversationRecipient, dateSent, dateReceived, threadId, deliveryStatus, deliveryReceiptCount, type, readReceiptCount); @@ -75,6 +76,7 @@ public abstract class MessageRecord extends DisplayRecord { this.expireStarted = expireStarted; this.unidentified = unidentified; this.reactions = reactions; + this.hasMention = hasMention; } public long getId() { @@ -97,6 +99,8 @@ public abstract class MessageRecord extends DisplayRecord { } public long getExpireStarted() { return expireStarted; } + public boolean getHasMention() { return hasMention; } + public boolean isMediaPending() { return false; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java index b186e668eb..9f34f3fa0e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java @@ -27,9 +27,9 @@ public abstract class MmsMessageRecord extends MessageRecord { List networkFailures, long expiresIn, long expireStarted, @NonNull SlideDeck slideDeck, int readReceiptCount, @Nullable Quote quote, @NonNull List contacts, - @NonNull List linkPreviews, boolean unidentified, List reactions) + @NonNull List linkPreviews, boolean unidentified, List reactions, boolean hasMention) { - super(id, body, conversationRecipient, individualRecipient, dateSent, dateReceived, threadId, deliveryStatus, deliveryReceiptCount, type, mismatches, networkFailures, expiresIn, expireStarted, readReceiptCount, unidentified, reactions); + super(id, body, conversationRecipient, individualRecipient, dateSent, dateReceived, threadId, deliveryStatus, deliveryReceiptCount, type, mismatches, networkFailures, expiresIn, expireStarted, readReceiptCount, unidentified, reactions, hasMention); this.slideDeck = slideDeck; this.quote = quote; this.contacts.addAll(contacts); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java index c1c87800d2..9fb4047879 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java @@ -50,12 +50,12 @@ public class NotificationMmsMessageRecord extends MmsMessageRecord { long dateSent, long dateReceived, int deliveryReceiptCount, long threadId, byte[] contentLocation, long messageSize, long expiry, int status, byte[] transactionId, long mailbox, - SlideDeck slideDeck, int readReceiptCount) + SlideDeck slideDeck, int readReceiptCount, boolean hasMention) { super(id, "", conversationRecipient, individualRecipient, dateSent, dateReceived, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox, emptyList(), emptyList(), - 0, 0, slideDeck, readReceiptCount, null, emptyList(), emptyList(), false, emptyList()); + 0, 0, slideDeck, readReceiptCount, null, emptyList(), emptyList(), false, emptyList(), hasMention); this.contentLocation = contentLocation; this.messageSize = messageSize; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java index c1d50def2f..83ee921a2a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java @@ -43,12 +43,12 @@ public class SmsMessageRecord extends MessageRecord { long type, long threadId, int status, List mismatches, long expiresIn, long expireStarted, - int readReceiptCount, boolean unidentified, List reactions) + int readReceiptCount, boolean unidentified, List reactions, boolean hasMention) { super(id, body, recipient, individualRecipient, dateSent, dateReceived, threadId, status, deliveryReceiptCount, type, mismatches, new LinkedList<>(), - expiresIn, expireStarted, readReceiptCount, unidentified, reactions); + expiresIn, expireStarted, readReceiptCount, unidentified, reactions, hasMention); } public long getType() { 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 1e5a2fef0e..dfc4c1bc84 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 @@ -45,6 +45,7 @@ public class ThreadRecord extends DisplayRecord { private @Nullable final Uri snippetUri; private final long count; private final int unreadCount; + private final int unreadMentionCount; private final int distributionType; private final boolean archived; private final long expiresIn; @@ -53,19 +54,20 @@ public class ThreadRecord extends DisplayRecord { public ThreadRecord(@NonNull String body, @Nullable Uri snippetUri, @NonNull Recipient recipient, long date, long count, int unreadCount, - long threadId, int deliveryReceiptCount, int status, long snippetType, - int distributionType, boolean archived, long expiresIn, long lastSeen, - int readReceiptCount, boolean pinned) + int unreadMentionCount, long threadId, int deliveryReceiptCount, int status, + long snippetType, int distributionType, boolean archived, long expiresIn, + long lastSeen, int readReceiptCount, boolean pinned) { super(body, recipient, date, date, threadId, status, deliveryReceiptCount, snippetType, readReceiptCount); - this.snippetUri = snippetUri; - this.count = count; - this.unreadCount = unreadCount; - this.distributionType = distributionType; - this.archived = archived; - this.expiresIn = expiresIn; - this.lastSeen = lastSeen; - this.pinned = pinned; + this.snippetUri = snippetUri; + this.count = count; + this.unreadCount = unreadCount; + this.unreadMentionCount = unreadMentionCount; + this.distributionType = distributionType; + this.archived = archived; + this.expiresIn = expiresIn; + this.lastSeen = lastSeen; + this.pinned = pinned; } public @Nullable Uri getSnippetUri() { @@ -147,6 +149,10 @@ public class ThreadRecord extends DisplayRecord { return unreadCount; } + public int getUnreadMentionCount() { + return unreadMentionCount; + } + public long getDate() { return getDateReceived(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt index 7a0c865a42..c6a6e1f7f5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt @@ -25,17 +25,17 @@ import org.thoughtcrime.securesms.util.getAccentColor import java.util.Locale class ConversationView : LinearLayout { - private lateinit var binding: ViewConversationBinding + private val binding: ViewConversationBinding by lazy { ViewConversationBinding.bind(this) } private val screenWidth = Resources.getSystem().displayMetrics.widthPixels var thread: ThreadRecord? = null // region Lifecycle - constructor(context: Context) : super(context) { initialize() } - constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() } - constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() } + constructor(context: Context) : super(context) + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) - private fun initialize() { - binding = ViewConversationBinding.inflate(LayoutInflater.from(context), this, true) + override fun onFinishInflate() { + super.onFinishInflate() layoutParams = RecyclerView.LayoutParams(screenWidth, RecyclerView.LayoutParams.WRAP_CONTENT) } // endregion @@ -53,7 +53,7 @@ class ConversationView : LinearLayout { } else { binding.conversationViewDisplayNameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0) } - background = if (thread.unreadCount > 0) { + binding.root.background = if (thread.unreadCount > 0) { ContextCompat.getDrawable(context, R.drawable.conversation_unread_background) } else { ContextCompat.getDrawable(context, R.drawable.conversation_view_background) @@ -79,8 +79,9 @@ class ConversationView : LinearLayout { binding.unreadCountTextView.text = formattedUnreadCount val textSize = if (unreadCount < 1000) 12.0f else 10.0f binding.unreadCountTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize) - binding.unreadCountIndicator.background.setTint(context.getAccentColor()) binding.unreadCountIndicator.isVisible = (unreadCount != 0 && !thread.isRead) + binding.unreadMentionTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize) + binding.unreadMentionIndicator.isVisible = (thread.unreadMentionCount != 0 && thread.recipient.address.isGroup) val senderDisplayName = getUserDisplayName(thread.recipient) ?: thread.recipient.address.toString() binding.conversationViewDisplayNameTextView.text = senderDisplayName diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt index 0effc43fb2..4273794f5e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt @@ -1,12 +1,14 @@ package org.thoughtcrime.securesms.home import android.content.Context +import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListUpdateCallback import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.NO_ID +import network.loki.messenger.R import org.thoughtcrime.securesms.database.model.ThreadRecord import org.thoughtcrime.securesms.mms.GlideRequests @@ -76,19 +78,20 @@ class HomeAdapter( HeaderFooterViewHolder(header!!) } ITEM -> { - val view = ConversationView(context) - view.setOnClickListener { view.thread?.let { listener.onConversationClick(it) } } - view.setOnLongClickListener { - view.thread?.let { listener.onLongConversationClick(it) } + val conversationView = LayoutInflater.from(parent.context).inflate(R.layout.view_conversation, parent, false) as ConversationView + val viewHolder = ConversationViewHolder(conversationView) + viewHolder.view.setOnClickListener { viewHolder.view.thread?.let { listener.onConversationClick(it) } } + viewHolder.view.setOnLongClickListener { + viewHolder.view.thread?.let { listener.onLongConversationClick(it) } true } - ViewHolder(view) + viewHolder } else -> throw Exception("viewType $viewType isn't valid") } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - if (holder is ViewHolder) { + if (holder is ConversationViewHolder) { val offset = if (hasHeaderView()) position - 1 else position val thread = data[offset] val isTyping = typingThreadIDs.contains(thread.threadId) @@ -97,7 +100,7 @@ class HomeAdapter( } override fun onViewRecycled(holder: RecyclerView.ViewHolder) { - if (holder is ViewHolder) { + if (holder is ConversationViewHolder) { holder.view.recycle() } else { super.onViewRecycled(holder) @@ -110,7 +113,7 @@ class HomeAdapter( override fun getItemCount(): Int = data.size + if (hasHeaderView()) 1 else 0 - class ViewHolder(val view: ConversationView) : RecyclerView.ViewHolder(view) + class ConversationViewHolder(val view: ConversationView) : RecyclerView.ViewHolder(view) class HeaderFooterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) 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 b6d2e2bc57..f42b55b5fe 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.java @@ -111,6 +111,7 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM duration * 1000L, true, false, false, + false, Optional.absent(), groupInfo, Optional.absent(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/MockDataGenerator.kt b/app/src/main/java/org/thoughtcrime/securesms/util/MockDataGenerator.kt index 4d76e6aad7..f70ea7bd0f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/MockDataGenerator.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/MockDataGenerator.kt @@ -135,7 +135,8 @@ object MockDataGenerator { Optional.absent(), 0, false, - -1 + -1, + false ), (timestampNow - (index * 5000)), false, @@ -264,7 +265,8 @@ object MockDataGenerator { Optional.absent(), 0, false, - -1 + -1, + false ), (timestampNow - (index * 5000)), false, @@ -389,7 +391,8 @@ object MockDataGenerator { Optional.absent(), 0, false, - -1 + -1, + false ), (timestampNow - (index * 5000)), false, diff --git a/app/src/main/res/layout/view_conversation.xml b/app/src/main/res/layout/view_conversation.xml index 04833b6a96..08c3693c13 100644 --- a/app/src/main/res/layout/view_conversation.xml +++ b/app/src/main/res/layout/view_conversation.xml @@ -1,5 +1,5 @@ - + + + + + + - + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 3fd16ffc67..d3b9d9b255 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -141,22 +141,25 @@ #57C9FA - #111111 + #000000 #1A1C28 #252735 #2B2D40 #3D4A5D #A6A9CE - #FFFFFF - - #19345D - #6A6E90 - #5CAACC - #B3EDF2 - #E7F3F4 - #ECFAFB - #FCFFFF - - #EA5545 + #5CAACC + #FFFFFF + + #000000 + #19345D + #6A6E90 + #5CAACC + #B3EDF2 + #E7F3F4 + #ECFAFB + #FCFFFF + + #FF3A3A + #E12D19 diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 4c7dfd1db4..a2c3842b51 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -277,7 +277,7 @@ ?android:textColorPrimary diff --git a/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt b/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt index eb40df6e09..81ce45b34f 100644 --- a/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt +++ b/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt @@ -21,7 +21,7 @@ interface MessageDataProvider { */ fun getMessageID(serverId: Long, threadId: Long): Pair? fun deleteMessage(messageID: Long, isSms: Boolean) - fun updateMessageAsDeleted(timestamp: Long, author: String) + fun updateMessageAsDeleted(timestamp: Long, author: String): Long? fun getServerHashForMessage(messageID: Long): String? fun getDatabaseAttachment(attachmentId: Long): DatabaseAttachment? fun getAttachmentStream(attachmentId: Long): SessionServiceAttachmentStream? @@ -36,7 +36,7 @@ interface MessageDataProvider { fun isOutgoingMessage(timestamp: Long): Boolean fun handleSuccessfulAttachmentUpload(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: UploadResult) fun handleFailedAttachmentUpload(attachmentId: Long) - fun getMessageForQuote(timestamp: Long, author: Address): Pair? + fun getMessageForQuote(timestamp: Long, author: Address): Triple? fun getAttachmentsAndLinkPreviewFor(mmsId: Long): List fun getMessageBodyFor(timestamp: Long, author: String): String fun getAttachmentIDsFor(messageID: Long): List diff --git a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt index edca7cd15e..34a7b81cb5 100644 --- a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt @@ -174,7 +174,7 @@ interface StorageProtocol { */ fun persist(message: VisibleMessage, quotes: QuoteModel?, linkPreview: List, groupPublicKey: String?, openGroupID: String?, attachments: List, runIncrement: Boolean, runThreadUpdate: Boolean): Long? fun markConversationAsRead(threadId: Long, updateLastSeen: Boolean) - fun incrementUnread(threadId: Long, amount: Int) + fun incrementUnread(threadId: Long, amount: Int, unreadMentionAmount: Int) fun updateThread(threadId: Long, unarchive: Boolean) fun insertDataExtractionNotificationMessage(senderPublicKey: String, message: DataExtractionNotificationInfoMessage, sentTimestamp: Long) fun insertMessageRequestResponse(response: MessageRequestResponse) diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt index 07c104cfda..de512c5569 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt @@ -11,13 +11,11 @@ import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate +import org.session.libsession.messaging.messages.control.UnsendRequest import org.session.libsession.messaging.messages.visible.ParsedMessage import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.open_groups.OpenGroupApi -import org.session.libsession.messaging.sending_receiving.MessageReceiver -import org.session.libsession.messaging.sending_receiving.handle -import org.session.libsession.messaging.sending_receiving.handleOpenGroupReactions -import org.session.libsession.messaging.sending_receiving.handleVisibleMessage +import org.session.libsession.messaging.sending_receiving.* import org.session.libsession.messaging.utilities.Data import org.session.libsession.messaging.utilities.SessionId import org.session.libsession.messaging.utilities.SodiumUtilities @@ -108,25 +106,42 @@ class BatchMessageReceiveJob( runBlocking(Dispatchers.IO) { val deferredThreadMap = threadMap.entries.map { (threadId, messages) -> async { - val messageIds = mutableListOf>() + // The LinkedHashMap should preserve insertion order + val messageIds = linkedMapOf>() + messages.forEach { (parameters, message, proto) -> try { - if (message is VisibleMessage) { - val messageId = MessageReceiver.handleVisibleMessage(message, proto, openGroupID, - runIncrement = false, - runThreadUpdate = false, - runProfileUpdate = true - ) - if (messageId != null && message.reaction == null) { - val isUserBlindedSender = message.sender == serverPublicKey?.let { SodiumUtilities.blindedKeyPair(it, MessagingModuleConfiguration.shared.getUserED25519KeyPair()!!) }?.let { SessionId( - IdPrefix.BLINDED, it.publicKey.asBytes).hexString } - messageIds += messageId to (message.sender == localUserPublicKey || isUserBlindedSender) + when (message) { + is VisibleMessage -> { + val messageId = MessageReceiver.handleVisibleMessage(message, proto, openGroupID, + runIncrement = false, + runThreadUpdate = false, + runProfileUpdate = true + ) + + if (messageId != null && message.reaction == null) { + val isUserBlindedSender = message.sender == serverPublicKey?.let { SodiumUtilities.blindedKeyPair(it, MessagingModuleConfiguration.shared.getUserED25519KeyPair()!!) }?.let { SessionId( + IdPrefix.BLINDED, it.publicKey.asBytes).hexString } + messageIds[messageId] = Pair( + (message.sender == localUserPublicKey || isUserBlindedSender), + message.hasMention + ) + } + parameters.openGroupMessageServerID?.let { + MessageReceiver.handleOpenGroupReactions(threadId, it, parameters.reactions) + } } - parameters.openGroupMessageServerID?.let { - MessageReceiver.handleOpenGroupReactions(threadId, it, parameters.reactions) + + is UnsendRequest -> { + val deletedMessageId = MessageReceiver.handleUnsendRequest(message) + + // If we removed a message then ensure it isn't in the 'messageIds' + if (deletedMessageId != null) { + messageIds.remove(deletedMessageId) + } } - } else { - MessageReceiver.handle(message, proto, openGroupID) + + else -> MessageReceiver.handle(message, proto, openGroupID) } } catch (e: Exception) { Log.e(TAG, "Couldn't process message.", e) @@ -139,14 +154,15 @@ class BatchMessageReceiveJob( } } // increment unreads, notify, and update thread - val unreadFromMine = messageIds.indexOfLast { (_,fromMe) -> fromMe } - var trueUnreadCount = messageIds.filter { (_,fromMe) -> !fromMe }.size + val unreadFromMine = messageIds.map { it.value.first }.indexOfLast { it } + var trueUnreadCount = messageIds.filter { !it.value.first }.size + val trueUnreadMentionCount = messageIds.filter { !it.value.first && it.value.second }.size if (unreadFromMine >= 0) { trueUnreadCount -= (unreadFromMine + 1) storage.markConversationAsRead(threadId, false) } if (trueUnreadCount > 0) { - storage.incrementUnread(threadId, trueUnreadCount) + storage.incrementUnread(threadId, trueUnreadCount, trueUnreadMentionCount) } storage.updateThread(threadId, true) SSKEnvironment.shared.notificationManager.updateNotification(context, threadId) 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 e5160b7543..ab24234e81 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 @@ -29,6 +29,7 @@ public class IncomingMediaMessage { private final boolean expirationUpdate; private final boolean unidentified; private final boolean messageRequestResponse; + private final boolean hasMention; private final DataExtractionNotificationInfoMessage dataExtractionNotification; private final QuoteModel quote; @@ -44,6 +45,7 @@ public class IncomingMediaMessage { boolean expirationUpdate, boolean unidentified, boolean messageRequestResponse, + boolean hasMention, Optional body, Optional group, Optional> attachments, @@ -63,6 +65,7 @@ public class IncomingMediaMessage { this.quote = quote.orNull(); this.unidentified = unidentified; this.messageRequestResponse = messageRequestResponse; + this.hasMention = hasMention; if (group.isPresent()) this.groupId = Address.fromSerialized(GroupUtil.INSTANCE.getEncodedId(group.get())); else this.groupId = null; @@ -81,7 +84,8 @@ public class IncomingMediaMessage { Optional> linkPreviews) { return new IncomingMediaMessage(from, message.getSentTimestamp(), -1, expiresIn, false, - false, false, Optional.fromNullable(message.getText()), group, Optional.fromNullable(attachments), quote, Optional.absent(), linkPreviews, Optional.absent()); + false, false, message.getHasMention(), Optional.fromNullable(message.getText()), + group, Optional.fromNullable(attachments), quote, Optional.absent(), linkPreviews, Optional.absent()); } public int getSubscriptionId() { @@ -124,6 +128,10 @@ public class IncomingMediaMessage { return groupId != null; } + public boolean hasMention() { + return hasMention; + } + public boolean isScreenshotDataExtraction() { if (dataExtractionNotification == null) return false; else { diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/signal/IncomingTextMessage.java b/libsession/src/main/java/org/session/libsession/messaging/messages/signal/IncomingTextMessage.java index 93347f5276..ca8e89f1e0 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/signal/IncomingTextMessage.java +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/signal/IncomingTextMessage.java @@ -43,24 +43,25 @@ public class IncomingTextMessage implements Parcelable { private final long expiresInMillis; private final boolean unidentified; private final int callType; + private final boolean hasMention; private boolean isOpenGroupInvitation = false; public IncomingTextMessage(Address sender, int senderDeviceId, long sentTimestampMillis, String encodedBody, Optional group, - long expiresInMillis, boolean unidentified) { - this(sender, senderDeviceId, sentTimestampMillis, encodedBody, group, expiresInMillis, unidentified, -1); + long expiresInMillis, boolean unidentified, boolean hasMention) { + this(sender, senderDeviceId, sentTimestampMillis, encodedBody, group, expiresInMillis, unidentified, -1, hasMention); } public IncomingTextMessage(Address sender, int senderDeviceId, long sentTimestampMillis, String encodedBody, Optional group, - long expiresInMillis, boolean unidentified, int callType) { - this(sender, senderDeviceId, sentTimestampMillis, encodedBody, group, expiresInMillis, unidentified, callType, true); + long expiresInMillis, boolean unidentified, int callType, boolean hasMention) { + this(sender, senderDeviceId, sentTimestampMillis, encodedBody, group, expiresInMillis, unidentified, callType, hasMention, true); } public IncomingTextMessage(Address sender, int senderDeviceId, long sentTimestampMillis, String encodedBody, Optional group, - long expiresInMillis, boolean unidentified, int callType, boolean isPush) { + long expiresInMillis, boolean unidentified, int callType, boolean hasMention, boolean isPush) { this.message = encodedBody; this.sender = sender; this.senderDeviceId = senderDeviceId; @@ -74,6 +75,7 @@ public class IncomingTextMessage implements Parcelable { this.expiresInMillis = expiresInMillis; this.unidentified = unidentified; this.callType = callType; + this.hasMention = hasMention; if (group.isPresent()) { this.groupId = Address.fromSerialized(GroupUtil.getEncodedId(group.get())); @@ -98,6 +100,7 @@ public class IncomingTextMessage implements Parcelable { this.unidentified = in.readInt() == 1; this.isOpenGroupInvitation = in.readInt() == 1; this.callType = in.readInt(); + this.hasMention = in.readInt() == 1; } public IncomingTextMessage(IncomingTextMessage base, String newBody) { @@ -116,6 +119,7 @@ public class IncomingTextMessage implements Parcelable { this.unidentified = base.isUnidentified(); this.isOpenGroupInvitation = base.isOpenGroupInvitation(); this.callType = base.callType; + this.hasMention = base.hasMention; } public static IncomingTextMessage from(VisibleMessage message, @@ -123,7 +127,7 @@ public class IncomingTextMessage implements Parcelable { Optional group, long expiresInMillis) { - return new IncomingTextMessage(sender, 1, message.getSentTimestamp(), message.getText(), group, expiresInMillis, false); + return new IncomingTextMessage(sender, 1, message.getSentTimestamp(), message.getText(), group, expiresInMillis, false, message.getHasMention()); } public static IncomingTextMessage fromOpenGroupInvitation(OpenGroupInvitation openGroupInvitation, Address sender, Long sentTimestamp) @@ -133,7 +137,7 @@ public class IncomingTextMessage implements Parcelable { if (url == null || name == null) { return null; } // FIXME: Doing toJSON() to get the body here is weird String body = UpdateMessageData.Companion.buildOpenGroupInvitation(url, name).toJSON(); - IncomingTextMessage incomingTextMessage = new IncomingTextMessage(sender, 1, sentTimestamp, body, Optional.absent(), 0, false); + IncomingTextMessage incomingTextMessage = new IncomingTextMessage(sender, 1, sentTimestamp, body, Optional.absent(), 0, false, false); incomingTextMessage.isOpenGroupInvitation = true; return incomingTextMessage; } @@ -142,7 +146,7 @@ public class IncomingTextMessage implements Parcelable { Address sender, Optional group, long sentTimestamp) { - return new IncomingTextMessage(sender, 1, sentTimestamp, null, group, 0, false, callMessageType.ordinal(), false); + return new IncomingTextMessage(sender, 1, sentTimestamp, null, group, 0, false, callMessageType.ordinal(), false, false); } public int getSubscriptionId() { @@ -207,6 +211,8 @@ public class IncomingTextMessage implements Parcelable { public boolean isOpenGroupInvitation() { return isOpenGroupInvitation; } + public boolean hasMention() { return hasMention; } + public boolean isCallInfo() { int callMessageTypeLength = CallMessageType.values().length; return callType >= 0 && callType < callMessageTypeLength; @@ -240,5 +246,6 @@ public class IncomingTextMessage implements Parcelable { out.writeInt(unidentified ? 1 : 0); out.writeInt(isOpenGroupInvitation ? 1 : 0); out.writeInt(callType); + out.writeInt(hasMention ? 1 : 0); } } 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 a163b667d9..08cb2c4b02 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 @@ -85,8 +85,8 @@ public class OutgoingMediaMessage { previews = Collections.singletonList(linkPreview); } return new OutgoingMediaMessage(recipient, message.getText(), attachments, message.getSentTimestamp(), -1, - recipient.getExpireMessages() * 1000, DistributionTypes.DEFAULT, outgoingQuote, Collections.emptyList(), - previews, Collections.emptyList(), Collections.emptyList()); + recipient.getExpireMessages() * 1000, DistributionTypes.DEFAULT, outgoingQuote, + Collections.emptyList(), previews, Collections.emptyList(), Collections.emptyList()); } public Recipient getRecipient() { diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt index 2891400c9a..e66147da18 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt @@ -24,6 +24,7 @@ class VisibleMessage : Message() { var profile: Profile? = null var openGroupInvitation: OpenGroupInvitation? = null var reaction: Reaction? = null + var hasMention: Boolean = false override val isSelfSendValid: Boolean = true diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt index acab1f0977..dccb2ec888 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt @@ -173,22 +173,24 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) { storage.addContacts(message.contacts) } -fun MessageReceiver.handleUnsendRequest(message: UnsendRequest) { +fun MessageReceiver.handleUnsendRequest(message: UnsendRequest): Long? { val userPublicKey = MessagingModuleConfiguration.shared.storage.getUserPublicKey() - if (message.sender != message.author && (message.sender != userPublicKey && userPublicKey != null)) { return } + if (message.sender != message.author && (message.sender != userPublicKey && userPublicKey != null)) { return null } val context = MessagingModuleConfiguration.shared.context val storage = MessagingModuleConfiguration.shared.storage val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider - val timestamp = message.timestamp ?: return - val author = message.author ?: return - val messageIdToDelete = storage.getMessageIdInDatabase(timestamp, author) ?: return + val timestamp = message.timestamp ?: return null + val author = message.author ?: return null + val messageIdToDelete = storage.getMessageIdInDatabase(timestamp, author) ?: return null messageDataProvider.getServerHashForMessage(messageIdToDelete)?.let { serverHash -> SnodeAPI.deleteMessage(author, listOf(serverHash)) } - messageDataProvider.updateMessageAsDeleted(timestamp, author) + val deletedMessageId = messageDataProvider.updateMessageAsDeleted(timestamp, author) if (!messageDataProvider.isOutgoingMessage(messageIdToDelete)) { SSKEnvironment.shared.notificationManager.updateNotification(context) } + + return deletedMessageId } fun handleMessageRequestResponse(message: MessageRequestResponse) { @@ -248,6 +250,7 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, } // Parse quote if needed var quoteModel: QuoteModel? = null + var quoteMessageBody: String? = null if (message.quote != null && proto.dataMessage.hasQuote()) { val quote = proto.dataMessage.quote @@ -259,6 +262,7 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider val messageInfo = messageDataProvider.getMessageForQuote(quote.id, author) + quoteMessageBody = messageInfo?.third quoteModel = if (messageInfo != null) { val attachments = if (messageInfo.second) messageDataProvider.getAttachmentsAndLinkPreviewFor(messageInfo.first) else ArrayList() QuoteModel(quote.id, author,null,false, attachments) @@ -305,6 +309,20 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, storage.removeReaction(reaction.emoji!!, reaction.timestamp!!, reaction.publicKey!!, threadIsGroup) } } ?: run { + // A user is mentioned if their public key is in the body of a message or one of their messages + // was quoted + val messageText = message.text + message.hasMention = listOf(userPublicKey, userBlindedKey) + .filterNotNull() + .any { key -> + return@any ( + messageText != null && + messageText.contains("@$key") + ) || ( + (quoteModel?.author?.serialize() ?: "") == key + ) + } + // Persist the message message.threadID = threadID val messageID =