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
pull/1082/head
Morgan Pretty 1 year ago
parent cae15a200d
commit 694ca79958

@ -74,10 +74,10 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
attachmentDatabase.setTransferState(messageID, attachmentId, attachmentState.value) attachmentDatabase.setTransferState(messageID, attachmentId, attachmentState.value)
} }
override fun getMessageForQuote(timestamp: Long, author: Address): Pair<Long, Boolean>? { override fun getMessageForQuote(timestamp: Long, author: Address): Triple<Long, Boolean, String>? {
val messagingDatabase = DatabaseComponent.get(context).mmsSmsDatabase() val messagingDatabase = DatabaseComponent.get(context).mmsSmsDatabase()
val message = messagingDatabase.getMessageFor(timestamp, author) 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<Attachment> { override fun getAttachmentsAndLinkPreviewFor(mmsId: Long): List<Attachment> {
@ -184,16 +184,18 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHash(messageID) 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 database = DatabaseComponent.get(context).mmsSmsDatabase()
val address = Address.fromSerialized(author) 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() val messagingDatabase: MessagingDatabase = if (message.isMms) DatabaseComponent.get(context).mmsDatabase()
else DatabaseComponent.get(context).smsDatabase() else DatabaseComponent.get(context).smsDatabase()
messagingDatabase.markAsDeleted(message.id, message.isRead) messagingDatabase.markAsDeleted(message.id, message.isRead, message.hasMention)
if (message.isOutgoing) { if (message.isOutgoing) {
messagingDatabase.deleteMessage(message.id) messagingDatabase.deleteMessage(message.id)
} }
return message.id
} }
override fun getServerHashForMessage(messageID: Long): String? { override fun getServerHashForMessage(messageID: Long): String? {

@ -39,7 +39,7 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn
public abstract void markAsSent(long messageId, boolean secure); public abstract void markAsSent(long messageId, boolean secure);
public abstract void markUnidentified(long messageId, boolean unidentified); 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); public abstract boolean deleteMessage(long messageId);

@ -356,17 +356,19 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
db.update(TABLE_NAME, contentValues, ID_WHERE, arrayOf(messageId.toString())) 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 database = databaseHelper.writableDatabase
val contentValues = ContentValues() val contentValues = ContentValues()
contentValues.put(READ, 1) contentValues.put(READ, 1)
contentValues.put(BODY, "") contentValues.put(BODY, "")
contentValues.put(HAS_MENTION, 0)
database.update(TABLE_NAME, contentValues, ID_WHERE, arrayOf(messageId.toString())) database.update(TABLE_NAME, contentValues, ID_WHERE, arrayOf(messageId.toString()))
val attachmentDatabase = get(context).attachmentDatabase() val attachmentDatabase = get(context).attachmentDatabase()
queue(Runnable { attachmentDatabase.deleteAttachmentsForMessage(messageId) }) queue(Runnable { attachmentDatabase.deleteAttachmentsForMessage(messageId) })
val threadId = getThreadIdForMessage(messageId) val threadId = getThreadIdForMessage(messageId)
if (!read) { if (!read) {
get(context).threadDatabase().decrementUnread(threadId, 1) val mentionChange = if (hasMention) { 1 } else { 0 }
get(context).threadDatabase().decrementUnread(threadId, 1, mentionChange)
} }
updateMailboxBitmask( updateMailboxBitmask(
messageId, messageId,
@ -659,6 +661,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
contentValues.put(EXPIRES_IN, retrieved.expiresIn) contentValues.put(EXPIRES_IN, retrieved.expiresIn)
contentValues.put(READ, if (retrieved.isExpirationUpdate) 1 else 0) contentValues.put(READ, if (retrieved.isExpirationUpdate) 1 else 0)
contentValues.put(UNIDENTIFIED, retrieved.isUnidentified) contentValues.put(UNIDENTIFIED, retrieved.isUnidentified)
contentValues.put(HAS_MENTION, retrieved.hasMention())
contentValues.put(MESSAGE_REQUEST_RESPONSE, retrieved.isMessageRequestResponse) contentValues.put(MESSAGE_REQUEST_RESPONSE, retrieved.isMessageRequestResponse)
if (!contentValues.containsKey(DATE_SENT)) { if (!contentValues.containsKey(DATE_SENT)) {
contentValues.put(DATE_SENT, contentValues.getAsLong(DATE_RECEIVED)) 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 (!MmsSmsColumns.Types.isExpirationTimerUpdate(mailbox)) {
if (runIncrement) { 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) { if (runThreadUpdate) {
get(context).threadDatabase().update(threadId, true) get(context).threadDatabase().update(threadId, true)
@ -1272,7 +1276,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
message.outgoingQuote!!.missing, message.outgoingQuote!!.missing,
SlideDeck(context, message.outgoingQuote!!.attachments!!) SlideDeck(context, message.outgoingQuote!!.attachments!!)
) else null, ) 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)) var readReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(READ_RECEIPT_COUNT))
val subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(SUBSCRIPTION_ID)) val subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(SUBSCRIPTION_ID))
val hasMention = (cursor.getInt(cursor.getColumnIndexOrThrow(HAS_MENTION)) == 1)
if (!isReadReceiptsEnabled(context)) { if (!isReadReceiptsEnabled(context)) {
readReceiptCount = 0 readReceiptCount = 0
} }
@ -1333,7 +1338,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
dateSent, dateReceived, deliveryReceiptCount, threadId, dateSent, dateReceived, deliveryReceiptCount, threadId,
contentLocationBytes, messageSize, expiry, status, contentLocationBytes, messageSize, expiry, status,
transactionIdBytes, mailbox, slideDeck, 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 expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(EXPIRES_IN))
val expireStarted = cursor.getLong(cursor.getColumnIndexOrThrow(EXPIRE_STARTED)) val expireStarted = cursor.getLong(cursor.getColumnIndexOrThrow(EXPIRE_STARTED))
val unidentified = cursor.getInt(cursor.getColumnIndexOrThrow(UNIDENTIFIED)) == 1 val unidentified = cursor.getInt(cursor.getColumnIndexOrThrow(UNIDENTIFIED)) == 1
val hasMention = cursor.getInt(cursor.getColumnIndexOrThrow(HAS_MENTION)) == 1
if (!isReadReceiptsEnabled(context)) { if (!isReadReceiptsEnabled(context)) {
readReceiptCount = 0 readReceiptCount = 0
} }
@ -1403,7 +1409,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
addressDeviceId, dateSent, dateReceived, deliveryReceiptCount, addressDeviceId, dateSent, dateReceived, deliveryReceiptCount,
threadId, body, slideDeck!!, partCount, box, mismatches, threadId, body, slideDeck!!, partCount, box, mismatches,
networkFailures, subscriptionId, expiresIn, expireStarted, 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_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_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_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;"
} }
} }

@ -24,6 +24,8 @@ public interface MmsSmsColumns {
public static final String REACTIONS_UNREAD = "reactions_unread"; public static final String REACTIONS_UNREAD = "reactions_unread";
public static final String REACTIONS_LAST_SEEN = "reactions_last_seen"; public static final String REACTIONS_LAST_SEEN = "reactions_last_seen";
public static final String HAS_MENTION = "has_mention";
public static class Types { public static class Types {
protected static final long TOTAL_MASK = 0xFFFFFFFF; protected static final long TOTAL_MASK = 0xFFFFFFFF;

@ -75,7 +75,9 @@ public class MmsSmsDatabase extends Database {
MmsDatabase.QUOTE_ATTACHMENT, MmsDatabase.QUOTE_ATTACHMENT,
MmsDatabase.SHARED_CONTACTS, MmsDatabase.SHARED_CONTACTS,
MmsDatabase.LINK_PREVIEWS, MmsDatabase.LINK_PREVIEWS,
ReactionDatabase.REACTION_JSON_ALIAS}; ReactionDatabase.REACTION_JSON_ALIAS,
MmsSmsColumns.HAS_MENTION
};
public MmsSmsDatabase(Context context, SQLCipherOpenHelper databaseHelper) { public MmsSmsDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
super(context, databaseHelper); super(context, databaseHelper);
@ -337,7 +339,9 @@ public class MmsSmsDatabase extends Database {
MmsDatabase.QUOTE_MISSING, MmsDatabase.QUOTE_MISSING,
MmsDatabase.QUOTE_ATTACHMENT, MmsDatabase.QUOTE_ATTACHMENT,
MmsDatabase.SHARED_CONTACTS, MmsDatabase.SHARED_CONTACTS,
MmsDatabase.LINK_PREVIEWS}; MmsDatabase.LINK_PREVIEWS,
MmsSmsColumns.HAS_MENTION
};
String[] smsProjection = {SmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.NORMALIZED_DATE_SENT, String[] smsProjection = {SmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.NORMALIZED_DATE_SENT,
SmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED, SmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED,
@ -364,7 +368,9 @@ public class MmsSmsDatabase extends Database {
MmsDatabase.QUOTE_MISSING, MmsDatabase.QUOTE_MISSING,
MmsDatabase.QUOTE_ATTACHMENT, MmsDatabase.QUOTE_ATTACHMENT,
MmsDatabase.SHARED_CONTACTS, MmsDatabase.SHARED_CONTACTS,
MmsDatabase.LINK_PREVIEWS}; MmsDatabase.LINK_PREVIEWS,
MmsSmsColumns.HAS_MENTION
};
SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder(); SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder();
SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder(); SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder();
@ -408,6 +414,7 @@ public class MmsSmsDatabase extends Database {
mmsColumnsPresent.add(MmsDatabase.STATUS); mmsColumnsPresent.add(MmsDatabase.STATUS);
mmsColumnsPresent.add(MmsDatabase.UNIDENTIFIED); mmsColumnsPresent.add(MmsDatabase.UNIDENTIFIED);
mmsColumnsPresent.add(MmsDatabase.NETWORK_FAILURE); mmsColumnsPresent.add(MmsDatabase.NETWORK_FAILURE);
mmsColumnsPresent.add(MmsSmsColumns.HAS_MENTION);
mmsColumnsPresent.add(AttachmentDatabase.ROW_ID); mmsColumnsPresent.add(AttachmentDatabase.ROW_ID);
mmsColumnsPresent.add(AttachmentDatabase.UNIQUE_ID); mmsColumnsPresent.add(AttachmentDatabase.UNIQUE_ID);
@ -470,6 +477,7 @@ public class MmsSmsDatabase extends Database {
smsColumnsPresent.add(SmsDatabase.DATE_RECEIVED); smsColumnsPresent.add(SmsDatabase.DATE_RECEIVED);
smsColumnsPresent.add(SmsDatabase.STATUS); smsColumnsPresent.add(SmsDatabase.STATUS);
smsColumnsPresent.add(SmsDatabase.UNIDENTIFIED); smsColumnsPresent.add(SmsDatabase.UNIDENTIFIED);
smsColumnsPresent.add(MmsSmsColumns.HAS_MENTION);
smsColumnsPresent.add(ReactionDatabase.ROW_ID); smsColumnsPresent.add(ReactionDatabase.ROW_ID);
smsColumnsPresent.add(ReactionDatabase.MESSAGE_ID); smsColumnsPresent.add(ReactionDatabase.MESSAGE_ID);
smsColumnsPresent.add(ReactionDatabase.IS_MMS); smsColumnsPresent.add(ReactionDatabase.IS_MMS);

@ -121,6 +121,9 @@ public class SmsDatabase extends MessagingDatabase {
public static String CREATE_REACTIONS_UNREAD_COMMAND = "ALTER TABLE "+ TABLE_NAME + " " + public static String CREATE_REACTIONS_UNREAD_COMMAND = "ALTER TABLE "+ TABLE_NAME + " " +
"ADD COLUMN " + REACTIONS_UNREAD + " INTEGER DEFAULT 0;"; "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 earlyDeliveryReceiptCache = new EarlyReceiptCache();
private static final EarlyReceiptCache earlyReadReceiptCache = new EarlyReceiptCache(); private static final EarlyReceiptCache earlyReadReceiptCache = new EarlyReceiptCache();
@ -206,14 +209,17 @@ public class SmsDatabase extends MessagingDatabase {
} }
@Override @Override
public void markAsDeleted(long messageId, boolean read) { public void markAsDeleted(long messageId, boolean read, boolean hasMention) {
SQLiteDatabase database = databaseHelper.getWritableDatabase(); SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues contentValues = new ContentValues(); ContentValues contentValues = new ContentValues();
contentValues.put(READ, 1); contentValues.put(READ, 1);
contentValues.put(BODY, ""); contentValues.put(BODY, "");
contentValues.put(HAS_MENTION, 0);
database.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(messageId)}); database.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(messageId)});
long threadId = getThreadIdForMessage(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); 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(SUBSCRIPTION_ID, message.getSubscriptionId());
values.put(EXPIRES_IN, message.getExpiresIn()); values.put(EXPIRES_IN, message.getExpiresIn());
values.put(UNIDENTIFIED, message.isUnidentified()); values.put(UNIDENTIFIED, message.isUnidentified());
values.put(HAS_MENTION, message.hasMention());
if (!TextUtils.isEmpty(message.getPseudoSubject())) if (!TextUtils.isEmpty(message.getPseudoSubject()))
values.put(SUBJECT, message.getPseudoSubject()); values.put(SUBJECT, message.getPseudoSubject());
@ -462,7 +469,7 @@ public class SmsDatabase extends MessagingDatabase {
long messageId = db.insert(TABLE_NAME, null, values); long messageId = db.insert(TABLE_NAME, null, values);
if (unread && runIncrement) { if (unread && runIncrement) {
DatabaseComponent.get(context).threadDatabase().incrementUnread(threadId, 1); DatabaseComponent.get(context).threadDatabase().incrementUnread(threadId, 1, (message.hasMention() ? 1 : 0));
} }
if (runThreadUpdate) { if (runThreadUpdate) {
@ -736,7 +743,7 @@ public class SmsDatabase extends MessagingDatabase {
0, message.isSecureMessage() ? MmsSmsColumns.Types.getOutgoingEncryptedMessageType() : MmsSmsColumns.Types.getOutgoingSmsMessageType(), 0, message.isSecureMessage() ? MmsSmsColumns.Types.getOutgoingEncryptedMessageType() : MmsSmsColumns.Types.getOutgoingSmsMessageType(),
threadId, 0, new LinkedList<IdentityKeyMismatch>(), threadId, 0, new LinkedList<IdentityKeyMismatch>(),
message.getExpiresIn(), 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)); long expireStarted = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.EXPIRE_STARTED));
String body = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.BODY)); String body = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.BODY));
boolean unidentified = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.UNIDENTIFIED)) == 1; boolean unidentified = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.UNIDENTIFIED)) == 1;
boolean hasMention = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.HAS_MENTION)) == 1;
if (!TextSecurePreferences.isReadReceiptsEnabled(context)) { if (!TextSecurePreferences.isReadReceiptsEnabled(context)) {
readReceiptCount = 0; readReceiptCount = 0;
@ -790,7 +798,7 @@ public class SmsDatabase extends MessagingDatabase {
recipient, recipient,
dateSent, dateReceived, deliveryReceiptCount, type, dateSent, dateReceived, deliveryReceiptCount, type,
threadId, status, mismatches, threadId, status, mismatches,
expiresIn, expireStarted, readReceiptCount, unidentified, reactions); expiresIn, expireStarted, readReceiptCount, unidentified, reactions, hasMention);
} }
private List<IdentityKeyMismatch> getMismatches(String document) { private List<IdentityKeyMismatch> getMismatches(String document) {

@ -101,9 +101,9 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
threadDb.setRead(threadId, updateLastSeen) 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() val threadDb = DatabaseComponent.get(context).threadDatabase()
threadDb.incrementUnread(threadId, amount) threadDb.incrementUnread(threadId, amount, unreadMentionAmount)
} }
override fun updateThread(threadId: Long, unarchive: Boolean) { 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<String>, admins: Collection<String>, sentTimestamp: Long) { override fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type: SignalServiceGroup.Type, name: String, members: Collection<String>, admins: Collection<String>, sentTimestamp: Long) {
val group = SignalServiceGroup(type, GroupUtil.getDecodedGroupIDAsData(groupID), SignalServiceGroup.GroupType.SIGNAL, name, members.toList(), null, admins.toList()) 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 updateData = UpdateMessageData.buildGroupUpdate(type, name, members)?.toJSON()
val infoMessage = IncomingGroupMessage(m, groupID, updateData, true) val infoMessage = IncomingGroupMessage(m, groupID, updateData, true)
val smsDB = DatabaseComponent.get(context).smsDatabase() val smsDB = DatabaseComponent.get(context).smsDatabase()
@ -721,6 +721,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
false, false,
false, false,
false, false,
false,
Optional.absent(), Optional.absent(),
Optional.absent(), Optional.absent(),
Optional.absent(), Optional.absent(),
@ -795,6 +796,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
false, false,
false, false,
true, true,
false,
Optional.absent(), Optional.absent(),
Optional.absent(), Optional.absent(),
Optional.absent(), Optional.absent(),

@ -87,6 +87,7 @@ public class ThreadDatabase extends Database {
private static final String SNIPPET_CHARSET = "snippet_cs"; private static final String SNIPPET_CHARSET = "snippet_cs";
public static final String READ = "read"; public static final String READ = "read";
public static final String UNREAD_COUNT = "unread_count"; public static final String UNREAD_COUNT = "unread_count";
public static final String UNREAD_MENTION_COUNT = "unread_mention_count";
public static final String TYPE = "type"; public static final String TYPE = "type";
private static final String ERROR = "error"; private static final String ERROR = "error";
public static final String SNIPPET_TYPE = "snippet_type"; public static final String SNIPPET_TYPE = "snippet_type";
@ -117,7 +118,7 @@ public class ThreadDatabase extends Database {
}; };
private static final String[] THREAD_PROJECTION = { 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 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;"; "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) { public ThreadDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
super(context, databaseHelper); super(context, databaseHelper);
} }
@ -293,6 +299,7 @@ public class ThreadDatabase extends Database {
ContentValues contentValues = new ContentValues(1); ContentValues contentValues = new ContentValues(1);
contentValues.put(READ, 1); contentValues.put(READ, 1);
contentValues.put(UNREAD_COUNT, 0); contentValues.put(UNREAD_COUNT, 0);
contentValues.put(UNREAD_MENTION_COUNT, 0);
if (lastSeen) { if (lastSeen) {
contentValues.put(LAST_SEEN, System.currentTimeMillis()); 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(); SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.execSQL("UPDATE " + TABLE_NAME + " SET " + READ + " = 0, " + db.execSQL("UPDATE " + TABLE_NAME + " SET " + READ + " = 0, " +
UNREAD_COUNT + " = " + UNREAD_COUNT + " + ? WHERE " + ID + " = ?", UNREAD_COUNT + " = " + UNREAD_COUNT + " + ?, " +
new String[] {String.valueOf(amount), UNREAD_MENTION_COUNT + " = " + UNREAD_MENTION_COUNT + " + ? WHERE " + ID + " = ?",
String.valueOf(threadId)}); 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(); SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.execSQL("UPDATE " + TABLE_NAME + " SET " + READ + " = 0, " + db.execSQL("UPDATE " + TABLE_NAME + " SET " + READ + " = 0, " +
UNREAD_COUNT + " = " + UNREAD_COUNT + " - ? WHERE " + ID + " = ? AND " + UNREAD_COUNT + " > 0", UNREAD_COUNT + " = " + UNREAD_COUNT + " - ?, " +
new String[] {String.valueOf(amount), UNREAD_MENTION_COUNT + " = " + UNREAD_MENTION_COUNT + " - ? WHERE " + ID + " = ? AND " + UNREAD_COUNT + " > 0",
String.valueOf(threadId)}); new String[] {
String.valueOf(amount),
String.valueOf(unreadMentionAmount),
String.valueOf(threadId)
});
} }
public void setDistributionType(long threadId, int distributionType) { 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 date = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.DATE));
long count = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.MESSAGE_COUNT)); long count = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.MESSAGE_COUNT));
int unreadCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.UNREAD_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)); long type = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET_TYPE));
boolean archived = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.ARCHIVED)) != 0; boolean archived = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.ARCHIVED)) != 0;
int status = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.STATUS)); 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, 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); distributionType, archived, expiresIn, lastSeen, readReceiptCount, pinned);
} }

@ -85,9 +85,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int lokiV37 = 58; private static final int lokiV37 = 58;
private static final int lokiV38 = 59; private static final int lokiV38 = 59;
private static final int lokiV39 = 60; 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 // 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 int MIN_DATABASE_VERSION = lokiV7;
private static final String CIPHER3_DATABASE_NAME = "signal.db"; private static final String CIPHER3_DATABASE_NAME = "signal.db";
public static final String DATABASE_NAME = "signal_v4.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(LokiAPIDatabase.RESET_SEQ_NO); // probably not needed but consistent with all migrations
db.execSQL(EmojiSearchDatabase.CREATE_EMOJI_SEARCH_TABLE_COMMAND); db.execSQL(EmojiSearchDatabase.CREATE_EMOJI_SEARCH_TABLE_COMMAND);
db.execSQL(ReactionDatabase.CREATE_REACTION_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, SmsDatabase.CREATE_INDEXS);
executeStatements(db, MmsDatabase.CREATE_INDEXS); executeStatements(db, MmsDatabase.CREATE_INDEXS);
@ -543,6 +547,12 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
executeStatements(db, ReactionDatabase.CREATE_INDEXS); 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(); db.setTransactionSuccessful();
} finally { } finally {
db.endTransaction(); db.endTransaction();

@ -57,12 +57,12 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
long expiresIn, long expireStarted, int readReceiptCount, long expiresIn, long expireStarted, int readReceiptCount,
@Nullable Quote quote, @NonNull List<Contact> contacts, @Nullable Quote quote, @NonNull List<Contact> contacts,
@NonNull List<LinkPreview> linkPreviews, @NonNull List<LinkPreview> linkPreviews,
@NonNull List<ReactionRecord> reactions, boolean unidentified) @NonNull List<ReactionRecord> reactions, boolean unidentified, boolean hasMention)
{ {
super(id, body, conversationRecipient, individualRecipient, dateSent, super(id, body, conversationRecipient, individualRecipient, dateSent,
dateReceived, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox, mismatches, failures, dateReceived, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox, mismatches, failures,
expiresIn, expireStarted, slideDeck, readReceiptCount, quote, contacts, expiresIn, expireStarted, slideDeck, readReceiptCount, quote, contacts,
linkPreviews, unidentified, reactions); linkPreviews, unidentified, reactions, hasMention);
this.partCount = partCount; this.partCount = partCount;
} }

@ -51,7 +51,8 @@ public abstract class MessageRecord extends DisplayRecord {
private final long expireStarted; private final long expireStarted;
private final boolean unidentified; private final boolean unidentified;
public final long id; public final long id;
private final List<ReactionRecord> reactions; private final List<ReactionRecord> reactions;
private final boolean hasMention;
public abstract boolean isMms(); public abstract boolean isMms();
public abstract boolean isMmsNotification(); public abstract boolean isMmsNotification();
@ -63,7 +64,7 @@ public abstract class MessageRecord extends DisplayRecord {
List<IdentityKeyMismatch> mismatches, List<IdentityKeyMismatch> mismatches,
List<NetworkFailure> networkFailures, List<NetworkFailure> networkFailures,
long expiresIn, long expireStarted, long expiresIn, long expireStarted,
int readReceiptCount, boolean unidentified, List<ReactionRecord> reactions) int readReceiptCount, boolean unidentified, List<ReactionRecord> reactions, boolean hasMention)
{ {
super(body, conversationRecipient, dateSent, dateReceived, super(body, conversationRecipient, dateSent, dateReceived,
threadId, deliveryStatus, deliveryReceiptCount, type, readReceiptCount); threadId, deliveryStatus, deliveryReceiptCount, type, readReceiptCount);
@ -75,6 +76,7 @@ public abstract class MessageRecord extends DisplayRecord {
this.expireStarted = expireStarted; this.expireStarted = expireStarted;
this.unidentified = unidentified; this.unidentified = unidentified;
this.reactions = reactions; this.reactions = reactions;
this.hasMention = hasMention;
} }
public long getId() { public long getId() {
@ -97,6 +99,8 @@ public abstract class MessageRecord extends DisplayRecord {
} }
public long getExpireStarted() { return expireStarted; } public long getExpireStarted() { return expireStarted; }
public boolean getHasMention() { return hasMention; }
public boolean isMediaPending() { public boolean isMediaPending() {
return false; return false;
} }

@ -27,9 +27,9 @@ public abstract class MmsMessageRecord extends MessageRecord {
List<NetworkFailure> networkFailures, long expiresIn, List<NetworkFailure> networkFailures, long expiresIn,
long expireStarted, @NonNull SlideDeck slideDeck, int readReceiptCount, long expireStarted, @NonNull SlideDeck slideDeck, int readReceiptCount,
@Nullable Quote quote, @NonNull List<Contact> contacts, @Nullable Quote quote, @NonNull List<Contact> contacts,
@NonNull List<LinkPreview> linkPreviews, boolean unidentified, List<ReactionRecord> reactions) @NonNull List<LinkPreview> linkPreviews, boolean unidentified, List<ReactionRecord> 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.slideDeck = slideDeck;
this.quote = quote; this.quote = quote;
this.contacts.addAll(contacts); this.contacts.addAll(contacts);

@ -50,12 +50,12 @@ public class NotificationMmsMessageRecord extends MmsMessageRecord {
long dateSent, long dateReceived, int deliveryReceiptCount, long dateSent, long dateReceived, int deliveryReceiptCount,
long threadId, byte[] contentLocation, long messageSize, long threadId, byte[] contentLocation, long messageSize,
long expiry, int status, byte[] transactionId, long mailbox, long expiry, int status, byte[] transactionId, long mailbox,
SlideDeck slideDeck, int readReceiptCount) SlideDeck slideDeck, int readReceiptCount, boolean hasMention)
{ {
super(id, "", conversationRecipient, individualRecipient, super(id, "", conversationRecipient, individualRecipient,
dateSent, dateReceived, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox, dateSent, dateReceived, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox,
emptyList(), emptyList(), 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.contentLocation = contentLocation;
this.messageSize = messageSize; this.messageSize = messageSize;

@ -43,12 +43,12 @@ public class SmsMessageRecord extends MessageRecord {
long type, long threadId, long type, long threadId,
int status, List<IdentityKeyMismatch> mismatches, int status, List<IdentityKeyMismatch> mismatches,
long expiresIn, long expireStarted, long expiresIn, long expireStarted,
int readReceiptCount, boolean unidentified, List<ReactionRecord> reactions) int readReceiptCount, boolean unidentified, List<ReactionRecord> reactions, boolean hasMention)
{ {
super(id, body, recipient, individualRecipient, super(id, body, recipient, individualRecipient,
dateSent, dateReceived, threadId, status, deliveryReceiptCount, type, dateSent, dateReceived, threadId, status, deliveryReceiptCount, type,
mismatches, new LinkedList<>(), mismatches, new LinkedList<>(),
expiresIn, expireStarted, readReceiptCount, unidentified, reactions); expiresIn, expireStarted, readReceiptCount, unidentified, reactions, hasMention);
} }
public long getType() { public long getType() {

@ -45,6 +45,7 @@ public class ThreadRecord extends DisplayRecord {
private @Nullable final Uri snippetUri; private @Nullable final Uri snippetUri;
private final long count; private final long count;
private final int unreadCount; private final int unreadCount;
private final int unreadMentionCount;
private final int distributionType; private final int distributionType;
private final boolean archived; private final boolean archived;
private final long expiresIn; private final long expiresIn;
@ -53,19 +54,20 @@ public class ThreadRecord extends DisplayRecord {
public ThreadRecord(@NonNull String body, @Nullable Uri snippetUri, public ThreadRecord(@NonNull String body, @Nullable Uri snippetUri,
@NonNull Recipient recipient, long date, long count, int unreadCount, @NonNull Recipient recipient, long date, long count, int unreadCount,
long threadId, int deliveryReceiptCount, int status, long snippetType, int unreadMentionCount, long threadId, int deliveryReceiptCount, int status,
int distributionType, boolean archived, long expiresIn, long lastSeen, long snippetType, int distributionType, boolean archived, long expiresIn,
int readReceiptCount, boolean pinned) long lastSeen, int readReceiptCount, boolean pinned)
{ {
super(body, recipient, date, date, threadId, status, deliveryReceiptCount, snippetType, readReceiptCount); super(body, recipient, date, date, threadId, status, deliveryReceiptCount, snippetType, readReceiptCount);
this.snippetUri = snippetUri; this.snippetUri = snippetUri;
this.count = count; this.count = count;
this.unreadCount = unreadCount; this.unreadCount = unreadCount;
this.distributionType = distributionType; this.unreadMentionCount = unreadMentionCount;
this.archived = archived; this.distributionType = distributionType;
this.expiresIn = expiresIn; this.archived = archived;
this.lastSeen = lastSeen; this.expiresIn = expiresIn;
this.pinned = pinned; this.lastSeen = lastSeen;
this.pinned = pinned;
} }
public @Nullable Uri getSnippetUri() { public @Nullable Uri getSnippetUri() {
@ -147,6 +149,10 @@ public class ThreadRecord extends DisplayRecord {
return unreadCount; return unreadCount;
} }
public int getUnreadMentionCount() {
return unreadMentionCount;
}
public long getDate() { public long getDate() {
return getDateReceived(); return getDateReceived();
} }

@ -25,17 +25,17 @@ import org.thoughtcrime.securesms.util.getAccentColor
import java.util.Locale import java.util.Locale
class ConversationView : LinearLayout { class ConversationView : LinearLayout {
private lateinit var binding: ViewConversationBinding private val binding: ViewConversationBinding by lazy { ViewConversationBinding.bind(this) }
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
var thread: ThreadRecord? = null var thread: ThreadRecord? = null
// region Lifecycle // region Lifecycle
constructor(context: Context) : super(context) { initialize() } constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() } constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() } constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
private fun initialize() { override fun onFinishInflate() {
binding = ViewConversationBinding.inflate(LayoutInflater.from(context), this, true) super.onFinishInflate()
layoutParams = RecyclerView.LayoutParams(screenWidth, RecyclerView.LayoutParams.WRAP_CONTENT) layoutParams = RecyclerView.LayoutParams(screenWidth, RecyclerView.LayoutParams.WRAP_CONTENT)
} }
// endregion // endregion
@ -53,7 +53,7 @@ class ConversationView : LinearLayout {
} else { } else {
binding.conversationViewDisplayNameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0) 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) ContextCompat.getDrawable(context, R.drawable.conversation_unread_background)
} else { } else {
ContextCompat.getDrawable(context, R.drawable.conversation_view_background) ContextCompat.getDrawable(context, R.drawable.conversation_view_background)
@ -79,8 +79,9 @@ class ConversationView : LinearLayout {
binding.unreadCountTextView.text = formattedUnreadCount binding.unreadCountTextView.text = formattedUnreadCount
val textSize = if (unreadCount < 1000) 12.0f else 10.0f val textSize = if (unreadCount < 1000) 12.0f else 10.0f
binding.unreadCountTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize) binding.unreadCountTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize)
binding.unreadCountIndicator.background.setTint(context.getAccentColor())
binding.unreadCountIndicator.isVisible = (unreadCount != 0 && !thread.isRead) 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) val senderDisplayName = getUserDisplayName(thread.recipient)
?: thread.recipient.address.toString() ?: thread.recipient.address.toString()
binding.conversationViewDisplayNameTextView.text = senderDisplayName binding.conversationViewDisplayNameTextView.text = senderDisplayName

@ -1,12 +1,14 @@
package org.thoughtcrime.securesms.home package org.thoughtcrime.securesms.home
import android.content.Context import android.content.Context
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListUpdateCallback import androidx.recyclerview.widget.ListUpdateCallback
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.NO_ID import androidx.recyclerview.widget.RecyclerView.NO_ID
import network.loki.messenger.R
import org.thoughtcrime.securesms.database.model.ThreadRecord import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
@ -76,19 +78,20 @@ class HomeAdapter(
HeaderFooterViewHolder(header!!) HeaderFooterViewHolder(header!!)
} }
ITEM -> { ITEM -> {
val view = ConversationView(context) val conversationView = LayoutInflater.from(parent.context).inflate(R.layout.view_conversation, parent, false) as ConversationView
view.setOnClickListener { view.thread?.let { listener.onConversationClick(it) } } val viewHolder = ConversationViewHolder(conversationView)
view.setOnLongClickListener { viewHolder.view.setOnClickListener { viewHolder.view.thread?.let { listener.onConversationClick(it) } }
view.thread?.let { listener.onLongConversationClick(it) } viewHolder.view.setOnLongClickListener {
viewHolder.view.thread?.let { listener.onLongConversationClick(it) }
true true
} }
ViewHolder(view) viewHolder
} }
else -> throw Exception("viewType $viewType isn't valid") else -> throw Exception("viewType $viewType isn't valid")
} }
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { 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 offset = if (hasHeaderView()) position - 1 else position
val thread = data[offset] val thread = data[offset]
val isTyping = typingThreadIDs.contains(thread.threadId) val isTyping = typingThreadIDs.contains(thread.threadId)
@ -97,7 +100,7 @@ class HomeAdapter(
} }
override fun onViewRecycled(holder: RecyclerView.ViewHolder) { override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
if (holder is ViewHolder) { if (holder is ConversationViewHolder) {
holder.view.recycle() holder.view.recycle()
} else { } else {
super.onViewRecycled(holder) super.onViewRecycled(holder)
@ -110,7 +113,7 @@ class HomeAdapter(
override fun getItemCount(): Int = data.size + if (hasHeaderView()) 1 else 0 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) class HeaderFooterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)

@ -111,6 +111,7 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
duration * 1000L, true, duration * 1000L, true,
false, false,
false, false,
false,
Optional.absent(), Optional.absent(),
groupInfo, groupInfo,
Optional.absent(), Optional.absent(),

@ -135,7 +135,8 @@ object MockDataGenerator {
Optional.absent(), Optional.absent(),
0, 0,
false, false,
-1 -1,
false
), ),
(timestampNow - (index * 5000)), (timestampNow - (index * 5000)),
false, false,
@ -264,7 +265,8 @@ object MockDataGenerator {
Optional.absent(), Optional.absent(),
0, 0,
false, false,
-1 -1,
false
), ),
(timestampNow - (index * 5000)), (timestampNow - (index * 5000)),
false, false,
@ -389,7 +391,8 @@ object MockDataGenerator {
Optional.absent(), Optional.absent(),
0, 0,
false, false,
-1 -1,
false
), ),
(timestampNow - (index * 5000)), (timestampNow - (index * 5000)),
false, false,

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout <org.thoughtcrime.securesms.home.ConversationView
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -66,7 +66,7 @@
android:layout_height="20dp" android:layout_height="20dp"
android:layout_marginStart="4dp" android:layout_marginStart="4dp"
app:layout_constraintStart_toEndOf="@id/conversationViewDisplayNameTextView" app:layout_constraintStart_toEndOf="@id/conversationViewDisplayNameTextView"
app:layout_constraintEnd_toStartOf="@id/timestampTextView" app:layout_constraintEnd_toStartOf="@id/unreadMentionIndicator"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
android:minWidth="20dp" android:minWidth="20dp"
@ -89,6 +89,36 @@
</RelativeLayout> </RelativeLayout>
<RelativeLayout
android:id="@+id/unreadMentionIndicator"
android:layout_width="wrap_content"
android:layout_height="20dp"
android:layout_marginStart="4dp"
app:layout_constraintStart_toEndOf="@id/unreadCountIndicator"
app:layout_constraintEnd_toStartOf="@id/timestampTextView"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:minWidth="20dp"
android:maxWidth="40dp"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:background="@drawable/rounded_rectangle"
android:backgroundTint="?unreadIndicatorBackgroundColor">
<TextView
android:id="@+id/unreadMentionTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:paddingBottom="3dp"
android:textColor="?unreadIndicatorTextColor"
android:textSize="@dimen/very_small_font_size"
android:textStyle="bold"
android:text="@"
tools:textColor="?android:textColorPrimary" />
</RelativeLayout>
<TextView <TextView
android:id="@+id/timestampTextView" android:id="@+id/timestampTextView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -157,4 +187,4 @@
</FrameLayout> </FrameLayout>
</LinearLayout> </org.thoughtcrime.securesms.home.ConversationView>

@ -141,22 +141,25 @@
<color name="ocean_accent">#57C9FA</color> <color name="ocean_accent">#57C9FA</color>
<color name="ocean_dark_0">#111111</color> <color name="ocean_dark_0">#000000</color>
<color name="ocean_dark_1">#1A1C28</color> <color name="ocean_dark_1">#1A1C28</color>
<color name="ocean_dark_2">#252735</color> <color name="ocean_dark_2">#252735</color>
<color name="ocean_dark_3">#2B2D40</color> <color name="ocean_dark_3">#2B2D40</color>
<color name="ocean_dark_4">#3D4A5D</color> <color name="ocean_dark_4">#3D4A5D</color>
<color name="ocean_dark_5">#A6A9CE</color> <color name="ocean_dark_5">#A6A9CE</color>
<color name="ocean_dark_6">#FFFFFF</color> <color name="ocean_dark_6">#5CAACC</color>
<color name="ocean_dark_7">#FFFFFF</color>
<color name="ocean_light_0">#19345D</color>
<color name="ocean_light_1">#6A6E90</color> <color name="ocean_light_0">#000000</color>
<color name="ocean_light_2">#5CAACC</color> <color name="ocean_light_1">#19345D</color>
<color name="ocean_light_3">#B3EDF2</color> <color name="ocean_light_2">#6A6E90</color>
<color name="ocean_light_4">#E7F3F4</color> <color name="ocean_light_3">#5CAACC</color>
<color name="ocean_light_5">#ECFAFB</color> <color name="ocean_light_4">#B3EDF2</color>
<color name="ocean_light_6">#FCFFFF</color> <color name="ocean_light_5">#E7F3F4</color>
<color name="ocean_light_6">#ECFAFB</color>
<color name="danger">#EA5545</color> <color name="ocean_light_7">#FCFFFF</color>
<color name="danger_dark">#FF3A3A</color>
<color name="danger_light">#E12D19</color>
</resources> </resources>

@ -277,7 +277,7 @@
<item name="android:textColorSecondary">?android:textColorPrimary</item> <item name="android:textColorSecondary">?android:textColorPrimary</item>
</style> </style>
<style name="Ocean.Light.BottomSheet" parent="Theme.MaterialComponents.BottomSheetDialog"> <style name="Ocean.Light.BottomSheet" parent="Theme.MaterialComponents.BottomSheetDialog">
<item name="colorPrimary">@color/ocean_light_5</item> <item name="colorPrimary">@color/ocean_light_6</item>
<item name="bottomSheetStyle">@style/Widget.Session.BottomSheetDialog</item> <item name="bottomSheetStyle">@style/Widget.Session.BottomSheetDialog</item>
<item name="dialog_border">@color/transparent_black_15</item> <item name="dialog_border">@color/transparent_black_15</item>
<item name="android:textColorPrimary">@color/black</item> <item name="android:textColorPrimary">@color/black</item>
@ -354,7 +354,7 @@
<item name="conversation_unread_background_color">@color/classic_dark_2</item> <item name="conversation_unread_background_color">@color/classic_dark_2</item>
<item name="conversation_pinned_icon_color">@color/classic_dark_4</item> <item name="conversation_pinned_icon_color">@color/classic_dark_4</item>
<item name="unreadIndicatorBackgroundColor">@color/classic_dark_3</item> <item name="unreadIndicatorBackgroundColor">@color/classic_dark_3</item>
<item name="unreadIndicatorTextColor">@color/classic_dark_0</item> <item name="unreadIndicatorTextColor">@color/classic_dark_6</item>
<!-- New conversation button --> <!-- New conversation button -->
<item name="conversation_color_non_main">@color/classic_dark_2</item> <item name="conversation_color_non_main">@color/classic_dark_2</item>
@ -475,13 +475,13 @@
<style name="Ocean.Dark"> <style name="Ocean.Dark">
<!-- Main styles --> <!-- Main styles -->
<item name="sessionLogoTint">@color/ocean_dark_6</item> <item name="sessionLogoTint">@color/ocean_dark_7</item>
<item name="colorPrimary">@color/ocean_dark_2</item> <item name="colorPrimary">@color/ocean_dark_2</item>
<item name="colorPrimaryDark">@color/ocean_dark_2</item> <item name="colorPrimaryDark">@color/ocean_dark_2</item>
<item name="colorControlNormal">@color/ocean_dark_6</item> <item name="colorControlNormal">@color/ocean_dark_7</item>
<item name="colorControlActivated">?colorAccent</item> <item name="colorControlActivated">?colorAccent</item>
<item name="android:colorControlHighlight">?colorAccent</item> <item name="android:colorControlHighlight">?colorAccent</item>
<item name="android:textColorPrimary">@color/ocean_dark_6</item> <item name="android:textColorPrimary">@color/ocean_dark_7</item>
<item name="android:textColorSecondary">@color/ocean_dark_5</item> <item name="android:textColorSecondary">@color/ocean_dark_5</item>
<item name="android:textColorTertiary">@color/ocean_dark_5</item> <item name="android:textColorTertiary">@color/ocean_dark_5</item>
<item name="android:textColor">?android:textColorPrimary</item> <item name="android:textColor">?android:textColorPrimary</item>
@ -507,7 +507,7 @@
<!-- Home screen --> <!-- Home screen -->
<item name="searchBackgroundColor">@color/ocean_dark_3</item> <item name="searchBackgroundColor">@color/ocean_dark_3</item>
<item name="searchIconColor">@color/ocean_dark_6</item> <item name="searchIconColor">@color/ocean_dark_7</item>
<item name="searchHintColor">@color/ocean_dark_5</item> <item name="searchHintColor">@color/ocean_dark_5</item>
<item name="searchTextColor">?android:textColorPrimary</item> <item name="searchTextColor">?android:textColorPrimary</item>
<item name="searchHighlightTint">?colorAccent</item> <item name="searchHighlightTint">?colorAccent</item>
@ -532,15 +532,15 @@
<!-- Conversation --> <!-- Conversation -->
<item name="message_received_background_color">@color/ocean_dark_4</item> <item name="message_received_background_color">@color/ocean_dark_4</item>
<item name="message_received_text_color">@color/ocean_dark_6</item> <item name="message_received_text_color">@color/ocean_dark_7</item>
<item name="message_sent_background_color">?colorAccent</item> <item name="message_sent_background_color">?colorAccent</item>
<item name="message_sent_text_color">@color/ocean_dark_0</item> <item name="message_sent_text_color">@color/ocean_dark_0</item>
<item name="input_bar_background">@color/ocean_dark_1</item> <item name="input_bar_background">@color/ocean_dark_1</item>
<item name="input_bar_text_hint">@color/ocean_dark_5</item> <item name="input_bar_text_hint">@color/ocean_dark_5</item>
<item name="input_bar_text_user">@color/ocean_dark_6</item> <item name="input_bar_text_user">@color/ocean_dark_7</item>
<item name="input_bar_border">@color/ocean_dark_4</item> <item name="input_bar_border">@color/ocean_dark_4</item>
<item name="input_bar_button_background">@color/ocean_dark_2</item> <item name="input_bar_button_background">@color/ocean_dark_2</item>
<item name="input_bar_button_text_color">@color/ocean_dark_6</item> <item name="input_bar_button_text_color">@color/ocean_dark_7</item>
<item name="input_bar_button_background_opaque">@color/ocean_dark_4</item> <item name="input_bar_button_background_opaque">@color/ocean_dark_4</item>
<item name="input_bar_button_background_opaque_border">@color/ocean_dark_2</item> <item name="input_bar_button_background_opaque_border">@color/ocean_dark_2</item>
<item name="input_bar_lock_view_background">?colorPrimary</item> <item name="input_bar_lock_view_background">?colorPrimary</item>
@ -556,26 +556,26 @@
<style name="Ocean.Light"> <style name="Ocean.Light">
<!-- Main styles --> <!-- Main styles -->
<item name="sessionLogoTint">@color/ocean_light_0</item> <item name="sessionLogoTint">@color/ocean_light_1</item>
<item name="colorPrimary">@color/ocean_light_6</item> <item name="colorPrimary">@color/ocean_light_7</item>
<item name="colorPrimaryDark">@color/ocean_light_5</item> <item name="colorPrimaryDark">@color/ocean_light_6</item>
<item name="colorControlNormal">@color/ocean_light_0</item> <item name="colorControlNormal">@color/ocean_light_1</item>
<item name="colorControlActivated">?colorAccent</item> <item name="colorControlActivated">?colorAccent</item>
<item name="android:colorControlHighlight">?colorAccent</item> <item name="android:colorControlHighlight">?colorAccent</item>
<item name="android:textColorPrimary">@color/ocean_light_0</item> <item name="android:textColorPrimary">@color/ocean_light_1</item>
<item name="android:textColorSecondary">@color/ocean_light_1</item> <item name="android:textColorSecondary">@color/ocean_light_2</item>
<item name="android:textColorTertiary">@color/ocean_light_1</item> <item name="android:textColorTertiary">@color/ocean_light_2</item>
<item name="android:textColor">?android:textColorPrimary</item> <item name="android:textColor">?android:textColorPrimary</item>
<item name="android:textColorHint">@color/ocean_light_5</item> <item name="android:textColorHint">@color/ocean_light_6</item>
<item name="android:navigationBarColor">@color/ocean_light_6</item> <item name="android:navigationBarColor">@color/ocean_light_7</item>
<item name="android:windowBackground">?colorPrimary</item> <item name="android:windowBackground">?colorPrimary</item>
<item name="android:colorBackground">@color/default_background_start</item> <item name="android:colorBackground">@color/default_background_start</item>
<item name="default_background_end">@color/ocean_light_6</item> <item name="default_background_end">@color/ocean_light_7</item>
<item name="default_background_start">@color/ocean_light_5</item> <item name="default_background_start">@color/ocean_light_6</item>
<item name="colorCellBackground">@color/ocean_light_4</item> <item name="colorCellBackground">@color/ocean_light_5</item>
<item name="colorSettingsBackground">@color/ocean_light_5</item> <item name="colorSettingsBackground">@color/ocean_light_6</item>
<item name="colorDividerBackground">@color/ocean_light_2</item> <item name="colorDividerBackground">@color/ocean_light_3</item>
<item name="colorCellRipple">@color/ocean_light_3</item> <item name="colorCellRipple">@color/ocean_light_4</item>
<item name="bottomSheetDialogTheme">@style/Ocean.Light.BottomSheet</item> <item name="bottomSheetDialogTheme">@style/Ocean.Light.BottomSheet</item>
<item name="actionBarPopupTheme">@style/Light.Popup</item> <item name="actionBarPopupTheme">@style/Light.Popup</item>
<item name="popupTheme">?actionBarPopupTheme</item> <item name="popupTheme">?actionBarPopupTheme</item>
@ -584,7 +584,7 @@
<item name="actionBarTheme">@style/ThemeOverlay.AppCompat.ActionBar</item> <item name="actionBarTheme">@style/ThemeOverlay.AppCompat.ActionBar</item>
<item name="actionBarStyle">@style/Widget.Session.ActionBar</item> <item name="actionBarStyle">@style/Widget.Session.ActionBar</item>
<item name="prominentButtonColor">?android:textColorPrimary</item> <item name="prominentButtonColor">?android:textColorPrimary</item>
<item name="elementBorderColor">@color/ocean_light_2</item> <item name="elementBorderColor">@color/ocean_light_3</item>
<!-- Light mode --> <!-- Light mode -->
<item name="theme_type">light</item> <item name="theme_type">light</item>
@ -595,48 +595,48 @@
<item name="android:statusBarColor">?colorPrimary</item> <item name="android:statusBarColor">?colorPrimary</item>
<item name="searchBackgroundColor">@color/ocean_light_4</item> <item name="searchBackgroundColor">@color/ocean_light_5</item>
<item name="searchIconColor">@color/ocean_light_0</item> <item name="searchIconColor">@color/ocean_light_1</item>
<item name="searchHintColor">@color/ocean_light_1</item> <item name="searchHintColor">@color/ocean_light_2</item>
<item name="searchTextColor">@color/ocean_light_0</item> <item name="searchTextColor">@color/ocean_light_1</item>
<item name="searchHighlightTint">?colorAccent</item> <item name="searchHighlightTint">?colorAccent</item>
<item name="home_gradient_start">#00000000</item> <item name="home_gradient_start">#00000000</item>
<item name="home_gradient_end">@color/ocean_light_6</item> <item name="home_gradient_end">@color/ocean_light_7</item>
<item name="conversation_shadow_non_main">@color/black</item> <item name="conversation_shadow_non_main">@color/black</item>
<item name="conversation_shadow_main">@color/black</item> <item name="conversation_shadow_main">@color/black</item>
<item name="conversation_menu_background_color">@color/ocean_light_6</item> <item name="conversation_menu_background_color">@color/ocean_light_7</item>
<item name="conversation_menu_cell_color">@color/ocean_light_5</item> <item name="conversation_menu_cell_color">@color/ocean_light_6</item>
<item name="conversation_menu_border_color">@color/ocean_light_2</item> <item name="conversation_menu_border_color">@color/ocean_light_3</item>
<item name="conversationMenuSearchBackgroundColor">@color/ocean_light_6</item> <item name="conversationMenuSearchBackgroundColor">@color/ocean_light_7</item>
<item name="unreadIndicatorBackgroundColor">?colorAccent</item> <item name="unreadIndicatorBackgroundColor">?colorAccent</item>
<item name="unreadIndicatorTextColor">@color/ocean_light_0</item> <item name="unreadIndicatorTextColor">@color/ocean_light_1</item>
<!-- Conversation --> <!-- Conversation -->
<item name="message_received_background_color">@color/ocean_light_3</item> <item name="message_received_background_color">@color/ocean_light_4</item>
<item name="message_received_text_color">@color/ocean_light_0</item> <item name="message_received_text_color">@color/ocean_light_1</item>
<item name="message_sent_background_color">?colorAccent</item> <item name="message_sent_background_color">?colorAccent</item>
<item name="message_sent_text_color">@color/ocean_light_0</item> <item name="message_sent_text_color">@color/ocean_light_1</item>
<item name="input_bar_background">@color/ocean_light_6</item> <item name="input_bar_background">@color/ocean_light_7</item>
<item name="input_bar_text_hint">@color/ocean_light_1</item> <item name="input_bar_text_hint">@color/ocean_light_2</item>
<item name="input_bar_text_user">@color/ocean_light_0</item> <item name="input_bar_text_user">@color/ocean_light_1</item>
<item name="input_bar_border">@color/ocean_light_2</item> <item name="input_bar_border">@color/ocean_light_3</item>
<item name="input_bar_button_background">@color/ocean_light_4</item> <item name="input_bar_button_background">@color/ocean_light_5</item>
<item name="input_bar_button_background_opaque">@color/ocean_light_4</item> <item name="input_bar_button_background_opaque">@color/ocean_light_5</item>
<item name="input_bar_button_text_color">@color/ocean_light_0</item> <item name="input_bar_button_text_color">@color/ocean_light_1</item>
<item name="input_bar_button_background_opaque_border">@color/ocean_light_0</item> <item name="input_bar_button_background_opaque_border">@color/ocean_light_1</item>
<item name="input_bar_lock_view_background">@color/ocean_light_4</item> <item name="input_bar_lock_view_background">@color/ocean_light_5</item>
<item name="input_bar_lock_view_border">@color/ocean_light_0</item> <item name="input_bar_lock_view_border">@color/ocean_light_1</item>
<item name="mention_candidates_view_background">?colorCellBackground</item> <item name="mention_candidates_view_background">?colorCellBackground</item>
<item name="mention_candidates_view_background_ripple">?colorCellRipple</item> <item name="mention_candidates_view_background_ripple">?colorCellRipple</item>
<item name="scroll_to_bottom_button_background">?input_bar_button_background_opaque</item> <item name="scroll_to_bottom_button_background">?input_bar_button_background_opaque</item>
<item name="scroll_to_bottom_button_border">?input_bar_button_background_opaque_border</item> <item name="scroll_to_bottom_button_border">?input_bar_button_background_opaque_border</item>
<item name="conversation_unread_count_indicator_background">?colorAccent</item> <item name="conversation_unread_count_indicator_background">?colorAccent</item>
<item name="conversation_pinned_background_color">?colorCellBackground</item> <item name="conversation_pinned_background_color">?colorCellBackground</item>
<item name="conversation_unread_background_color">@color/ocean_light_5</item> <item name="conversation_unread_background_color">@color/ocean_light_6</item>
<item name="conversation_pinned_icon_color">?android:textColorPrimary</item> <item name="conversation_pinned_icon_color">?android:textColorPrimary</item>
<item name="message_selected">@color/ocean_light_5</item> <item name="message_selected">@color/ocean_light_6</item>
</style> </style>
</resources> </resources>

@ -21,7 +21,7 @@ interface MessageDataProvider {
*/ */
fun getMessageID(serverId: Long, threadId: Long): Pair<Long, Boolean>? fun getMessageID(serverId: Long, threadId: Long): Pair<Long, Boolean>?
fun deleteMessage(messageID: Long, isSms: Boolean) 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 getServerHashForMessage(messageID: Long): String?
fun getDatabaseAttachment(attachmentId: Long): DatabaseAttachment? fun getDatabaseAttachment(attachmentId: Long): DatabaseAttachment?
fun getAttachmentStream(attachmentId: Long): SessionServiceAttachmentStream? fun getAttachmentStream(attachmentId: Long): SessionServiceAttachmentStream?
@ -36,7 +36,7 @@ interface MessageDataProvider {
fun isOutgoingMessage(timestamp: Long): Boolean fun isOutgoingMessage(timestamp: Long): Boolean
fun handleSuccessfulAttachmentUpload(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: UploadResult) fun handleSuccessfulAttachmentUpload(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: UploadResult)
fun handleFailedAttachmentUpload(attachmentId: Long) fun handleFailedAttachmentUpload(attachmentId: Long)
fun getMessageForQuote(timestamp: Long, author: Address): Pair<Long, Boolean>? fun getMessageForQuote(timestamp: Long, author: Address): Triple<Long, Boolean, String>?
fun getAttachmentsAndLinkPreviewFor(mmsId: Long): List<Attachment> fun getAttachmentsAndLinkPreviewFor(mmsId: Long): List<Attachment>
fun getMessageBodyFor(timestamp: Long, author: String): String fun getMessageBodyFor(timestamp: Long, author: String): String
fun getAttachmentIDsFor(messageID: Long): List<Long> fun getAttachmentIDsFor(messageID: Long): List<Long>

@ -174,7 +174,7 @@ interface StorageProtocol {
*/ */
fun persist(message: VisibleMessage, quotes: QuoteModel?, linkPreview: List<LinkPreview?>, groupPublicKey: String?, openGroupID: String?, attachments: List<Attachment>, runIncrement: Boolean, runThreadUpdate: Boolean): Long? fun persist(message: VisibleMessage, quotes: QuoteModel?, linkPreview: List<LinkPreview?>, groupPublicKey: String?, openGroupID: String?, attachments: List<Attachment>, runIncrement: Boolean, runThreadUpdate: Boolean): Long?
fun markConversationAsRead(threadId: Long, updateLastSeen: Boolean) 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 updateThread(threadId: Long, unarchive: Boolean)
fun insertDataExtractionNotificationMessage(senderPublicKey: String, message: DataExtractionNotificationInfoMessage, sentTimestamp: Long) fun insertDataExtractionNotificationMessage(senderPublicKey: String, message: DataExtractionNotificationInfoMessage, sentTimestamp: Long)
fun insertMessageRequestResponse(response: MessageRequestResponse) fun insertMessageRequestResponse(response: MessageRequestResponse)

@ -11,13 +11,11 @@ import org.session.libsession.database.StorageProtocol
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate 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.ParsedMessage
import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.open_groups.OpenGroupApi import org.session.libsession.messaging.open_groups.OpenGroupApi
import org.session.libsession.messaging.sending_receiving.MessageReceiver import org.session.libsession.messaging.sending_receiving.*
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.utilities.Data import org.session.libsession.messaging.utilities.Data
import org.session.libsession.messaging.utilities.SessionId import org.session.libsession.messaging.utilities.SessionId
import org.session.libsession.messaging.utilities.SodiumUtilities import org.session.libsession.messaging.utilities.SodiumUtilities
@ -108,25 +106,42 @@ class BatchMessageReceiveJob(
runBlocking(Dispatchers.IO) { runBlocking(Dispatchers.IO) {
val deferredThreadMap = threadMap.entries.map { (threadId, messages) -> val deferredThreadMap = threadMap.entries.map { (threadId, messages) ->
async { async {
val messageIds = mutableListOf<Pair<Long, Boolean>>() // The LinkedHashMap should preserve insertion order
val messageIds = linkedMapOf<Long, Pair<Boolean, Boolean>>()
messages.forEach { (parameters, message, proto) -> messages.forEach { (parameters, message, proto) ->
try { try {
if (message is VisibleMessage) { when (message) {
val messageId = MessageReceiver.handleVisibleMessage(message, proto, openGroupID, is VisibleMessage -> {
runIncrement = false, val messageId = MessageReceiver.handleVisibleMessage(message, proto, openGroupID,
runThreadUpdate = false, runIncrement = false,
runProfileUpdate = true 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 } if (messageId != null && message.reaction == null) {
messageIds += messageId to (message.sender == localUserPublicKey || isUserBlindedSender) 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) { } catch (e: Exception) {
Log.e(TAG, "Couldn't process message.", e) Log.e(TAG, "Couldn't process message.", e)
@ -139,14 +154,15 @@ class BatchMessageReceiveJob(
} }
} }
// increment unreads, notify, and update thread // increment unreads, notify, and update thread
val unreadFromMine = messageIds.indexOfLast { (_,fromMe) -> fromMe } val unreadFromMine = messageIds.map { it.value.first }.indexOfLast { it }
var trueUnreadCount = messageIds.filter { (_,fromMe) -> !fromMe }.size var trueUnreadCount = messageIds.filter { !it.value.first }.size
val trueUnreadMentionCount = messageIds.filter { !it.value.first && it.value.second }.size
if (unreadFromMine >= 0) { if (unreadFromMine >= 0) {
trueUnreadCount -= (unreadFromMine + 1) trueUnreadCount -= (unreadFromMine + 1)
storage.markConversationAsRead(threadId, false) storage.markConversationAsRead(threadId, false)
} }
if (trueUnreadCount > 0) { if (trueUnreadCount > 0) {
storage.incrementUnread(threadId, trueUnreadCount) storage.incrementUnread(threadId, trueUnreadCount, trueUnreadMentionCount)
} }
storage.updateThread(threadId, true) storage.updateThread(threadId, true)
SSKEnvironment.shared.notificationManager.updateNotification(context, threadId) SSKEnvironment.shared.notificationManager.updateNotification(context, threadId)

@ -29,6 +29,7 @@ public class IncomingMediaMessage {
private final boolean expirationUpdate; private final boolean expirationUpdate;
private final boolean unidentified; private final boolean unidentified;
private final boolean messageRequestResponse; private final boolean messageRequestResponse;
private final boolean hasMention;
private final DataExtractionNotificationInfoMessage dataExtractionNotification; private final DataExtractionNotificationInfoMessage dataExtractionNotification;
private final QuoteModel quote; private final QuoteModel quote;
@ -44,6 +45,7 @@ public class IncomingMediaMessage {
boolean expirationUpdate, boolean expirationUpdate,
boolean unidentified, boolean unidentified,
boolean messageRequestResponse, boolean messageRequestResponse,
boolean hasMention,
Optional<String> body, Optional<String> body,
Optional<SignalServiceGroup> group, Optional<SignalServiceGroup> group,
Optional<List<SignalServiceAttachment>> attachments, Optional<List<SignalServiceAttachment>> attachments,
@ -63,6 +65,7 @@ public class IncomingMediaMessage {
this.quote = quote.orNull(); this.quote = quote.orNull();
this.unidentified = unidentified; this.unidentified = unidentified;
this.messageRequestResponse = messageRequestResponse; this.messageRequestResponse = messageRequestResponse;
this.hasMention = hasMention;
if (group.isPresent()) this.groupId = Address.fromSerialized(GroupUtil.INSTANCE.getEncodedId(group.get())); if (group.isPresent()) this.groupId = Address.fromSerialized(GroupUtil.INSTANCE.getEncodedId(group.get()));
else this.groupId = null; else this.groupId = null;
@ -81,7 +84,8 @@ public class IncomingMediaMessage {
Optional<List<LinkPreview>> linkPreviews) Optional<List<LinkPreview>> linkPreviews)
{ {
return new IncomingMediaMessage(from, message.getSentTimestamp(), -1, expiresIn, false, 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() { public int getSubscriptionId() {
@ -124,6 +128,10 @@ public class IncomingMediaMessage {
return groupId != null; return groupId != null;
} }
public boolean hasMention() {
return hasMention;
}
public boolean isScreenshotDataExtraction() { public boolean isScreenshotDataExtraction() {
if (dataExtractionNotification == null) return false; if (dataExtractionNotification == null) return false;
else { else {

@ -43,24 +43,25 @@ public class IncomingTextMessage implements Parcelable {
private final long expiresInMillis; private final long expiresInMillis;
private final boolean unidentified; private final boolean unidentified;
private final int callType; private final int callType;
private final boolean hasMention;
private boolean isOpenGroupInvitation = false; private boolean isOpenGroupInvitation = false;
public IncomingTextMessage(Address sender, int senderDeviceId, long sentTimestampMillis, public IncomingTextMessage(Address sender, int senderDeviceId, long sentTimestampMillis,
String encodedBody, Optional<SignalServiceGroup> group, String encodedBody, Optional<SignalServiceGroup> group,
long expiresInMillis, boolean unidentified) { long expiresInMillis, boolean unidentified, boolean hasMention) {
this(sender, senderDeviceId, sentTimestampMillis, encodedBody, group, expiresInMillis, unidentified, -1); this(sender, senderDeviceId, sentTimestampMillis, encodedBody, group, expiresInMillis, unidentified, -1, hasMention);
} }
public IncomingTextMessage(Address sender, int senderDeviceId, long sentTimestampMillis, public IncomingTextMessage(Address sender, int senderDeviceId, long sentTimestampMillis,
String encodedBody, Optional<SignalServiceGroup> group, String encodedBody, Optional<SignalServiceGroup> group,
long expiresInMillis, boolean unidentified, int callType) { long expiresInMillis, boolean unidentified, int callType, boolean hasMention) {
this(sender, senderDeviceId, sentTimestampMillis, encodedBody, group, expiresInMillis, unidentified, callType, true); this(sender, senderDeviceId, sentTimestampMillis, encodedBody, group, expiresInMillis, unidentified, callType, hasMention, true);
} }
public IncomingTextMessage(Address sender, int senderDeviceId, long sentTimestampMillis, public IncomingTextMessage(Address sender, int senderDeviceId, long sentTimestampMillis,
String encodedBody, Optional<SignalServiceGroup> group, String encodedBody, Optional<SignalServiceGroup> group,
long expiresInMillis, boolean unidentified, int callType, boolean isPush) { long expiresInMillis, boolean unidentified, int callType, boolean hasMention, boolean isPush) {
this.message = encodedBody; this.message = encodedBody;
this.sender = sender; this.sender = sender;
this.senderDeviceId = senderDeviceId; this.senderDeviceId = senderDeviceId;
@ -74,6 +75,7 @@ public class IncomingTextMessage implements Parcelable {
this.expiresInMillis = expiresInMillis; this.expiresInMillis = expiresInMillis;
this.unidentified = unidentified; this.unidentified = unidentified;
this.callType = callType; this.callType = callType;
this.hasMention = hasMention;
if (group.isPresent()) { if (group.isPresent()) {
this.groupId = Address.fromSerialized(GroupUtil.getEncodedId(group.get())); this.groupId = Address.fromSerialized(GroupUtil.getEncodedId(group.get()));
@ -98,6 +100,7 @@ public class IncomingTextMessage implements Parcelable {
this.unidentified = in.readInt() == 1; this.unidentified = in.readInt() == 1;
this.isOpenGroupInvitation = in.readInt() == 1; this.isOpenGroupInvitation = in.readInt() == 1;
this.callType = in.readInt(); this.callType = in.readInt();
this.hasMention = in.readInt() == 1;
} }
public IncomingTextMessage(IncomingTextMessage base, String newBody) { public IncomingTextMessage(IncomingTextMessage base, String newBody) {
@ -116,6 +119,7 @@ public class IncomingTextMessage implements Parcelable {
this.unidentified = base.isUnidentified(); this.unidentified = base.isUnidentified();
this.isOpenGroupInvitation = base.isOpenGroupInvitation(); this.isOpenGroupInvitation = base.isOpenGroupInvitation();
this.callType = base.callType; this.callType = base.callType;
this.hasMention = base.hasMention;
} }
public static IncomingTextMessage from(VisibleMessage message, public static IncomingTextMessage from(VisibleMessage message,
@ -123,7 +127,7 @@ public class IncomingTextMessage implements Parcelable {
Optional<SignalServiceGroup> group, Optional<SignalServiceGroup> group,
long expiresInMillis) 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) 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; } if (url == null || name == null) { return null; }
// FIXME: Doing toJSON() to get the body here is weird // FIXME: Doing toJSON() to get the body here is weird
String body = UpdateMessageData.Companion.buildOpenGroupInvitation(url, name).toJSON(); 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; incomingTextMessage.isOpenGroupInvitation = true;
return incomingTextMessage; return incomingTextMessage;
} }
@ -142,7 +146,7 @@ public class IncomingTextMessage implements Parcelable {
Address sender, Address sender,
Optional<SignalServiceGroup> group, Optional<SignalServiceGroup> group,
long sentTimestamp) { 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() { public int getSubscriptionId() {
@ -207,6 +211,8 @@ public class IncomingTextMessage implements Parcelable {
public boolean isOpenGroupInvitation() { return isOpenGroupInvitation; } public boolean isOpenGroupInvitation() { return isOpenGroupInvitation; }
public boolean hasMention() { return hasMention; }
public boolean isCallInfo() { public boolean isCallInfo() {
int callMessageTypeLength = CallMessageType.values().length; int callMessageTypeLength = CallMessageType.values().length;
return callType >= 0 && callType < callMessageTypeLength; return callType >= 0 && callType < callMessageTypeLength;
@ -240,5 +246,6 @@ public class IncomingTextMessage implements Parcelable {
out.writeInt(unidentified ? 1 : 0); out.writeInt(unidentified ? 1 : 0);
out.writeInt(isOpenGroupInvitation ? 1 : 0); out.writeInt(isOpenGroupInvitation ? 1 : 0);
out.writeInt(callType); out.writeInt(callType);
out.writeInt(hasMention ? 1 : 0);
} }
} }

@ -85,8 +85,8 @@ public class OutgoingMediaMessage {
previews = Collections.singletonList(linkPreview); previews = Collections.singletonList(linkPreview);
} }
return new OutgoingMediaMessage(recipient, message.getText(), attachments, message.getSentTimestamp(), -1, return new OutgoingMediaMessage(recipient, message.getText(), attachments, message.getSentTimestamp(), -1,
recipient.getExpireMessages() * 1000, DistributionTypes.DEFAULT, outgoingQuote, Collections.emptyList(), recipient.getExpireMessages() * 1000, DistributionTypes.DEFAULT, outgoingQuote,
previews, Collections.emptyList(), Collections.emptyList()); Collections.emptyList(), previews, Collections.emptyList(), Collections.emptyList());
} }
public Recipient getRecipient() { public Recipient getRecipient() {

@ -24,6 +24,7 @@ class VisibleMessage : Message() {
var profile: Profile? = null var profile: Profile? = null
var openGroupInvitation: OpenGroupInvitation? = null var openGroupInvitation: OpenGroupInvitation? = null
var reaction: Reaction? = null var reaction: Reaction? = null
var hasMention: Boolean = false
override val isSelfSendValid: Boolean = true override val isSelfSendValid: Boolean = true

@ -173,22 +173,24 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) {
storage.addContacts(message.contacts) storage.addContacts(message.contacts)
} }
fun MessageReceiver.handleUnsendRequest(message: UnsendRequest) { fun MessageReceiver.handleUnsendRequest(message: UnsendRequest): Long? {
val userPublicKey = MessagingModuleConfiguration.shared.storage.getUserPublicKey() 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 context = MessagingModuleConfiguration.shared.context
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
val timestamp = message.timestamp ?: return val timestamp = message.timestamp ?: return null
val author = message.author ?: return val author = message.author ?: return null
val messageIdToDelete = storage.getMessageIdInDatabase(timestamp, author) ?: return val messageIdToDelete = storage.getMessageIdInDatabase(timestamp, author) ?: return null
messageDataProvider.getServerHashForMessage(messageIdToDelete)?.let { serverHash -> messageDataProvider.getServerHashForMessage(messageIdToDelete)?.let { serverHash ->
SnodeAPI.deleteMessage(author, listOf(serverHash)) SnodeAPI.deleteMessage(author, listOf(serverHash))
} }
messageDataProvider.updateMessageAsDeleted(timestamp, author) val deletedMessageId = messageDataProvider.updateMessageAsDeleted(timestamp, author)
if (!messageDataProvider.isOutgoingMessage(messageIdToDelete)) { if (!messageDataProvider.isOutgoingMessage(messageIdToDelete)) {
SSKEnvironment.shared.notificationManager.updateNotification(context) SSKEnvironment.shared.notificationManager.updateNotification(context)
} }
return deletedMessageId
} }
fun handleMessageRequestResponse(message: MessageRequestResponse) { fun handleMessageRequestResponse(message: MessageRequestResponse) {
@ -248,6 +250,7 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage,
} }
// Parse quote if needed // Parse quote if needed
var quoteModel: QuoteModel? = null var quoteModel: QuoteModel? = null
var quoteMessageBody: String? = null
if (message.quote != null && proto.dataMessage.hasQuote()) { if (message.quote != null && proto.dataMessage.hasQuote()) {
val quote = proto.dataMessage.quote val quote = proto.dataMessage.quote
@ -259,6 +262,7 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage,
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
val messageInfo = messageDataProvider.getMessageForQuote(quote.id, author) val messageInfo = messageDataProvider.getMessageForQuote(quote.id, author)
quoteMessageBody = messageInfo?.third
quoteModel = if (messageInfo != null) { quoteModel = if (messageInfo != null) {
val attachments = if (messageInfo.second) messageDataProvider.getAttachmentsAndLinkPreviewFor(messageInfo.first) else ArrayList() val attachments = if (messageInfo.second) messageDataProvider.getAttachmentsAndLinkPreviewFor(messageInfo.first) else ArrayList()
QuoteModel(quote.id, author,null,false, attachments) 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) storage.removeReaction(reaction.emoji!!, reaction.timestamp!!, reaction.publicKey!!, threadIsGroup)
} }
} ?: run { } ?: 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 // Persist the message
message.threadID = threadID message.threadID = threadID
val messageID = val messageID =

Loading…
Cancel
Save