Merge pull request #1082 from mpretty-cyro/feature/unread-mention-indicator

Added the unread mention indicator to the conversation list
This commit is contained in:
Morgan Pretty 2023-01-24 15:53:54 +11:00 committed by GitHub
commit abf733fbdb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 344 additions and 183 deletions

View File

@ -74,10 +74,10 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
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 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> {
@ -198,16 +198,18 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHashes(messageIDs)
}
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? {

View File

@ -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);
public abstract boolean deleteMessages(long[] messageId, long threadId);

View File

@ -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)
@ -1289,7 +1293,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
)
}
@ -1333,6 +1337,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
}
@ -1350,7 +1355,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
dateSent, dateReceived, deliveryReceiptCount, threadId,
contentLocationBytes, messageSize, expiry, status,
transactionIdBytes, mailbox, slideDeck,
readReceiptCount
readReceiptCount, hasMention
)
}
@ -1384,6 +1389,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
}
@ -1420,7 +1426,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
)
}
@ -1613,5 +1619,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;"
}
}

View File

@ -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;

View File

@ -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);
@ -186,7 +188,7 @@ public class MmsSmsDatabase extends Database {
}
public Cursor getConversationSnippet(long threadId) {
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC";
String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC";
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
return queryTables(PROJECTION, selection, order, null);
@ -203,7 +205,7 @@ public class MmsSmsDatabase extends Database {
}
public Cursor getUnread() {
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " ASC";
String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " ASC";
String selection = "(" + MmsSmsColumns.READ + " = 0 OR " + MmsSmsColumns.REACTIONS_UNREAD + " = 1) AND " + MmsSmsColumns.NOTIFIED + " = 0";
return queryTables(PROJECTION, selection, order, null);
@ -238,7 +240,7 @@ public class MmsSmsDatabase extends Database {
}
public int getQuotedMessagePosition(long threadId, long quoteId, @NonNull Address address) {
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC";
String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC";
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
try (Cursor cursor = queryTables(new String[]{ MmsSmsColumns.NORMALIZED_DATE_SENT, MmsSmsColumns.ADDRESS }, selection, order, null)) {
@ -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);

View File

@ -123,6 +123,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();
@ -208,14 +211,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);
}
@ -446,6 +452,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());
@ -464,7 +471,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) {
@ -762,7 +769,7 @@ public class SmsDatabase extends MessagingDatabase {
0, message.isSecureMessage() ? MmsSmsColumns.Types.getOutgoingEncryptedMessageType() : MmsSmsColumns.Types.getOutgoingSmsMessageType(),
threadId, 0, new LinkedList<IdentityKeyMismatch>(),
message.getExpiresIn(),
System.currentTimeMillis(), 0, false, Collections.emptyList());
System.currentTimeMillis(), 0, false, Collections.emptyList(), false);
}
}
@ -803,6 +810,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;
@ -816,7 +824,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<IdentityKeyMismatch> getMismatches(String document) {

View File

@ -104,9 +104,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) {
@ -465,7 +465,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) {
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()
@ -728,6 +728,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
false,
false,
false,
false,
Optional.absent(),
Optional.absent(),
Optional.absent(),
@ -821,6 +822,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
false,
false,
true,
false,
Optional.absent(),
Optional.absent(),
Optional.absent(),

View File

@ -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);
}

View File

@ -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();

View File

@ -57,12 +57,12 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
long expiresIn, long expireStarted, int readReceiptCount,
@Nullable Quote quote, @NonNull List<Contact> contacts,
@NonNull List<LinkPreview> linkPreviews,
@NonNull List<ReactionRecord> reactions, boolean unidentified)
@NonNull List<ReactionRecord> 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;
}

View File

@ -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<ReactionRecord> reactions;
private final List<ReactionRecord> reactions;
private final boolean hasMention;
public abstract boolean isMms();
public abstract boolean isMmsNotification();
@ -63,7 +64,7 @@ public abstract class MessageRecord extends DisplayRecord {
List<IdentityKeyMismatch> mismatches,
List<NetworkFailure> networkFailures,
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,
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;
}

View File

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

View File

@ -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;

View File

@ -43,12 +43,12 @@ public class SmsMessageRecord extends MessageRecord {
long type, long threadId,
int status, List<IdentityKeyMismatch> mismatches,
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,
dateSent, dateReceived, threadId, status, deliveryReceiptCount, type,
mismatches, new LinkedList<>(),
expiresIn, expireStarted, readReceiptCount, unidentified, reactions);
expiresIn, expireStarted, readReceiptCount, unidentified, reactions, hasMention);
}
public long getType() {

View File

@ -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();
}

View File

@ -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

View File

@ -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)

View File

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

View File

@ -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,

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
<org.thoughtcrime.securesms.home.ConversationView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
@ -66,7 +66,7 @@
android:layout_height="20dp"
android:layout_marginStart="4dp"
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_constraintBottom_toBottomOf="parent"
android:minWidth="20dp"
@ -89,6 +89,36 @@
</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
android:id="@+id/timestampTextView"
android:layout_width="wrap_content"
@ -157,4 +187,4 @@
</FrameLayout>
</LinearLayout>
</org.thoughtcrime.securesms.home.ConversationView>

View File

@ -141,22 +141,25 @@
<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_2">#252735</color>
<color name="ocean_dark_3">#2B2D40</color>
<color name="ocean_dark_4">#3D4A5D</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_2">#5CAACC</color>
<color name="ocean_light_3">#B3EDF2</color>
<color name="ocean_light_4">#E7F3F4</color>
<color name="ocean_light_5">#ECFAFB</color>
<color name="ocean_light_6">#FCFFFF</color>
<color name="ocean_light_0">#000000</color>
<color name="ocean_light_1">#19345D</color>
<color name="ocean_light_2">#6A6E90</color>
<color name="ocean_light_3">#5CAACC</color>
<color name="ocean_light_4">#B3EDF2</color>
<color name="ocean_light_5">#E7F3F4</color>
<color name="ocean_light_6">#ECFAFB</color>
<color name="ocean_light_7">#FCFFFF</color>
<color name="danger">#EA5545</color>
<color name="danger_dark">#FF3A3A</color>
<color name="danger_light">#E12D19</color>
</resources>

View File

@ -277,7 +277,7 @@
<item name="android:textColorSecondary">?android:textColorPrimary</item>
</style>
<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="dialog_border">@color/transparent_black_15</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_pinned_icon_color">?android:textColorSecondary</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 -->
<item name="conversation_color_non_main">@color/classic_dark_2</item>
@ -477,13 +477,13 @@
<style name="Ocean.Dark">
<!-- 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="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="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:textColorTertiary">@color/ocean_dark_5</item>
<item name="android:textColor">?android:textColorPrimary</item>
@ -509,7 +509,7 @@
<!-- Home screen -->
<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="searchTextColor">?android:textColorPrimary</item>
<item name="searchHighlightTint">?colorAccent</item>
@ -534,16 +534,16 @@
<!-- Conversation -->
<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_text_color">@color/ocean_dark_0</item>
<item name="message_status_color">@color/ocean_dark_5</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_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_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_border">@color/ocean_dark_2</item>
<item name="input_bar_lock_view_background">?colorPrimary</item>
@ -559,26 +559,26 @@
<style name="Ocean.Light">
<!-- Main styles -->
<item name="sessionLogoTint">@color/ocean_light_0</item>
<item name="colorPrimary">@color/ocean_light_6</item>
<item name="colorPrimaryDark">@color/ocean_light_5</item>
<item name="colorControlNormal">@color/ocean_light_0</item>
<item name="sessionLogoTint">@color/ocean_light_1</item>
<item name="colorPrimary">@color/ocean_light_7</item>
<item name="colorPrimaryDark">@color/ocean_light_6</item>
<item name="colorControlNormal">@color/ocean_light_1</item>
<item name="colorControlActivated">?colorAccent</item>
<item name="android:colorControlHighlight">?colorAccent</item>
<item name="android:textColorPrimary">@color/ocean_light_0</item>
<item name="android:textColorSecondary">@color/ocean_light_1</item>
<item name="android:textColorTertiary">@color/ocean_light_1</item>
<item name="android:textColorPrimary">@color/ocean_light_1</item>
<item name="android:textColorSecondary">@color/ocean_light_2</item>
<item name="android:textColorTertiary">@color/ocean_light_2</item>
<item name="android:textColor">?android:textColorPrimary</item>
<item name="android:textColorHint">@color/ocean_light_5</item>
<item name="android:navigationBarColor">@color/ocean_light_6</item>
<item name="android:textColorHint">@color/ocean_light_6</item>
<item name="android:navigationBarColor">@color/ocean_light_7</item>
<item name="android:windowBackground">?colorPrimary</item>
<item name="android:colorBackground">@color/default_background_start</item>
<item name="default_background_end">@color/ocean_light_6</item>
<item name="default_background_start">@color/ocean_light_5</item>
<item name="colorCellBackground">@color/ocean_light_4</item>
<item name="colorSettingsBackground">@color/ocean_light_5</item>
<item name="colorDividerBackground">@color/ocean_light_2</item>
<item name="colorCellRipple">@color/ocean_light_3</item>
<item name="default_background_end">@color/ocean_light_7</item>
<item name="default_background_start">@color/ocean_light_6</item>
<item name="colorCellBackground">@color/ocean_light_5</item>
<item name="colorSettingsBackground">@color/ocean_light_6</item>
<item name="colorDividerBackground">@color/ocean_light_3</item>
<item name="colorCellRipple">@color/ocean_light_4</item>
<item name="bottomSheetDialogTheme">@style/Ocean.Light.BottomSheet</item>
<item name="actionBarPopupTheme">@style/Light.Popup</item>
<item name="popupTheme">?actionBarPopupTheme</item>
@ -587,7 +587,7 @@
<item name="actionBarTheme">@style/ThemeOverlay.AppCompat.ActionBar</item>
<item name="actionBarStyle">@style/Widget.Session.ActionBar</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 -->
<item name="theme_type">light</item>
@ -598,49 +598,49 @@
<item name="android:statusBarColor">?colorPrimary</item>
<item name="searchBackgroundColor">@color/ocean_light_4</item>
<item name="searchIconColor">@color/ocean_light_0</item>
<item name="searchHintColor">@color/ocean_light_1</item>
<item name="searchTextColor">@color/ocean_light_0</item>
<item name="searchBackgroundColor">@color/ocean_light_5</item>
<item name="searchIconColor">@color/ocean_light_1</item>
<item name="searchHintColor">@color/ocean_light_2</item>
<item name="searchTextColor">@color/ocean_light_1</item>
<item name="searchHighlightTint">?colorAccent</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_main">@color/black</item>
<item name="conversation_menu_background_color">@color/ocean_light_6</item>
<item name="conversation_menu_cell_color">@color/ocean_light_5</item>
<item name="conversation_menu_border_color">@color/ocean_light_2</item>
<item name="conversationMenuSearchBackgroundColor">@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_6</item>
<item name="conversation_menu_border_color">@color/ocean_light_3</item>
<item name="conversationMenuSearchBackgroundColor">@color/ocean_light_7</item>
<item name="unreadIndicatorBackgroundColor">?colorAccent</item>
<item name="unreadIndicatorTextColor">@color/ocean_light_0</item>
<item name="unreadIndicatorTextColor">@color/ocean_light_1</item>
<!-- Conversation -->
<item name="message_received_background_color">@color/ocean_light_3</item>
<item name="message_received_text_color">@color/ocean_light_0</item>
<item name="message_received_background_color">@color/ocean_light_4</item>
<item name="message_received_text_color">@color/ocean_light_1</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="message_status_color">@color/ocean_light_2</item>
<item name="input_bar_background">@color/ocean_light_6</item>
<item name="input_bar_text_hint">@color/ocean_light_1</item>
<item name="input_bar_text_user">@color/ocean_light_0</item>
<item name="input_bar_border">@color/ocean_light_2</item>
<item name="input_bar_button_background">@color/ocean_light_4</item>
<item name="input_bar_button_background_opaque">@color/ocean_light_4</item>
<item name="input_bar_button_text_color">@color/ocean_light_0</item>
<item name="input_bar_button_background_opaque_border">@color/ocean_light_0</item>
<item name="input_bar_lock_view_background">@color/ocean_light_4</item>
<item name="input_bar_lock_view_border">@color/ocean_light_0</item>
<item name="input_bar_background">@color/ocean_light_7</item>
<item name="input_bar_text_hint">@color/ocean_light_2</item>
<item name="input_bar_text_user">@color/ocean_light_1</item>
<item name="input_bar_border">@color/ocean_light_3</item>
<item name="input_bar_button_background">@color/ocean_light_5</item>
<item name="input_bar_button_background_opaque">@color/ocean_light_5</item>
<item name="input_bar_button_text_color">@color/ocean_light_1</item>
<item name="input_bar_button_background_opaque_border">@color/ocean_light_1</item>
<item name="input_bar_lock_view_background">@color/ocean_light_5</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_ripple">?colorCellRipple</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="conversation_unread_count_indicator_background">?colorAccent</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:textColorSecondary</item>
<item name="message_selected">@color/ocean_light_5</item>
<item name="message_selected">@color/ocean_light_6</item>
</style>
</resources>

View File

@ -23,7 +23,7 @@ interface MessageDataProvider {
fun getMessageIDs(serverIDs: List<Long>, threadID: Long): Pair<List<Long>, List<Long>>
fun deleteMessage(messageID: Long, isSms: Boolean)
fun deleteMessages(messageIDs: List<Long>, threadId: 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?
@ -38,7 +38,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<Long, Boolean>?
fun getMessageForQuote(timestamp: Long, author: Address): Triple<Long, Boolean, String>?
fun getAttachmentsAndLinkPreviewFor(mmsId: Long): List<Attachment>
fun getMessageBodyFor(timestamp: Long, author: String): String
fun getAttachmentIDsFor(messageID: Long): List<Long>

View File

@ -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 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)

View File

@ -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
@ -119,25 +117,42 @@ class BatchMessageReceiveJob(
runBlocking(Dispatchers.IO) {
val deferredThreadMap = threadMap.entries.map { (threadId, messages) ->
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) ->
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)
@ -150,14 +165,20 @@ 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
var trueUnreadMentionCount = messageIds.filter { !it.value.first && it.value.second }.size
if (unreadFromMine >= 0) {
trueUnreadCount -= (unreadFromMine + 1)
storage.markConversationAsRead(threadId, false)
val trueUnreadIds = messageIds.keys.toList().subList(unreadFromMine + 1, messageIds.keys.count())
trueUnreadCount = trueUnreadIds.size
trueUnreadMentionCount = messageIds
.filter { trueUnreadIds.contains(it.key) && !it.value.first && it.value.second }
.size
}
if (trueUnreadCount > 0) {
storage.incrementUnread(threadId, trueUnreadCount)
storage.incrementUnread(threadId, trueUnreadCount, trueUnreadMentionCount)
}
storage.updateThread(threadId, true)
SSKEnvironment.shared.notificationManager.updateNotification(context, threadId)

View File

@ -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<String> body,
Optional<SignalServiceGroup> group,
Optional<List<SignalServiceAttachment>> 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<List<LinkPreview>> 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 {

View File

@ -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<SignalServiceGroup> 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<SignalServiceGroup> 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<SignalServiceGroup> 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<SignalServiceGroup> 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<SignalServiceGroup> 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);
}
}

View File

@ -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() {

View File

@ -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

View File

@ -189,22 +189,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) {
@ -264,6 +266,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
@ -275,6 +278,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)
@ -321,6 +325,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 =