mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-24 10:51:27 +01:00
Move reactions into their own table.
This commit is contained in:
committed by
Cody Henthorne
parent
3a1f06f510
commit
ab55fec6bd
@@ -167,6 +167,7 @@ import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.database.identity.IdentityRecordList;
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.database.model.MessageId;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
@@ -2361,9 +2362,9 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
.orElse(null);
|
||||
|
||||
if (oldRecord != null && oldRecord.getEmoji().equals(emoji)) {
|
||||
MessageSender.sendReactionRemoval(context, messageRecord.getId(), messageRecord.isMms(), oldRecord);
|
||||
MessageSender.sendReactionRemoval(context, new MessageId(messageRecord.getId(), messageRecord.isMms()), oldRecord);
|
||||
} else {
|
||||
MessageSender.sendNewReaction(context, messageRecord.getId(), messageRecord.isMms(), emoji);
|
||||
MessageSender.sendNewReaction(context, new MessageId(messageRecord.getId(), messageRecord.isMms()), emoji);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -2381,8 +2382,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
reactionDelegate.hide();
|
||||
|
||||
SignalExecutors.BOUNDED.execute(() -> MessageSender.sendReactionRemoval(context,
|
||||
messageRecord.getId(),
|
||||
messageRecord.isMms(),
|
||||
new MessageId(messageRecord.getId(), messageRecord.isMms()),
|
||||
oldRecord));
|
||||
} else {
|
||||
reactionDelegate.hideForReactWithAny();
|
||||
|
||||
@@ -13,7 +13,6 @@ import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationData.MessageRequestData;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MentionDatabase;
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord;
|
||||
@@ -21,6 +20,8 @@ import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.database.model.MessageId;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
@@ -72,12 +73,14 @@ class ConversationDataSource implements PagedDataSource<MessageId, ConversationM
|
||||
List<MessageRecord> records = new ArrayList<>(length);
|
||||
MentionHelper mentionHelper = new MentionHelper();
|
||||
AttachmentHelper attachmentHelper = new AttachmentHelper();
|
||||
ReactionHelper reactionHelper = new ReactionHelper();
|
||||
|
||||
try (MmsSmsDatabase.Reader reader = MmsSmsDatabase.readerFor(db.getConversation(threadId, start, length))) {
|
||||
MessageRecord record;
|
||||
while ((record = reader.getNext()) != null && !cancellationSignal.isCanceled()) {
|
||||
records.add(record);
|
||||
mentionHelper.add(record);
|
||||
reactionHelper.add(record);
|
||||
attachmentHelper.add(record);
|
||||
}
|
||||
}
|
||||
@@ -93,15 +96,18 @@ class ConversationDataSource implements PagedDataSource<MessageId, ConversationM
|
||||
stopwatch.split("messages");
|
||||
|
||||
mentionHelper.fetchMentions(context);
|
||||
|
||||
stopwatch.split("mentions");
|
||||
|
||||
attachmentHelper.fetchAttachments(context);
|
||||
reactionHelper.fetchReactions(context);
|
||||
stopwatch.split("reactions");
|
||||
|
||||
records = reactionHelper.buildUpdatedModels(context, records);
|
||||
stopwatch.split("reaction-models");
|
||||
|
||||
attachmentHelper.fetchAttachments(context);
|
||||
stopwatch.split("attachments");
|
||||
|
||||
records = attachmentHelper.buildUpdatedModels(context, records);
|
||||
|
||||
stopwatch.split("attachment-models");
|
||||
|
||||
List<ConversationMessage> messages = Stream.of(records)
|
||||
@@ -133,6 +139,11 @@ class ConversationDataSource implements PagedDataSource<MessageId, ConversationM
|
||||
|
||||
stopwatch.split("mentions");
|
||||
|
||||
List<ReactionRecord> reactions = DatabaseFactory.getReactionDatabase(context).getReactions(messageId);
|
||||
record = ReactionHelper.recordWithReactions(record, reactions);
|
||||
|
||||
stopwatch.split("reactions");
|
||||
|
||||
if (messageId.isMms()) {
|
||||
List<DatabaseAttachment> attachments = DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(messageId.getId());
|
||||
if (attachments.size() > 0) {
|
||||
@@ -208,4 +219,43 @@ class ConversationDataSource implements PagedDataSource<MessageId, ConversationM
|
||||
}
|
||||
}
|
||||
|
||||
private static class ReactionHelper {
|
||||
|
||||
private Collection<MessageId> messageIds = new LinkedList<>();
|
||||
private Map<MessageId, List<ReactionRecord>> messageIdToReactions = new HashMap<>();
|
||||
|
||||
void add(MessageRecord record) {
|
||||
messageIds.add(new MessageId(record.getId(), record.isMms()));
|
||||
}
|
||||
|
||||
void fetchReactions(Context context) {
|
||||
messageIdToReactions = DatabaseFactory.getReactionDatabase(context).getReactionsForMessages(messageIds);
|
||||
}
|
||||
|
||||
@NonNull List<MessageRecord> buildUpdatedModels(@NonNull Context context, @NonNull List<MessageRecord> records) {
|
||||
return records.stream()
|
||||
.map(record -> {
|
||||
MessageId messageId = new MessageId(record.getId(), record.isMms());
|
||||
List<ReactionRecord> reactions = messageIdToReactions.get(messageId);
|
||||
|
||||
return recordWithReactions(record, reactions);
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
static MessageRecord recordWithReactions(@NonNull MessageRecord record, List<ReactionRecord> reactions) {
|
||||
if (Util.hasItems(reactions)) {
|
||||
if (record instanceof MediaMmsMessageRecord) {
|
||||
return ((MediaMmsMessageRecord) record).withReactions(reactions);
|
||||
} else if (record instanceof SmsMessageRecord) {
|
||||
return ((SmsMessageRecord) record).withReactions(reactions);
|
||||
} else {
|
||||
throw new IllegalStateException("We have reactions for an unsupported record type: " + record.getClass().getName());
|
||||
}
|
||||
} else {
|
||||
return record;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ public class DatabaseFactory {
|
||||
private final MessageSendLogDatabase messageSendLogDatabase;
|
||||
private final AvatarPickerDatabase avatarPickerDatabase;
|
||||
private final GroupCallRingDatabase groupCallRingDatabase;
|
||||
private final ReactionDatabase reactionDatabase;
|
||||
|
||||
public static DatabaseFactory getInstance(Context context) {
|
||||
if (instance == null) {
|
||||
@@ -214,6 +215,10 @@ public class DatabaseFactory {
|
||||
return getInstance(context).groupCallRingDatabase;
|
||||
}
|
||||
|
||||
public static ReactionDatabase getReactionDatabase(Context context) {
|
||||
return getInstance(context).reactionDatabase;
|
||||
}
|
||||
|
||||
public static net.zetetic.database.sqlcipher.SQLiteDatabase getBackupDatabase(Context context) {
|
||||
return getInstance(context).databaseHelper.getRawReadableDatabase();
|
||||
}
|
||||
@@ -274,6 +279,7 @@ public class DatabaseFactory {
|
||||
this.messageSendLogDatabase = new MessageSendLogDatabase(context, databaseHelper);
|
||||
this.avatarPickerDatabase = new AvatarPickerDatabase(context, databaseHelper);
|
||||
this.groupCallRingDatabase = new GroupCallRingDatabase(context, databaseHelper);
|
||||
this.reactionDatabase = new ReactionDatabase(context, databaseHelper);
|
||||
}
|
||||
|
||||
public void onApplicationLevelUpgrade(@NonNull Context context, @NonNull MasterSecret masterSecret,
|
||||
|
||||
@@ -310,73 +310,6 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
|
||||
db.update(getTableName(), values, query, args);
|
||||
}
|
||||
|
||||
public void addReaction(long messageId, @NonNull ReactionRecord reaction) {
|
||||
SQLiteDatabase db = databaseHelper.getSignalWritableDatabase();
|
||||
|
||||
db.beginTransaction();
|
||||
|
||||
try {
|
||||
ReactionList reactions = getReactions(db, messageId).or(ReactionList.getDefaultInstance());
|
||||
ReactionList.Reaction newReaction = ReactionList.Reaction.newBuilder()
|
||||
.setEmoji(reaction.getEmoji())
|
||||
.setAuthor(reaction.getAuthor().toLong())
|
||||
.setSentTime(reaction.getDateSent())
|
||||
.setReceivedTime(reaction.getDateReceived())
|
||||
.build();
|
||||
|
||||
ReactionList updatedList = pruneByAuthor(reactions, reaction.getAuthor()).toBuilder()
|
||||
.addReactions(newReaction)
|
||||
.build();
|
||||
|
||||
setReactions(db, messageId, updatedList);
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} catch (NoSuchMessageException e) {
|
||||
Log.w(TAG, "No message for provided id", e);
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
notifyConversationListeners(getThreadId(db, messageId));
|
||||
}
|
||||
|
||||
public void deleteReaction(long messageId, @NonNull RecipientId author) {
|
||||
SQLiteDatabase db = databaseHelper.getSignalWritableDatabase();
|
||||
|
||||
db.beginTransaction();
|
||||
|
||||
try {
|
||||
ReactionList reactions = getReactions(db, messageId).or(ReactionList.getDefaultInstance());
|
||||
ReactionList updatedList = pruneByAuthor(reactions, author);
|
||||
|
||||
setReactions(db, messageId, updatedList);
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} catch (NoSuchMessageException e) {
|
||||
Log.w(TAG, "No message for provided id", e);
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
notifyConversationListeners(getThreadId(db, messageId));
|
||||
}
|
||||
|
||||
public boolean hasReaction(long messageId, @NonNull ReactionRecord reactionRecord) {
|
||||
SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
|
||||
|
||||
ReactionList reactions = getReactions(db, messageId).or(ReactionList.getDefaultInstance());
|
||||
|
||||
for (ReactionList.Reaction reaction : reactions.getReactionsList()) {
|
||||
if (reactionRecord.getAuthor().toLong() == reaction.getAuthor() &&
|
||||
reactionRecord.getEmoji().equals(reaction.getEmoji()))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void setNotifiedTimestamp(long timestamp, @NonNull List<Long> ids) {
|
||||
if (ids.isEmpty()) {
|
||||
return;
|
||||
@@ -438,25 +371,26 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
|
||||
return data;
|
||||
}
|
||||
|
||||
protected static List<ReactionRecord> parseReactions(@NonNull Cursor cursor) {
|
||||
byte[] raw = cursor.getBlob(cursor.getColumnIndexOrThrow(REACTIONS));
|
||||
void updateReactionsUnread(SQLiteDatabase db, long messageId, boolean hasReactions, boolean isRemoval) {
|
||||
try {
|
||||
boolean isOutgoing = getMessageRecord(messageId).isOutgoing();
|
||||
ContentValues values = new ContentValues();
|
||||
|
||||
if (raw != null) {
|
||||
try {
|
||||
return Stream.of(ReactionList.parseFrom(raw).getReactionsList())
|
||||
.map(r -> {
|
||||
return new ReactionRecord(r.getEmoji(),
|
||||
RecipientId.from(r.getAuthor()),
|
||||
r.getSentTime(),
|
||||
r.getReceivedTime());
|
||||
})
|
||||
.toList();
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
Log.w(TAG, "[parseReactions] Failed to parse reaction list!", e);
|
||||
return Collections.emptyList();
|
||||
if (!hasReactions) {
|
||||
values.put(REACTIONS_UNREAD, 0);
|
||||
} else if (!isRemoval) {
|
||||
values.put(REACTIONS_UNREAD, 1);
|
||||
}
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
|
||||
if (isOutgoing && hasReactions) {
|
||||
values.put(NOTIFIED, 0);
|
||||
}
|
||||
|
||||
if (values.size() > 0) {
|
||||
db.update(getTableName(), values, ID_WHERE, SqlUtil.buildArgs(messageId));
|
||||
}
|
||||
} catch (NoSuchMessageException e) {
|
||||
Log.w(TAG, "Failed to find message " + messageId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -555,55 +489,6 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
|
||||
}
|
||||
}
|
||||
|
||||
private static @NonNull ReactionList pruneByAuthor(@NonNull ReactionList reactionList, @NonNull RecipientId recipientId) {
|
||||
List<ReactionList.Reaction> pruned = Stream.of(reactionList.getReactionsList())
|
||||
.filterNot(r -> r.getAuthor() == recipientId.toLong())
|
||||
.toList();
|
||||
|
||||
return reactionList.toBuilder()
|
||||
.clearReactions()
|
||||
.addAllReactions(pruned)
|
||||
.build();
|
||||
}
|
||||
|
||||
private @NonNull Optional<ReactionList> getReactions(SQLiteDatabase db, long messageId) {
|
||||
String[] projection = new String[]{ REACTIONS };
|
||||
String query = ID + " = ?";
|
||||
String[] args = new String[]{String.valueOf(messageId)};
|
||||
|
||||
try (Cursor cursor = db.query(getTableName(), projection, query, args, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
byte[] raw = cursor.getBlob(cursor.getColumnIndexOrThrow(REACTIONS));
|
||||
|
||||
if (raw != null) {
|
||||
return Optional.of(ReactionList.parseFrom(raw));
|
||||
}
|
||||
}
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
Log.w(TAG, "[getRecipients] Failed to parse reaction list!", e);
|
||||
}
|
||||
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
private void setReactions(@NonNull SQLiteDatabase db, long messageId, @NonNull ReactionList reactionList) throws NoSuchMessageException {
|
||||
ContentValues values = new ContentValues();
|
||||
boolean isOutgoing = getMessageRecord(messageId).isOutgoing();
|
||||
boolean hasReactions = reactionList.getReactionsCount() != 0;
|
||||
|
||||
values.put(REACTIONS, reactionList.getReactionsList().isEmpty() ? null : reactionList.toByteArray());
|
||||
values.put(REACTIONS_UNREAD, hasReactions ? 1 : 0);
|
||||
|
||||
if (isOutgoing && hasReactions) {
|
||||
values.put(NOTIFIED, 0);
|
||||
}
|
||||
|
||||
String query = ID + " = ?";
|
||||
String[] args = new String[] { String.valueOf(messageId) };
|
||||
|
||||
db.update(getTableName(), values, query, args);
|
||||
}
|
||||
|
||||
private long getThreadId(@NonNull SQLiteDatabase db, long messageId) {
|
||||
String[] projection = new String[]{ THREAD_ID };
|
||||
String query = ID + " = ?";
|
||||
|
||||
@@ -163,7 +163,6 @@ public class MmsDatabase extends MessageDatabase {
|
||||
UNIDENTIFIED + " INTEGER DEFAULT 0, " +
|
||||
LINK_PREVIEWS + " TEXT, " +
|
||||
VIEW_ONCE + " INTEGER DEFAULT 0, " +
|
||||
REACTIONS + " BLOB DEFAULT NULL, " +
|
||||
REACTIONS_UNREAD + " INTEGER DEFAULT 0, " +
|
||||
REACTIONS_LAST_SEEN + " INTEGER DEFAULT -1, " +
|
||||
REMOTE_DELETED + " INTEGER DEFAULT 0, " +
|
||||
@@ -193,7 +192,7 @@ public class MmsDatabase extends MessageDatabase {
|
||||
BODY, PART_COUNT, RECIPIENT_ID, ADDRESS_DEVICE_ID,
|
||||
DELIVERY_RECEIPT_COUNT, READ_RECEIPT_COUNT, MISMATCHED_IDENTITIES, NETWORK_FAILURE, SUBSCRIPTION_ID,
|
||||
EXPIRES_IN, EXPIRE_STARTED, NOTIFIED, QUOTE_ID, QUOTE_AUTHOR, QUOTE_BODY, QUOTE_ATTACHMENT, QUOTE_MISSING, QUOTE_MENTIONS,
|
||||
SHARED_CONTACTS, LINK_PREVIEWS, UNIDENTIFIED, VIEW_ONCE, REACTIONS, REACTIONS_UNREAD, REACTIONS_LAST_SEEN,
|
||||
SHARED_CONTACTS, LINK_PREVIEWS, UNIDENTIFIED, VIEW_ONCE, REACTIONS_UNREAD, REACTIONS_LAST_SEEN,
|
||||
REMOTE_DELETED, MENTIONS_SELF, NOTIFIED_TIMESTAMP, VIEWED_RECEIPT_COUNT, RECEIPT_TIMESTAMP,
|
||||
"json_group_array(json_object(" +
|
||||
"'" + AttachmentDatabase.ROW_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + ", " +
|
||||
@@ -855,7 +854,6 @@ public class MmsDatabase extends MessageDatabase {
|
||||
values.putNull(QUOTE_ID);
|
||||
values.putNull(LINK_PREVIEWS);
|
||||
values.putNull(SHARED_CONTACTS);
|
||||
values.putNull(REACTIONS);
|
||||
db.update(TABLE_NAME, values, ID_WHERE, new String[] { String.valueOf(messageId) });
|
||||
|
||||
DatabaseFactory.getAttachmentDatabase(context).deleteAttachmentsForMessage(messageId);
|
||||
@@ -2099,7 +2097,6 @@ public class MmsDatabase extends MessageDatabase {
|
||||
boolean unidentified = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.UNIDENTIFIED)) == 1;
|
||||
boolean isViewOnce = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.VIEW_ONCE)) == 1;
|
||||
boolean remoteDelete = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.REMOTE_DELETED)) == 1;
|
||||
List<ReactionRecord> reactions = parseReactions(cursor);
|
||||
boolean mentionsSelf = CursorUtil.requireBoolean(cursor, MENTIONS_SELF);
|
||||
long notifiedTimestamp = CursorUtil.requireLong(cursor, NOTIFIED_TIMESTAMP);
|
||||
int viewedReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.VIEWED_RECEIPT_COUNT));
|
||||
@@ -2128,7 +2125,7 @@ public class MmsDatabase extends MessageDatabase {
|
||||
addressDeviceId, dateSent, dateReceived, dateServer, deliveryReceiptCount,
|
||||
threadId, body, slideDeck, partCount, box, mismatches,
|
||||
networkFailures, subscriptionId, expiresIn, expireStarted,
|
||||
isViewOnce, readReceiptCount, quote, contacts, previews, unidentified, reactions,
|
||||
isViewOnce, readReceiptCount, quote, contacts, previews, unidentified, Collections.emptyList(),
|
||||
remoteDelete, mentionsSelf, notifiedTimestamp, viewedReceiptCount, receiptTimestamp);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ public interface MmsSmsColumns {
|
||||
public static final String NOTIFIED = "notified";
|
||||
public static final String NOTIFIED_TIMESTAMP = "notified_timestamp";
|
||||
public static final String UNIDENTIFIED = "unidentified";
|
||||
public static final String REACTIONS = "reactions";
|
||||
public static final String REACTIONS_UNREAD = "reactions_unread";
|
||||
public static final String REACTIONS_LAST_SEEN = "reactions_last_seen";
|
||||
public static final String REMOTE_DELETED = "remote_deleted";
|
||||
|
||||
@@ -74,7 +74,6 @@ public class MmsSmsDatabase extends Database {
|
||||
MmsDatabase.MESSAGE_BOX,
|
||||
SmsDatabase.STATUS,
|
||||
MmsSmsColumns.UNIDENTIFIED,
|
||||
MmsSmsColumns.REACTIONS,
|
||||
MmsDatabase.PART_COUNT,
|
||||
MmsDatabase.CONTENT_LOCATION,
|
||||
MmsDatabase.TRANSACTION_ID,
|
||||
@@ -101,7 +100,6 @@ public class MmsSmsDatabase extends Database {
|
||||
MmsDatabase.LINK_PREVIEWS,
|
||||
MmsDatabase.VIEW_ONCE,
|
||||
MmsSmsColumns.READ,
|
||||
MmsSmsColumns.REACTIONS,
|
||||
MmsSmsColumns.REACTIONS_UNREAD,
|
||||
MmsSmsColumns.REACTIONS_LAST_SEEN,
|
||||
MmsSmsColumns.REMOTE_DELETED,
|
||||
@@ -678,7 +676,6 @@ public class MmsSmsDatabase extends Database {
|
||||
MmsDatabase.SHARED_CONTACTS,
|
||||
MmsDatabase.LINK_PREVIEWS,
|
||||
MmsDatabase.VIEW_ONCE,
|
||||
MmsDatabase.REACTIONS,
|
||||
MmsSmsColumns.REACTIONS_UNREAD,
|
||||
MmsSmsColumns.REACTIONS_LAST_SEEN,
|
||||
MmsSmsColumns.DATE_SERVER,
|
||||
@@ -712,7 +709,6 @@ public class MmsSmsDatabase extends Database {
|
||||
MmsDatabase.SHARED_CONTACTS,
|
||||
MmsDatabase.LINK_PREVIEWS,
|
||||
MmsDatabase.VIEW_ONCE,
|
||||
MmsDatabase.REACTIONS,
|
||||
MmsSmsColumns.REACTIONS_UNREAD,
|
||||
MmsSmsColumns.REACTIONS_LAST_SEEN,
|
||||
MmsSmsColumns.DATE_SERVER,
|
||||
@@ -775,7 +771,6 @@ public class MmsSmsDatabase extends Database {
|
||||
mmsColumnsPresent.add(MmsDatabase.SHARED_CONTACTS);
|
||||
mmsColumnsPresent.add(MmsDatabase.LINK_PREVIEWS);
|
||||
mmsColumnsPresent.add(MmsDatabase.VIEW_ONCE);
|
||||
mmsColumnsPresent.add(MmsDatabase.REACTIONS);
|
||||
mmsColumnsPresent.add(MmsDatabase.REACTIONS_UNREAD);
|
||||
mmsColumnsPresent.add(MmsDatabase.REACTIONS_LAST_SEEN);
|
||||
mmsColumnsPresent.add(MmsDatabase.REMOTE_DELETED);
|
||||
@@ -805,7 +800,6 @@ public class MmsSmsDatabase extends Database {
|
||||
smsColumnsPresent.add(SmsDatabase.DATE_SERVER);
|
||||
smsColumnsPresent.add(SmsDatabase.STATUS);
|
||||
smsColumnsPresent.add(SmsDatabase.UNIDENTIFIED);
|
||||
smsColumnsPresent.add(SmsDatabase.REACTIONS);
|
||||
smsColumnsPresent.add(SmsDatabase.REACTIONS_UNREAD);
|
||||
smsColumnsPresent.add(SmsDatabase.REACTIONS_LAST_SEEN);
|
||||
smsColumnsPresent.add(MmsDatabase.REMOTE_DELETED);
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||
import org.thoughtcrime.securesms.database.model.MessageId
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.CursorUtil
|
||||
import org.thoughtcrime.securesms.util.SqlUtil
|
||||
|
||||
/**
|
||||
* Store reactions on messages.
|
||||
*/
|
||||
class ReactionDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Database(context, databaseHelper) {
|
||||
|
||||
companion object {
|
||||
private const val TABLE_NAME = "reaction"
|
||||
|
||||
private const val ID = "_id"
|
||||
private const val MESSAGE_ID = "message_id"
|
||||
private const val IS_MMS = "is_mms"
|
||||
private const val AUTHOR_ID = "author_id"
|
||||
private const val EMOJI = "emoji"
|
||||
private const val DATE_SENT = "date_sent"
|
||||
private const val DATE_RECEIVED = "date_received"
|
||||
|
||||
@JvmField
|
||||
val CREATE_TABLE = """
|
||||
CREATE TABLE $TABLE_NAME (
|
||||
$ID INTEGER PRIMARY KEY,
|
||||
$MESSAGE_ID INTEGER NOT NULL,
|
||||
$IS_MMS INTEGER NOT NULL,
|
||||
$AUTHOR_ID INTEGER NOT NULL REFERENCES ${RecipientDatabase.TABLE_NAME} (${RecipientDatabase.ID}) ON DELETE CASCADE,
|
||||
$EMOJI TEXT NOT NULL,
|
||||
$DATE_SENT INTEGER NOT NULL,
|
||||
$DATE_RECEIVED INTEGER NOT NULL,
|
||||
UNIQUE($MESSAGE_ID, $IS_MMS, $AUTHOR_ID) ON CONFLICT REPLACE
|
||||
)
|
||||
""".trimIndent()
|
||||
|
||||
@JvmField
|
||||
val CREATE_TRIGGERS = arrayOf(
|
||||
"""
|
||||
CREATE TRIGGER reactions_sms_delete AFTER DELETE ON ${SmsDatabase.TABLE_NAME}
|
||||
BEGIN
|
||||
DELETE FROM $TABLE_NAME WHERE $MESSAGE_ID = old.${SmsDatabase.ID} AND $IS_MMS = 0;
|
||||
END
|
||||
""",
|
||||
"""
|
||||
CREATE TRIGGER reactions_mms_delete AFTER DELETE ON ${MmsDatabase.TABLE_NAME}
|
||||
BEGIN
|
||||
DELETE FROM $TABLE_NAME WHERE $MESSAGE_ID = old.${MmsDatabase.ID} AND $IS_MMS = 1;
|
||||
END
|
||||
"""
|
||||
)
|
||||
|
||||
private fun readReaction(cursor: Cursor): ReactionRecord {
|
||||
return ReactionRecord(
|
||||
emoji = CursorUtil.requireString(cursor, EMOJI),
|
||||
author = RecipientId.from(CursorUtil.requireLong(cursor, AUTHOR_ID)),
|
||||
dateSent = CursorUtil.requireLong(cursor, DATE_SENT),
|
||||
dateReceived = CursorUtil.requireLong(cursor, DATE_RECEIVED)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun getReactions(messageId: MessageId): List<ReactionRecord> {
|
||||
val query = "$MESSAGE_ID = ? AND $IS_MMS = ?"
|
||||
val args = SqlUtil.buildArgs(messageId.id, if (messageId.mms) 1 else 0)
|
||||
|
||||
val reactions: MutableList<ReactionRecord> = mutableListOf()
|
||||
|
||||
databaseHelper.signalReadableDatabase.query(TABLE_NAME, null, query, args, null, null, null).use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
reactions += readReaction(cursor)
|
||||
}
|
||||
}
|
||||
|
||||
return reactions
|
||||
}
|
||||
|
||||
fun getReactionsForMessages(messageIds: Collection<MessageId>): Map<MessageId, List<ReactionRecord>> {
|
||||
if (messageIds.isEmpty()) {
|
||||
return emptyMap()
|
||||
}
|
||||
|
||||
val messageIdToReactions: MutableMap<MessageId, MutableList<ReactionRecord>> = mutableMapOf()
|
||||
|
||||
val args: List<Array<String>> = messageIds.map { SqlUtil.buildArgs(it.id, if (it.mms) 1 else 0) }
|
||||
|
||||
for (query: SqlUtil.Query in SqlUtil.buildCustomCollectionQuery("$MESSAGE_ID = ? AND $IS_MMS = ?", args)) {
|
||||
databaseHelper.signalReadableDatabase.query(TABLE_NAME, null, query.where, query.whereArgs, null, null, null).use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
val reaction: ReactionRecord = readReaction(cursor)
|
||||
val messageId = MessageId(
|
||||
id = CursorUtil.requireLong(cursor, MESSAGE_ID),
|
||||
mms = CursorUtil.requireBoolean(cursor, IS_MMS)
|
||||
)
|
||||
|
||||
var reactionsList: MutableList<ReactionRecord>? = messageIdToReactions[messageId]
|
||||
|
||||
if (reactionsList == null) {
|
||||
reactionsList = mutableListOf()
|
||||
messageIdToReactions[messageId] = reactionsList
|
||||
}
|
||||
|
||||
reactionsList.add(reaction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return messageIdToReactions
|
||||
}
|
||||
|
||||
fun addReaction(messageId: MessageId, reaction: ReactionRecord) {
|
||||
val db: SQLiteDatabase = databaseHelper.signalWritableDatabase
|
||||
|
||||
db.beginTransaction()
|
||||
try {
|
||||
val values = ContentValues().apply {
|
||||
put(MESSAGE_ID, messageId.id)
|
||||
put(IS_MMS, if (messageId.mms) 1 else 0)
|
||||
put(EMOJI, reaction.emoji)
|
||||
put(AUTHOR_ID, reaction.author.serialize())
|
||||
put(DATE_SENT, reaction.dateSent)
|
||||
put(DATE_RECEIVED, reaction.dateReceived)
|
||||
}
|
||||
|
||||
db.insert(TABLE_NAME, null, values)
|
||||
|
||||
if (messageId.mms) {
|
||||
DatabaseFactory.getMmsDatabase(context).updateReactionsUnread(db, messageId.id, hasReactions(messageId), false)
|
||||
} else {
|
||||
DatabaseFactory.getSmsDatabase(context).updateReactionsUnread(db, messageId.id, hasReactions(messageId), false)
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful()
|
||||
} finally {
|
||||
db.endTransaction()
|
||||
}
|
||||
|
||||
ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(messageId)
|
||||
}
|
||||
|
||||
fun deleteReaction(messageId: MessageId, recipientId: RecipientId) {
|
||||
val db: SQLiteDatabase = databaseHelper.signalWritableDatabase
|
||||
|
||||
db.beginTransaction()
|
||||
try {
|
||||
val query = "$MESSAGE_ID = ? AND $IS_MMS = ? AND $AUTHOR_ID = ?"
|
||||
val args = SqlUtil.buildArgs(messageId.id, if (messageId.mms) 1 else 0, recipientId)
|
||||
|
||||
db.delete(TABLE_NAME, query, args)
|
||||
|
||||
if (messageId.mms) {
|
||||
DatabaseFactory.getMmsDatabase(context).updateReactionsUnread(db, messageId.id, hasReactions(messageId), true)
|
||||
} else {
|
||||
DatabaseFactory.getSmsDatabase(context).updateReactionsUnread(db, messageId.id, hasReactions(messageId), true)
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful()
|
||||
} finally {
|
||||
db.endTransaction()
|
||||
}
|
||||
|
||||
ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(messageId)
|
||||
}
|
||||
|
||||
fun hasReaction(messageId: MessageId, reaction: ReactionRecord): Boolean {
|
||||
val query = "$MESSAGE_ID = ? AND $IS_MMS = ? AND $AUTHOR_ID = ? AND $EMOJI = ?"
|
||||
val args = SqlUtil.buildArgs(messageId.id, if (messageId.mms) 1 else 0, reaction.author, reaction.emoji)
|
||||
|
||||
databaseHelper.signalReadableDatabase.query(TABLE_NAME, arrayOf(MESSAGE_ID), query, args, null, null, null).use { cursor ->
|
||||
return cursor.moveToFirst()
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasReactions(messageId: MessageId): Boolean {
|
||||
val query = "$MESSAGE_ID = ? AND $IS_MMS = ?"
|
||||
val args = SqlUtil.buildArgs(messageId.id, if (messageId.mms) 1 else 0)
|
||||
|
||||
databaseHelper.signalReadableDatabase.query(TABLE_NAME, arrayOf(MESSAGE_ID), query, args, null, null, null).use { cursor ->
|
||||
return cursor.moveToFirst()
|
||||
}
|
||||
}
|
||||
|
||||
fun remapRecipient(oldAuthorId: RecipientId, newAuthorId: RecipientId) {
|
||||
val query = "$AUTHOR_ID = ?"
|
||||
val args = SqlUtil.buildArgs(oldAuthorId)
|
||||
val values = ContentValues().apply {
|
||||
put(AUTHOR_ID, newAuthorId.serialize())
|
||||
}
|
||||
|
||||
databaseHelper.signalWritableDatabase.update(TABLE_NAME, values, query, args)
|
||||
}
|
||||
}
|
||||
@@ -3019,43 +3019,6 @@ public class RecipientDatabase extends Database {
|
||||
RecipientSettings uuidSettings = getRecipientSettings(byUuid);
|
||||
RecipientSettings e164Settings = getRecipientSettings(byE164);
|
||||
|
||||
// Recipient
|
||||
Log.w(TAG, "Deleting recipient " + byE164, true);
|
||||
db.delete(TABLE_NAME, ID_WHERE, SqlUtil.buildArgs(byE164));
|
||||
RemappedRecords.getInstance().addRecipient(context, byE164, byUuid);
|
||||
|
||||
ContentValues uuidValues = new ContentValues();
|
||||
uuidValues.put(PHONE, e164Settings.getE164());
|
||||
uuidValues.put(BLOCKED, e164Settings.isBlocked() || uuidSettings.isBlocked());
|
||||
uuidValues.put(MESSAGE_RINGTONE, Optional.fromNullable(uuidSettings.getMessageRingtone()).or(Optional.fromNullable(e164Settings.getMessageRingtone())).transform(Uri::toString).orNull());
|
||||
uuidValues.put(MESSAGE_VIBRATE, uuidSettings.getMessageVibrateState() != VibrateState.DEFAULT ? uuidSettings.getMessageVibrateState().getId() : e164Settings.getMessageVibrateState().getId());
|
||||
uuidValues.put(CALL_RINGTONE, Optional.fromNullable(uuidSettings.getCallRingtone()).or(Optional.fromNullable(e164Settings.getCallRingtone())).transform(Uri::toString).orNull());
|
||||
uuidValues.put(CALL_VIBRATE, uuidSettings.getCallVibrateState() != VibrateState.DEFAULT ? uuidSettings.getCallVibrateState().getId() : e164Settings.getCallVibrateState().getId());
|
||||
uuidValues.put(NOTIFICATION_CHANNEL, uuidSettings.getNotificationChannel() != null ? uuidSettings.getNotificationChannel() : e164Settings.getNotificationChannel());
|
||||
uuidValues.put(MUTE_UNTIL, uuidSettings.getMuteUntil() > 0 ? uuidSettings.getMuteUntil() : e164Settings.getMuteUntil());
|
||||
uuidValues.put(CHAT_COLORS, Optional.fromNullable(uuidSettings.getChatColors()).or(Optional.fromNullable(e164Settings.getChatColors())).transform(colors -> colors.serialize().toByteArray()).orNull());
|
||||
uuidValues.put(AVATAR_COLOR, uuidSettings.getAvatarColor().serialize());
|
||||
uuidValues.put(CUSTOM_CHAT_COLORS_ID, Optional.fromNullable(uuidSettings.getChatColors()).or(Optional.fromNullable(e164Settings.getChatColors())).transform(colors -> colors.getId().getLongValue()).orNull());
|
||||
uuidValues.put(SEEN_INVITE_REMINDER, e164Settings.getInsightsBannerTier().getId());
|
||||
uuidValues.put(DEFAULT_SUBSCRIPTION_ID, e164Settings.getDefaultSubscriptionId().or(-1));
|
||||
uuidValues.put(MESSAGE_EXPIRATION_TIME, uuidSettings.getExpireMessages() > 0 ? uuidSettings.getExpireMessages() : e164Settings.getExpireMessages());
|
||||
uuidValues.put(REGISTERED, RegisteredState.REGISTERED.getId());
|
||||
uuidValues.put(SYSTEM_GIVEN_NAME, e164Settings.getSystemProfileName().getGivenName());
|
||||
uuidValues.put(SYSTEM_FAMILY_NAME, e164Settings.getSystemProfileName().getFamilyName());
|
||||
uuidValues.put(SYSTEM_JOINED_NAME, e164Settings.getSystemProfileName().toString());
|
||||
uuidValues.put(SYSTEM_PHOTO_URI, e164Settings.getSystemContactPhotoUri());
|
||||
uuidValues.put(SYSTEM_PHONE_LABEL, e164Settings.getSystemPhoneLabel());
|
||||
uuidValues.put(SYSTEM_CONTACT_URI, e164Settings.getSystemContactUri());
|
||||
uuidValues.put(PROFILE_SHARING, uuidSettings.isProfileSharing() || e164Settings.isProfileSharing());
|
||||
uuidValues.put(CAPABILITIES, Math.max(uuidSettings.getCapabilities(), e164Settings.getCapabilities()));
|
||||
uuidValues.put(MENTION_SETTING, uuidSettings.getMentionSetting() != MentionSetting.ALWAYS_NOTIFY ? uuidSettings.getMentionSetting().getId() : e164Settings.getMentionSetting().getId());
|
||||
if (uuidSettings.getProfileKey() != null) {
|
||||
updateProfileValuesForMerge(uuidValues, uuidSettings);
|
||||
} else if (e164Settings.getProfileKey() != null) {
|
||||
updateProfileValuesForMerge(uuidValues, e164Settings);
|
||||
}
|
||||
db.update(TABLE_NAME, uuidValues, ID_WHERE, SqlUtil.buildArgs(byUuid));
|
||||
|
||||
// Identities
|
||||
ApplicationDependencies.getIdentityStore().delete(e164Settings.e164);
|
||||
|
||||
@@ -3141,6 +3104,46 @@ public class RecipientDatabase extends Database {
|
||||
DatabaseFactory.getThreadDatabase(context).setLastScrolled(threadMerge.threadId, 0);
|
||||
DatabaseFactory.getThreadDatabase(context).update(threadMerge.threadId, false, false);
|
||||
|
||||
// Reactions
|
||||
DatabaseFactory.getReactionDatabase(context).remapRecipient(byE164, byUuid);
|
||||
|
||||
// Recipient
|
||||
Log.w(TAG, "Deleting recipient " + byE164, true);
|
||||
db.delete(TABLE_NAME, ID_WHERE, SqlUtil.buildArgs(byE164));
|
||||
RemappedRecords.getInstance().addRecipient(context, byE164, byUuid);
|
||||
|
||||
ContentValues uuidValues = new ContentValues();
|
||||
uuidValues.put(PHONE, e164Settings.getE164());
|
||||
uuidValues.put(BLOCKED, e164Settings.isBlocked() || uuidSettings.isBlocked());
|
||||
uuidValues.put(MESSAGE_RINGTONE, Optional.fromNullable(uuidSettings.getMessageRingtone()).or(Optional.fromNullable(e164Settings.getMessageRingtone())).transform(Uri::toString).orNull());
|
||||
uuidValues.put(MESSAGE_VIBRATE, uuidSettings.getMessageVibrateState() != VibrateState.DEFAULT ? uuidSettings.getMessageVibrateState().getId() : e164Settings.getMessageVibrateState().getId());
|
||||
uuidValues.put(CALL_RINGTONE, Optional.fromNullable(uuidSettings.getCallRingtone()).or(Optional.fromNullable(e164Settings.getCallRingtone())).transform(Uri::toString).orNull());
|
||||
uuidValues.put(CALL_VIBRATE, uuidSettings.getCallVibrateState() != VibrateState.DEFAULT ? uuidSettings.getCallVibrateState().getId() : e164Settings.getCallVibrateState().getId());
|
||||
uuidValues.put(NOTIFICATION_CHANNEL, uuidSettings.getNotificationChannel() != null ? uuidSettings.getNotificationChannel() : e164Settings.getNotificationChannel());
|
||||
uuidValues.put(MUTE_UNTIL, uuidSettings.getMuteUntil() > 0 ? uuidSettings.getMuteUntil() : e164Settings.getMuteUntil());
|
||||
uuidValues.put(CHAT_COLORS, Optional.fromNullable(uuidSettings.getChatColors()).or(Optional.fromNullable(e164Settings.getChatColors())).transform(colors -> colors.serialize().toByteArray()).orNull());
|
||||
uuidValues.put(AVATAR_COLOR, uuidSettings.getAvatarColor().serialize());
|
||||
uuidValues.put(CUSTOM_CHAT_COLORS_ID, Optional.fromNullable(uuidSettings.getChatColors()).or(Optional.fromNullable(e164Settings.getChatColors())).transform(colors -> colors.getId().getLongValue()).orNull());
|
||||
uuidValues.put(SEEN_INVITE_REMINDER, e164Settings.getInsightsBannerTier().getId());
|
||||
uuidValues.put(DEFAULT_SUBSCRIPTION_ID, e164Settings.getDefaultSubscriptionId().or(-1));
|
||||
uuidValues.put(MESSAGE_EXPIRATION_TIME, uuidSettings.getExpireMessages() > 0 ? uuidSettings.getExpireMessages() : e164Settings.getExpireMessages());
|
||||
uuidValues.put(REGISTERED, RegisteredState.REGISTERED.getId());
|
||||
uuidValues.put(SYSTEM_GIVEN_NAME, e164Settings.getSystemProfileName().getGivenName());
|
||||
uuidValues.put(SYSTEM_FAMILY_NAME, e164Settings.getSystemProfileName().getFamilyName());
|
||||
uuidValues.put(SYSTEM_JOINED_NAME, e164Settings.getSystemProfileName().toString());
|
||||
uuidValues.put(SYSTEM_PHOTO_URI, e164Settings.getSystemContactPhotoUri());
|
||||
uuidValues.put(SYSTEM_PHONE_LABEL, e164Settings.getSystemPhoneLabel());
|
||||
uuidValues.put(SYSTEM_CONTACT_URI, e164Settings.getSystemContactUri());
|
||||
uuidValues.put(PROFILE_SHARING, uuidSettings.isProfileSharing() || e164Settings.isProfileSharing());
|
||||
uuidValues.put(CAPABILITIES, Math.max(uuidSettings.getCapabilities(), e164Settings.getCapabilities()));
|
||||
uuidValues.put(MENTION_SETTING, uuidSettings.getMentionSetting() != MentionSetting.ALWAYS_NOTIFY ? uuidSettings.getMentionSetting().getId() : e164Settings.getMentionSetting().getId());
|
||||
if (uuidSettings.getProfileKey() != null) {
|
||||
updateProfileValuesForMerge(uuidValues, uuidSettings);
|
||||
} else if (e164Settings.getProfileKey() != null) {
|
||||
updateProfileValuesForMerge(uuidValues, e164Settings);
|
||||
}
|
||||
db.update(TABLE_NAME, uuidValues, ID_WHERE, SqlUtil.buildArgs(byUuid));
|
||||
|
||||
return byUuid;
|
||||
}
|
||||
|
||||
|
||||
@@ -123,7 +123,6 @@ public class SmsDatabase extends MessageDatabase {
|
||||
NOTIFIED + " DEFAULT 0, " +
|
||||
READ_RECEIPT_COUNT + " INTEGER DEFAULT 0, " +
|
||||
UNIDENTIFIED + " INTEGER DEFAULT 0, " +
|
||||
REACTIONS + " BLOB DEFAULT NULL, " +
|
||||
REACTIONS_UNREAD + " INTEGER DEFAULT 0, " +
|
||||
REACTIONS_LAST_SEEN + " INTEGER DEFAULT -1, " +
|
||||
REMOTE_DELETED + " INTEGER DEFAULT 0, " +
|
||||
@@ -148,7 +147,7 @@ public class SmsDatabase extends MessageDatabase {
|
||||
PROTOCOL, READ, STATUS, TYPE,
|
||||
REPLY_PATH_PRESENT, SUBJECT, BODY, SERVICE_CENTER, DELIVERY_RECEIPT_COUNT,
|
||||
MISMATCHED_IDENTITIES, SUBSCRIPTION_ID, EXPIRES_IN, EXPIRE_STARTED,
|
||||
NOTIFIED, READ_RECEIPT_COUNT, UNIDENTIFIED, REACTIONS, REACTIONS_UNREAD, REACTIONS_LAST_SEEN,
|
||||
NOTIFIED, READ_RECEIPT_COUNT, UNIDENTIFIED, REACTIONS_UNREAD, REACTIONS_LAST_SEEN,
|
||||
REMOTE_DELETED, NOTIFIED_TIMESTAMP, RECEIPT_TIMESTAMP
|
||||
};
|
||||
|
||||
@@ -391,7 +390,6 @@ public class SmsDatabase extends MessageDatabase {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(REMOTE_DELETED, 1);
|
||||
values.putNull(BODY);
|
||||
values.putNull(REACTIONS);
|
||||
db.update(TABLE_NAME, values, ID_WHERE, new String[] { String.valueOf(id) });
|
||||
|
||||
threadId = getThreadIdForMessage(id);
|
||||
@@ -1696,7 +1694,6 @@ public class SmsDatabase extends MessageDatabase {
|
||||
String body = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.BODY));
|
||||
boolean unidentified = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.UNIDENTIFIED)) == 1;
|
||||
boolean remoteDelete = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.REMOTE_DELETED)) == 1;
|
||||
List<ReactionRecord> reactions = parseReactions(cursor);
|
||||
long notifiedTimestamp = CursorUtil.requireLong(cursor, NOTIFIED_TIMESTAMP);
|
||||
long receiptTimestamp = CursorUtil.requireLong(cursor, RECEIPT_TIMESTAMP);
|
||||
|
||||
@@ -1713,7 +1710,7 @@ public class SmsDatabase extends MessageDatabase {
|
||||
dateSent, dateReceived, dateServer, deliveryReceiptCount, type,
|
||||
threadId, status, mismatches, subscriptionId,
|
||||
expiresIn, expireStarted,
|
||||
readReceiptCount, unidentified, reactions, remoteDelete,
|
||||
readReceiptCount, unidentified, Collections.emptyList(), remoteDelete,
|
||||
notifiedTimestamp, receiptTimestamp);
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase;
|
||||
import org.thoughtcrime.securesms.database.PaymentDatabase;
|
||||
import org.thoughtcrime.securesms.database.PendingRetryReceiptDatabase;
|
||||
import org.thoughtcrime.securesms.database.PushDatabase;
|
||||
import org.thoughtcrime.securesms.database.ReactionDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.RemappedRecordsDatabase;
|
||||
import org.thoughtcrime.securesms.database.SearchDatabase;
|
||||
@@ -219,8 +220,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab
|
||||
private static final int BADGES = 118;
|
||||
private static final int SENDER_KEY_UUID = 119;
|
||||
private static final int SENDER_KEY_SHARED_TIMESTAMP = 120;
|
||||
private static final int REACTION_REFACTOR = 121;
|
||||
|
||||
private static final int DATABASE_VERSION = 120;
|
||||
private static final int DATABASE_VERSION = 121;
|
||||
private static final String DATABASE_NAME = "signal.db";
|
||||
|
||||
private final Context context;
|
||||
@@ -263,6 +265,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab
|
||||
db.execSQL(EmojiSearchDatabase.CREATE_TABLE);
|
||||
db.execSQL(AvatarPickerDatabase.CREATE_TABLE);
|
||||
db.execSQL(GroupCallRingDatabase.CREATE_TABLE);
|
||||
db.execSQL(ReactionDatabase.CREATE_TABLE);
|
||||
executeStatements(db, SearchDatabase.CREATE_TABLE);
|
||||
executeStatements(db, RemappedRecordsDatabase.CREATE_TABLE);
|
||||
executeStatements(db, MessageSendLogDatabase.CREATE_TABLE);
|
||||
@@ -283,6 +286,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab
|
||||
executeStatements(db, GroupCallRingDatabase.CREATE_INDEXES);
|
||||
|
||||
executeStatements(db, MessageSendLogDatabase.CREATE_TRIGGERS);
|
||||
executeStatements(db, ReactionDatabase.CREATE_TRIGGERS);
|
||||
|
||||
if (context.getDatabasePath(ClassicOpenHelper.NAME).exists()) {
|
||||
ClassicOpenHelper legacyHelper = new ClassicOpenHelper(context);
|
||||
@@ -2054,12 +2058,12 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
db.execSQL("CREATE TABLE sender_keys_tmp (_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
"address TEXT NOT NULL, " +
|
||||
"device INTEGER NOT NULL, " +
|
||||
"distribution_id TEXT NOT NULL, " +
|
||||
"record BLOB NOT NULL, " +
|
||||
"created_at INTEGER NOT NULL, " +
|
||||
"UNIQUE(address, device, distribution_id) ON CONFLICT REPLACE)");
|
||||
"address TEXT NOT NULL, " +
|
||||
"device INTEGER NOT NULL, " +
|
||||
"distribution_id TEXT NOT NULL, " +
|
||||
"record BLOB NOT NULL, " +
|
||||
"created_at INTEGER NOT NULL, " +
|
||||
"UNIQUE(address, device, distribution_id) ON CONFLICT REPLACE)");
|
||||
|
||||
db.execSQL("INSERT INTO sender_keys_tmp (address, device, distribution_id, record, created_at) " +
|
||||
"SELECT recipient.uuid AS new_address, " +
|
||||
@@ -2080,6 +2084,37 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab
|
||||
db.execSQL("ALTER TABLE sender_key_shared ADD COLUMN timestamp INTEGER DEFAULT 0");
|
||||
}
|
||||
|
||||
if (oldVersion < REACTION_REFACTOR) {
|
||||
db.execSQL("CREATE TABLE reaction (_id INTEGER PRIMARY KEY, " +
|
||||
"message_id INTEGER NOT NULL, " +
|
||||
"is_mms INTEGER NOT NULL, " +
|
||||
"author_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE, " +
|
||||
"emoji TEXT NOT NULL, " +
|
||||
"date_sent INTEGER NOT NULL, " +
|
||||
"date_received INTEGER NOT NULL, " +
|
||||
"UNIQUE(message_id, is_mms, author_id) ON CONFLICT REPLACE)");
|
||||
|
||||
try (Cursor cursor = db.rawQuery("SELECT _id, reactions FROM sms WHERE reactions NOT NULL", null)) {
|
||||
while (cursor.moveToNext()) {
|
||||
migrateReaction(db, cursor, false);
|
||||
}
|
||||
}
|
||||
|
||||
try (Cursor cursor = db.rawQuery("SELECT _id, reactions FROM mms WHERE reactions NOT NULL", null)) {
|
||||
while (cursor.moveToNext()) {
|
||||
migrateReaction(db, cursor, true);
|
||||
}
|
||||
}
|
||||
|
||||
db.execSQL("UPDATE reaction SET author_id = IFNULL((SELECT new_id FROM remapped_recipients WHERE author_id = old_id), author_id)");
|
||||
|
||||
db.execSQL("CREATE TRIGGER reactions_sms_delete AFTER DELETE ON sms BEGIN DELETE FROM reaction WHERE message_id = old._id AND is_mms = 0; END");
|
||||
db.execSQL("CREATE TRIGGER reactions_mms_delete AFTER DELETE ON mms BEGIN DELETE FROM reaction WHERE message_id = old._id AND is_mms = 0; END");
|
||||
|
||||
db.execSQL("UPDATE sms SET reactions = NULL WHERE reactions NOT NULL");
|
||||
db.execSQL("UPDATE mms SET reactions = NULL WHERE reactions NOT NULL");
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
@@ -2092,6 +2127,27 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab
|
||||
Log.i(TAG, "Upgrade complete. Took " + (System.currentTimeMillis() - startTime) + " ms.");
|
||||
}
|
||||
|
||||
private void migrateReaction(@NonNull SQLiteDatabase db, @NonNull Cursor cursor, boolean isMms) {
|
||||
try {
|
||||
long messageId = CursorUtil.requireLong(cursor, "_id");
|
||||
ReactionList reactionList = ReactionList.parseFrom(CursorUtil.requireBlob(cursor, "reactions"));
|
||||
|
||||
for (ReactionList.Reaction reaction : reactionList.getReactionsList()) {
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put("message_id", messageId);
|
||||
contentValues.put("is_mms", isMms ? 1 : 0);
|
||||
contentValues.put("author_id", reaction.getAuthor());
|
||||
contentValues.put("emoji", reaction.getEmoji());
|
||||
contentValues.put("date_sent", reaction.getSentTime());
|
||||
contentValues.put("date_received", reaction.getReceivedTime());
|
||||
|
||||
db.insert("reaction", null, contentValues);
|
||||
}
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
Log.w(TAG, "Failed to parse reaction!");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public net.zetetic.database.sqlcipher.SQLiteDatabase getReadableDatabase() {
|
||||
throw new UnsupportedOperationException("Call getSignalReadableDatabase() instead!");
|
||||
@@ -2099,7 +2155,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab
|
||||
|
||||
@Override
|
||||
public net.zetetic.database.sqlcipher.SQLiteDatabase getWritableDatabase() {
|
||||
throw new UnsupportedOperationException("Call getSignalReadableDatabase() instead!");
|
||||
throw new UnsupportedOperationException("Call getSignalWritableDatabase() instead!");
|
||||
}
|
||||
|
||||
public net.zetetic.database.sqlcipher.SQLiteDatabase getRawReadableDatabase() {
|
||||
|
||||
@@ -128,6 +128,13 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
|
||||
return partCount;
|
||||
}
|
||||
|
||||
public @NonNull MediaMmsMessageRecord withReactions(@NonNull List<ReactionRecord> reactions) {
|
||||
return new MediaMmsMessageRecord(getId(), getRecipient(), getIndividualRecipient(), getRecipientDeviceId(), getDateSent(), getDateReceived(), getServerTimestamp(), getDeliveryReceiptCount(), getThreadId(), getBody(), getSlideDeck(),
|
||||
getPartCount(), getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(),
|
||||
getReadReceiptCount(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), reactions, isRemoteDelete(), mentionsSelf,
|
||||
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp());
|
||||
}
|
||||
|
||||
public @NonNull MediaMmsMessageRecord withAttachments(@NonNull Context context, @NonNull List<DatabaseAttachment> attachments) {
|
||||
Map<AttachmentId, DatabaseAttachment> attachmentIdMap = new HashMap<>();
|
||||
for (DatabaseAttachment attachment : attachments) {
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database.model;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class ReactionRecord {
|
||||
private final String emoji;
|
||||
private final RecipientId author;
|
||||
private final long dateSent;
|
||||
private final long dateReceived;
|
||||
|
||||
public ReactionRecord(@NonNull String emoji,
|
||||
@NonNull RecipientId author,
|
||||
long dateSent,
|
||||
long dateReceived)
|
||||
{
|
||||
this.emoji = emoji;
|
||||
this.author = author;
|
||||
this.dateSent = dateSent;
|
||||
this.dateReceived = dateReceived;
|
||||
}
|
||||
|
||||
public @NonNull String getEmoji() {
|
||||
return emoji;
|
||||
}
|
||||
|
||||
public @NonNull RecipientId getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
public long getDateSent() {
|
||||
return dateSent;
|
||||
}
|
||||
|
||||
public long getDateReceived() {
|
||||
return dateReceived;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ReactionRecord that = (ReactionRecord) o;
|
||||
return dateSent == that.dateSent &&
|
||||
dateReceived == that.dateReceived &&
|
||||
Objects.equals(emoji, that.emoji) &&
|
||||
Objects.equals(author, that.author);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(emoji, author, dateSent, dateReceived);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.thoughtcrime.securesms.database.model
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
/**
|
||||
* Represents an individual reaction to a message.
|
||||
*/
|
||||
data class ReactionRecord(
|
||||
val emoji: String,
|
||||
val author: RecipientId,
|
||||
val dateSent: Long,
|
||||
val dateReceived: Long
|
||||
)
|
||||
@@ -109,4 +109,11 @@ public class SmsMessageRecord extends MessageRecord {
|
||||
public boolean isMmsNotification() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public @NonNull SmsMessageRecord withReactions(@NonNull List<ReactionRecord> reactions) {
|
||||
return new SmsMessageRecord(getId(), getBody(), getRecipient(), getIndividualRecipient(), getRecipientDeviceId(), getDateSent(), getDateReceived(),
|
||||
getServerTimestamp(), getDeliveryReceiptCount(), getType(), getThreadId(), getDeliveryStatus(), getIdentityKeyMismatches(),
|
||||
getSubscriptionId(), getExpiresIn(), getExpireStarted(), getReadReceiptCount(), isUnidentified(), reactions, isRemoteDelete(),
|
||||
getNotifiedTimestamp(), getReceiptTimestamp());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase;
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
import org.thoughtcrime.securesms.database.ReactionDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageId;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
@@ -50,8 +51,7 @@ public class ReactionSendJob extends BaseJob {
|
||||
private static final String KEY_RECIPIENTS = "recipients";
|
||||
private static final String KEY_INITIAL_RECIPIENT_COUNT = "initial_recipient_count";
|
||||
|
||||
private final long messageId;
|
||||
private final boolean isMms;
|
||||
private final MessageId messageId;
|
||||
private final List<RecipientId> recipients;
|
||||
private final int initialRecipientCount;
|
||||
private final ReactionRecord reaction;
|
||||
@@ -60,14 +60,13 @@ public class ReactionSendJob extends BaseJob {
|
||||
|
||||
@WorkerThread
|
||||
public static @NonNull ReactionSendJob create(@NonNull Context context,
|
||||
long messageId,
|
||||
boolean isMms,
|
||||
@NonNull MessageId messageId,
|
||||
@NonNull ReactionRecord reaction,
|
||||
boolean remove)
|
||||
throws NoSuchMessageException
|
||||
{
|
||||
MessageRecord message = isMms ? DatabaseFactory.getMmsDatabase(context).getMessageRecord(messageId)
|
||||
: DatabaseFactory.getSmsDatabase(context).getSmsMessage(messageId);
|
||||
MessageRecord message = messageId.isMms() ? DatabaseFactory.getMmsDatabase(context).getMessageRecord(messageId.getId())
|
||||
: DatabaseFactory.getSmsDatabase(context).getSmsMessage(messageId.getId());
|
||||
|
||||
Recipient conversationRecipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(message.getThreadId());
|
||||
|
||||
@@ -84,7 +83,6 @@ public class ReactionSendJob extends BaseJob {
|
||||
: Collections.singletonList(conversationRecipient.getId());
|
||||
|
||||
return new ReactionSendJob(messageId,
|
||||
isMms,
|
||||
recipients,
|
||||
recipients.size(),
|
||||
reaction,
|
||||
@@ -96,8 +94,7 @@ public class ReactionSendJob extends BaseJob {
|
||||
.build());
|
||||
}
|
||||
|
||||
private ReactionSendJob(long messageId,
|
||||
boolean isMms,
|
||||
private ReactionSendJob(@NonNull MessageId messageId,
|
||||
@NonNull List<RecipientId> recipients,
|
||||
int initialRecipientCount,
|
||||
@NonNull ReactionRecord reaction,
|
||||
@@ -107,7 +104,6 @@ public class ReactionSendJob extends BaseJob {
|
||||
super(parameters);
|
||||
|
||||
this.messageId = messageId;
|
||||
this.isMms = isMms;
|
||||
this.recipients = recipients;
|
||||
this.initialRecipientCount = initialRecipientCount;
|
||||
this.reaction = reaction;
|
||||
@@ -116,8 +112,8 @@ public class ReactionSendJob extends BaseJob {
|
||||
|
||||
@Override
|
||||
public @NonNull Data serialize() {
|
||||
return new Data.Builder().putLong(KEY_MESSAGE_ID, messageId)
|
||||
.putBoolean(KEY_IS_MMS, isMms)
|
||||
return new Data.Builder().putLong(KEY_MESSAGE_ID, messageId.getId())
|
||||
.putBoolean(KEY_IS_MMS, messageId.isMms())
|
||||
.putString(KEY_REACTION_EMOJI, reaction.getEmoji())
|
||||
.putString(KEY_REACTION_AUTHOR, reaction.getAuthor().serialize())
|
||||
.putLong(KEY_REACTION_DATE_SENT, reaction.getDateSent())
|
||||
@@ -139,26 +135,25 @@ public class ReactionSendJob extends BaseJob {
|
||||
throw new NotPushRegisteredException();
|
||||
}
|
||||
|
||||
MessageDatabase db;
|
||||
MessageRecord message;
|
||||
ReactionDatabase reactionDatabase = DatabaseFactory.getReactionDatabase(context);
|
||||
|
||||
if (isMms) {
|
||||
db = DatabaseFactory.getMmsDatabase(context);
|
||||
message = DatabaseFactory.getMmsDatabase(context).getMessageRecord(messageId);
|
||||
MessageRecord message;
|
||||
|
||||
if (messageId.isMms()) {
|
||||
message = DatabaseFactory.getMmsDatabase(context).getMessageRecord(messageId.getId());
|
||||
} else {
|
||||
db = DatabaseFactory.getSmsDatabase(context);
|
||||
message = DatabaseFactory.getSmsDatabase(context).getSmsMessage(messageId);
|
||||
message = DatabaseFactory.getSmsDatabase(context).getSmsMessage(messageId.getId());
|
||||
}
|
||||
|
||||
Recipient targetAuthor = message.isOutgoing() ? Recipient.self() : message.getIndividualRecipient();
|
||||
long targetSentTimestamp = message.getDateSent();
|
||||
|
||||
if (!remove && !db.hasReaction(messageId, reaction)) {
|
||||
if (!remove && !reactionDatabase.hasReaction(messageId, reaction)) {
|
||||
Log.w(TAG, "Went to add a reaction, but it's no longer present on the message!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (remove && db.hasReaction(messageId, reaction)) {
|
||||
if (remove && reactionDatabase.hasReaction(messageId, reaction)) {
|
||||
Log.w(TAG, "Went to remove a reaction, but it's still there!");
|
||||
return;
|
||||
}
|
||||
@@ -207,14 +202,14 @@ public class ReactionSendJob extends BaseJob {
|
||||
|
||||
Log.w(TAG, "Failed to send the reaction to all recipients!");
|
||||
|
||||
MessageDatabase db = isMms ? DatabaseFactory.getMmsDatabase(context) : DatabaseFactory.getSmsDatabase(context);
|
||||
ReactionDatabase reactionDatabase = DatabaseFactory.getReactionDatabase(context);
|
||||
|
||||
if (remove && !db.hasReaction(messageId, reaction)) {
|
||||
if (remove && !reactionDatabase.hasReaction(messageId, reaction)) {
|
||||
Log.w(TAG, "Reaction removal failed, so adding the reaction back.");
|
||||
db.addReaction(messageId, reaction);
|
||||
} else if (!remove && db.hasReaction(messageId, reaction)){
|
||||
reactionDatabase.addReaction(messageId, reaction);
|
||||
} else if (!remove && reactionDatabase.hasReaction(messageId, reaction)){
|
||||
Log.w(TAG, "Reaction addition failed, so removing the reaction.");
|
||||
db.deleteReaction(messageId, reaction.getAuthor());
|
||||
reactionDatabase.deleteReaction(messageId, reaction.getAuthor());
|
||||
} else {
|
||||
Log.w(TAG, "Reaction state didn't match what we'd expect to revert it, so we're just leaving it alone.");
|
||||
}
|
||||
@@ -237,7 +232,7 @@ public class ReactionSendJob extends BaseJob {
|
||||
destinations,
|
||||
false,
|
||||
ContentHint.RESENDABLE,
|
||||
new MessageId(messageId, isMms),
|
||||
messageId,
|
||||
dataMessage);
|
||||
|
||||
return GroupSendJobHelper.getCompletedSends(destinations, results);
|
||||
@@ -271,7 +266,7 @@ public class ReactionSendJob extends BaseJob {
|
||||
data.getLong(KEY_REACTION_DATE_RECEIVED));
|
||||
boolean remove = data.getBoolean(KEY_REMOVE);
|
||||
|
||||
return new ReactionSendJob(messageId, isMms, recipients, initialRecipientCount, reaction, remove, parameters);
|
||||
return new ReactionSendJob(new MessageId(messageId, isMms), recipients, initialRecipientCount, reaction, remove, parameters);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -874,14 +874,14 @@ public final class MessageContentProcessor {
|
||||
return null;
|
||||
}
|
||||
|
||||
MessageDatabase db = targetMessage.isMms() ? DatabaseFactory.getMmsDatabase(context) : DatabaseFactory.getSmsDatabase(context);
|
||||
MessageId targetMessageId = new MessageId(targetMessage.getId(), targetMessage.isMms());
|
||||
|
||||
if (reaction.isRemove()) {
|
||||
db.deleteReaction(targetMessage.getId(), senderRecipient.getId());
|
||||
DatabaseFactory.getReactionDatabase(context).deleteReaction(targetMessageId, senderRecipient.getId());
|
||||
ApplicationDependencies.getMessageNotifier().updateNotification(context);
|
||||
} else {
|
||||
ReactionRecord reactionRecord = new ReactionRecord(reaction.getEmoji(), senderRecipient.getId(), message.getTimestamp(), System.currentTimeMillis());
|
||||
db.addReaction(targetMessage.getId(), reactionRecord);
|
||||
DatabaseFactory.getReactionDatabase(context).addReaction(targetMessageId, reactionRecord);
|
||||
ApplicationDependencies.getMessageNotifier().updateNotification(context, targetMessage.getThreadId(), false);
|
||||
}
|
||||
|
||||
|
||||
@@ -89,9 +89,10 @@ public class ApplicationMigrations {
|
||||
static final int CHANGE_NUMBER_CAPABILITY = 45;
|
||||
static final int CHANGE_NUMBER_CAPABILITY_2 = 46;
|
||||
static final int DEFAULT_REACTIONS_SYNC = 47;
|
||||
static final int DB_REACTIONS_MIGRATION = 48;
|
||||
}
|
||||
|
||||
public static final int CURRENT_VERSION = 47;
|
||||
public static final int CURRENT_VERSION = 48;
|
||||
|
||||
/**
|
||||
* This *must* be called after the {@link JobManager} has been instantiated, but *before* the call
|
||||
@@ -389,6 +390,10 @@ public class ApplicationMigrations {
|
||||
jobs.put(Version.DEFAULT_REACTIONS_SYNC, new StorageServiceMigrationJob());
|
||||
}
|
||||
|
||||
if (lastSeenVersion < Version.DB_REACTIONS_MIGRATION) {
|
||||
jobs.put(Version.DB_REACTIONS_MIGRATION, new DatabaseMigrationJob());
|
||||
}
|
||||
|
||||
return jobs;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase
|
||||
import org.thoughtcrime.securesms.database.model.MessageId
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
@@ -34,13 +35,16 @@ object NotificationStateProvider {
|
||||
while (record != null) {
|
||||
val threadRecipient: Recipient? = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(record.threadId)
|
||||
if (threadRecipient != null) {
|
||||
val hasUnreadReactions = CursorUtil.requireInt(unreadMessages, MmsSmsColumns.REACTIONS_UNREAD) == 1
|
||||
|
||||
messages += NotificationMessage(
|
||||
messageRecord = record,
|
||||
reactions = if (hasUnreadReactions) DatabaseFactory.getReactionDatabase(context).getReactions(MessageId(record.id, record.isMms)) else emptyList(),
|
||||
threadRecipient = threadRecipient,
|
||||
threadId = record.threadId,
|
||||
stickyThread = stickyThreads.containsKey(record.threadId),
|
||||
isUnreadMessage = CursorUtil.requireInt(unreadMessages, MmsSmsColumns.READ) == 0,
|
||||
hasUnreadReactions = CursorUtil.requireInt(unreadMessages, MmsSmsColumns.REACTIONS_UNREAD) == 1,
|
||||
hasUnreadReactions = hasUnreadReactions,
|
||||
lastReactionRead = CursorUtil.requireLong(unreadMessages, MmsSmsColumns.REACTIONS_LAST_SEEN)
|
||||
)
|
||||
}
|
||||
@@ -66,7 +70,7 @@ object NotificationStateProvider {
|
||||
}
|
||||
|
||||
if (notification.hasUnreadReactions) {
|
||||
notification.messageRecord.reactions.filter { notification.includeReaction(it) }
|
||||
notification.reactions.filter { notification.includeReaction(it) }
|
||||
.forEach { notificationItems.add(ReactionNotification(notification.threadRecipient, notification.messageRecord, it)) }
|
||||
}
|
||||
}
|
||||
@@ -87,6 +91,7 @@ object NotificationStateProvider {
|
||||
|
||||
private data class NotificationMessage(
|
||||
val messageRecord: MessageRecord,
|
||||
val reactions: List<ReactionRecord>,
|
||||
val threadRecipient: Recipient,
|
||||
val threadId: Long,
|
||||
val stickyThread: Boolean,
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
package org.thoughtcrime.securesms.reactions;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
public class ReactionDetails {
|
||||
private final Recipient sender;
|
||||
private final String baseEmoji;
|
||||
private final String displayEmoji;
|
||||
private final long timestamp;
|
||||
|
||||
ReactionDetails(@NonNull Recipient sender, @NonNull String baseEmoji, @NonNull String displayEmoji, long timestamp) {
|
||||
this.sender = sender;
|
||||
this.baseEmoji = baseEmoji;
|
||||
this.displayEmoji = displayEmoji;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public @NonNull Recipient getSender() {
|
||||
return sender;
|
||||
}
|
||||
|
||||
public @NonNull String getBaseEmoji() {
|
||||
return baseEmoji;
|
||||
}
|
||||
|
||||
public @NonNull String getDisplayEmoji() {
|
||||
return displayEmoji;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.thoughtcrime.securesms.reactions
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
/**
|
||||
* A UI model for a reaction in the [ReactionsBottomSheetDialogFragment]
|
||||
*/
|
||||
data class ReactionDetails(
|
||||
val sender: Recipient,
|
||||
val baseEmoji: String,
|
||||
val displayEmoji: String,
|
||||
val timestamp: Long
|
||||
)
|
||||
@@ -13,8 +13,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||
@@ -23,6 +22,8 @@ import com.google.android.material.tabs.TabLayoutMediator;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiImageView;
|
||||
import org.thoughtcrime.securesms.database.model.MessageId;
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
@@ -33,13 +34,13 @@ public final class ReactionsBottomSheetDialogFragment extends BottomSheetDialogF
|
||||
private static final String ARGS_MESSAGE_ID = "reactions.args.message.id";
|
||||
private static final String ARGS_IS_MMS = "reactions.args.is.mms";
|
||||
|
||||
private long messageId;
|
||||
private ViewPager2 recipientPagerView;
|
||||
private ReactionsLoader reactionsLoader;
|
||||
private ReactionViewPagerAdapter recipientsAdapter;
|
||||
private ReactionsViewModel viewModel;
|
||||
private Callback callback;
|
||||
|
||||
private final LifecycleDisposable disposables = new LifecycleDisposable();
|
||||
|
||||
public static DialogFragment create(long messageId, boolean isMms) {
|
||||
Bundle args = new Bundle();
|
||||
DialogFragment fragment = new ReactionsBottomSheetDialogFragment();
|
||||
@@ -61,7 +62,6 @@ public final class ReactionsBottomSheetDialogFragment extends BottomSheetDialogF
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
|
||||
if (ThemeUtil.isDarkTheme(requireContext())) {
|
||||
setStyle(DialogFragment.STYLE_NORMAL, R.style.Theme_Signal_BottomSheetDialog_Fixed_ReactWithAny);
|
||||
} else {
|
||||
@@ -82,24 +82,14 @@ public final class ReactionsBottomSheetDialogFragment extends BottomSheetDialogF
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
recipientPagerView = view.findViewById(R.id.reactions_bottom_view_recipient_pager);
|
||||
messageId = requireArguments().getLong(ARGS_MESSAGE_ID);
|
||||
|
||||
disposables.bindTo(getViewLifecycleOwner());
|
||||
|
||||
setUpRecipientsRecyclerView();
|
||||
setUpTabMediator(savedInstanceState);
|
||||
|
||||
reactionsLoader = new ReactionsLoader(requireContext(),
|
||||
requireArguments().getLong(ARGS_MESSAGE_ID),
|
||||
requireArguments().getBoolean(ARGS_IS_MMS));
|
||||
|
||||
LoaderManager.getInstance(requireActivity()).initLoader((int) messageId, null, reactionsLoader);
|
||||
|
||||
setUpViewModel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
LoaderManager.getInstance(requireActivity()).destroyLoader((int) messageId);
|
||||
super.onDestroyView();
|
||||
MessageId messageId = new MessageId(requireArguments().getLong(ARGS_MESSAGE_ID), requireArguments().getBoolean(ARGS_IS_MMS));
|
||||
setUpViewModel(messageId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -167,16 +157,16 @@ public final class ReactionsBottomSheetDialogFragment extends BottomSheetDialogF
|
||||
recipientPagerView.setAdapter(recipientsAdapter);
|
||||
}
|
||||
|
||||
private void setUpViewModel() {
|
||||
ReactionsViewModel.Factory factory = new ReactionsViewModel.Factory(reactionsLoader);
|
||||
private void setUpViewModel(@NonNull MessageId messageId) {
|
||||
ReactionsViewModel.Factory factory = new ReactionsViewModel.Factory(messageId);
|
||||
|
||||
viewModel = ViewModelProviders.of(this, factory).get(ReactionsViewModel.class);
|
||||
viewModel = new ViewModelProvider(this, factory).get(ReactionsViewModel.class);
|
||||
|
||||
viewModel.getEmojiCounts().observe(getViewLifecycleOwner(), emojiCounts -> {
|
||||
disposables.add(viewModel.getEmojiCounts().subscribe(emojiCounts -> {
|
||||
if (emojiCounts.size() <= 1) dismiss();
|
||||
|
||||
recipientsAdapter.submitList(emojiCounts);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
package org.thoughtcrime.securesms.reactions;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.AbstractCursorLoader;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class ReactionsLoader implements ReactionsViewModel.Repository, LoaderManager.LoaderCallbacks<Cursor> {
|
||||
|
||||
private final long messageId;
|
||||
private final boolean isMms;
|
||||
private final Context appContext;
|
||||
|
||||
private MutableLiveData<List<ReactionDetails>> internalLiveData = new MutableLiveData<>();
|
||||
|
||||
public ReactionsLoader(@NonNull Context context, long messageId, boolean isMms)
|
||||
{
|
||||
this.messageId = messageId;
|
||||
this.isMms = isMms;
|
||||
this.appContext = context.getApplicationContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Loader<Cursor> onCreateLoader(int id, @Nullable Bundle args) {
|
||||
return isMms ? new MmsMessageRecordCursorLoader(appContext, messageId)
|
||||
: new SmsMessageRecordCursorLoader(appContext, messageId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
data.moveToPosition(-1);
|
||||
|
||||
MessageRecord record = isMms ? MmsDatabase.readerFor(data).getNext()
|
||||
: SmsDatabase.readerFor(data).getNext();
|
||||
|
||||
if (record == null) {
|
||||
internalLiveData.postValue(Collections.emptyList());
|
||||
} else {
|
||||
internalLiveData.postValue(Stream.of(record.getReactions())
|
||||
.map(reactionRecord -> new ReactionDetails(Recipient.resolved(reactionRecord.getAuthor()),
|
||||
EmojiUtil.getCanonicalRepresentation(reactionRecord.getEmoji()),
|
||||
reactionRecord.getEmoji(),
|
||||
reactionRecord.getDateReceived()))
|
||||
.toList());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
|
||||
// Do nothing?
|
||||
}
|
||||
|
||||
@Override
|
||||
public LiveData<List<ReactionDetails>> getReactions() {
|
||||
return internalLiveData;
|
||||
}
|
||||
|
||||
private static final class MmsMessageRecordCursorLoader extends AbstractCursorLoader {
|
||||
|
||||
private final long messageId;
|
||||
|
||||
public MmsMessageRecordCursorLoader(@NonNull Context context, long messageId) {
|
||||
super(context);
|
||||
this.messageId = messageId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor getCursor() {
|
||||
return DatabaseFactory.getMmsDatabase(context).getMessageCursor(messageId);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class SmsMessageRecordCursorLoader extends AbstractCursorLoader {
|
||||
|
||||
private final long messageId;
|
||||
|
||||
public SmsMessageRecordCursorLoader(@NonNull Context context, long messageId) {
|
||||
super(context);
|
||||
this.messageId = messageId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor getCursor() {
|
||||
return DatabaseFactory.getSmsDatabase(context).getMessageCursor(messageId);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.thoughtcrime.securesms.reactions
|
||||
|
||||
import android.content.Context
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.core.ObservableEmitter
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiUtil
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.DatabaseObserver
|
||||
import org.thoughtcrime.securesms.database.model.MessageId
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
class ReactionsRepository {
|
||||
|
||||
fun getReactions(messageId: MessageId): Observable<List<ReactionDetails>> {
|
||||
return Observable.create { emitter: ObservableEmitter<List<ReactionDetails>> ->
|
||||
val databaseObserver: DatabaseObserver = ApplicationDependencies.getDatabaseObserver()
|
||||
|
||||
val messageObserver = DatabaseObserver.MessageObserver { messageId ->
|
||||
emitter.onNext(fetchReactionDetails(messageId))
|
||||
}
|
||||
|
||||
databaseObserver.registerMessageUpdateObserver(messageObserver)
|
||||
|
||||
emitter.setCancellable {
|
||||
databaseObserver.unregisterObserver(messageObserver)
|
||||
}
|
||||
|
||||
emitter.onNext(fetchReactionDetails(messageId))
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
private fun fetchReactionDetails(messageId: MessageId): List<ReactionDetails> {
|
||||
val context: Context = ApplicationDependencies.getApplication()
|
||||
val reactions: List<ReactionRecord> = DatabaseFactory.getReactionDatabase(context).getReactions(messageId)
|
||||
|
||||
return reactions.map { reaction ->
|
||||
ReactionDetails(
|
||||
sender = Recipient.resolved(reaction.author),
|
||||
baseEmoji = EmojiUtil.getCanonicalRepresentation(reaction.emoji),
|
||||
displayEmoji = reaction.emoji,
|
||||
timestamp = reaction.dateReceived
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,32 +8,45 @@ import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseObserver;
|
||||
import org.thoughtcrime.securesms.database.model.MessageId;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject;
|
||||
|
||||
public class ReactionsViewModel extends ViewModel {
|
||||
|
||||
private final Repository repository;
|
||||
private final MessageId messageId;
|
||||
private final ReactionsRepository repository;
|
||||
|
||||
public ReactionsViewModel(@NonNull Repository repository) {
|
||||
this.repository = repository;
|
||||
public ReactionsViewModel(@NonNull MessageId messageId) {
|
||||
this.messageId = messageId;
|
||||
this.repository = new ReactionsRepository();
|
||||
}
|
||||
|
||||
public @NonNull LiveData<List<EmojiCount>> getEmojiCounts() {
|
||||
return Transformations.map(repository.getReactions(),
|
||||
reactionList -> {
|
||||
List<EmojiCount> emojiCounts = Stream.of(reactionList)
|
||||
.groupBy(ReactionDetails::getBaseEmoji)
|
||||
.sorted(this::compareReactions)
|
||||
.map(entry -> new EmojiCount(entry.getKey(),
|
||||
getCountDisplayEmoji(entry.getValue()),
|
||||
entry.getValue()))
|
||||
.toList();
|
||||
public @NonNull Observable<List<EmojiCount>> getEmojiCounts() {
|
||||
return repository.getReactions(messageId)
|
||||
.map(reactionList -> {
|
||||
List<EmojiCount> emojiCounts = Stream.of(reactionList)
|
||||
.groupBy(ReactionDetails::getBaseEmoji)
|
||||
.sorted(this::compareReactions)
|
||||
.map(entry -> new EmojiCount(entry.getKey(),
|
||||
getCountDisplayEmoji(entry.getValue()),
|
||||
entry.getValue()))
|
||||
.toList();
|
||||
|
||||
emojiCounts.add(0, EmojiCount.all(reactionList));
|
||||
emojiCounts.add(0, EmojiCount.all(reactionList));
|
||||
|
||||
return emojiCounts;
|
||||
});
|
||||
return emojiCounts;
|
||||
})
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
private int compareReactions(@NonNull Map.Entry<String, List<ReactionDetails>> lhs, @NonNull Map.Entry<String, List<ReactionDetails>> rhs) {
|
||||
@@ -48,7 +61,7 @@ public class ReactionsViewModel extends ViewModel {
|
||||
|
||||
private long getLatestTimestamp(List<ReactionDetails> reactions) {
|
||||
return Stream.of(reactions)
|
||||
.max((a, b) -> Long.compare(a.getTimestamp(), b.getTimestamp()))
|
||||
.max(Comparator.comparingLong(ReactionDetails::getTimestamp))
|
||||
.map(ReactionDetails::getTimestamp)
|
||||
.orElse(-1L);
|
||||
}
|
||||
@@ -63,21 +76,17 @@ public class ReactionsViewModel extends ViewModel {
|
||||
return reactions.get(reactions.size() - 1).getDisplayEmoji();
|
||||
}
|
||||
|
||||
interface Repository {
|
||||
LiveData<List<ReactionDetails>> getReactions();
|
||||
}
|
||||
|
||||
static final class Factory implements ViewModelProvider.Factory {
|
||||
|
||||
private final Repository repository;
|
||||
private final MessageId messageId;
|
||||
|
||||
Factory(@NonNull Repository repository) {
|
||||
this.repository = repository;
|
||||
Factory(@NonNull MessageId messageId) {
|
||||
this.messageId = messageId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
return (T) new ReactionsViewModel(repository);
|
||||
return modelClass.cast(new ReactionsViewModel(messageId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,8 +39,8 @@ import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardPageCategoriesAdapter;
|
||||
import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardPageCategoryMappingModel;
|
||||
import org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView;
|
||||
import org.thoughtcrime.securesms.reactions.ReactionsLoader;
|
||||
import org.thoughtcrime.securesms.reactions.edit.EditReactionsActivity;
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable;
|
||||
import org.thoughtcrime.securesms.util.MappingModel;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
@@ -65,11 +65,12 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee
|
||||
|
||||
private ReactWithAnyEmojiViewModel viewModel;
|
||||
private Callback callback;
|
||||
private ReactionsLoader reactionsLoader;
|
||||
private EmojiPageView emojiPageView;
|
||||
private KeyboardPageSearchView search;
|
||||
private View tabBar;
|
||||
|
||||
private final LifecycleDisposable disposables = new LifecycleDisposable();
|
||||
|
||||
private final UpdateCategorySelectionOnScroll categoryUpdateOnScroll = new UpdateCategorySelectionOnScroll();
|
||||
|
||||
public static DialogFragment createForMessageRecord(@NonNull MessageRecord messageRecord, int startingPage) {
|
||||
@@ -174,11 +175,7 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
reactionsLoader = new ReactionsLoader(requireContext(),
|
||||
requireArguments().getLong(ARG_MESSAGE_ID),
|
||||
requireArguments().getBoolean(ARG_IS_MMS));
|
||||
|
||||
LoaderManager.getInstance(requireActivity()).initLoader((int) requireArguments().getLong(ARG_MESSAGE_ID), null, reactionsLoader);
|
||||
disposables.bindTo(getViewLifecycleOwner());
|
||||
|
||||
emojiPageView = view.findViewById(R.id.react_with_any_emoji_page_view);
|
||||
emojiPageView.initialize(this, this, true);
|
||||
@@ -219,15 +216,15 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee
|
||||
emojiPageView.addOnScrollListener(new TopAndBottomShadowHelper(requireView().findViewById(R.id.react_with_any_emoji_top_shadow),
|
||||
tabBar.findViewById(R.id.react_with_any_emoji_bottom_shadow)));
|
||||
|
||||
viewModel.getEmojiList().observe(getViewLifecycleOwner(), pages -> emojiPageView.setList(pages, null));
|
||||
viewModel.getCategories().observe(getViewLifecycleOwner(), categoriesAdapter::submitList);
|
||||
viewModel.getSelectedKey().observe(getViewLifecycleOwner(), key -> categoriesRecycler.post(() -> {
|
||||
disposables.add(viewModel.getEmojiList().subscribe(pages -> emojiPageView.setList(pages, null)));
|
||||
disposables.add(viewModel.getCategories().subscribe(categoriesAdapter::submitList));
|
||||
disposables.add(viewModel.getSelectedKey().subscribe(key -> categoriesRecycler.post(() -> {
|
||||
int index = categoriesAdapter.indexOfFirst(EmojiKeyboardPageCategoryMappingModel.class, m -> m.getKey().equals(key));
|
||||
|
||||
if (index != -1) {
|
||||
categoriesRecycler.smoothScrollToPosition(index);
|
||||
}
|
||||
}));
|
||||
})));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,7 +256,7 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee
|
||||
private void initializeViewModel() {
|
||||
Bundle args = requireArguments();
|
||||
ReactWithAnyEmojiRepository repository = new ReactWithAnyEmojiRepository(requireContext(), args.getString(ARG_RECENT_KEY, REACTION_STORAGE_KEY));
|
||||
ReactWithAnyEmojiViewModel.Factory factory = new ReactWithAnyEmojiViewModel.Factory(reactionsLoader, repository, args.getLong(ARG_MESSAGE_ID), args.getBoolean(ARG_IS_MMS));
|
||||
ReactWithAnyEmojiViewModel.Factory factory = new ReactWithAnyEmojiViewModel.Factory(repository, args.getLong(ARG_MESSAGE_ID), args.getBoolean(ARG_IS_MMS));
|
||||
|
||||
viewModel = ViewModelProviders.of(this, factory).get(ReactWithAnyEmojiViewModel.class);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase;
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
import org.thoughtcrime.securesms.database.model.MessageId;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.emoji.EmojiCategory;
|
||||
@@ -68,24 +69,18 @@ final class ReactWithAnyEmojiRepository {
|
||||
return pages;
|
||||
}
|
||||
|
||||
void addEmojiToMessage(@NonNull String emoji, long messageId, boolean isMms) {
|
||||
void addEmojiToMessage(@NonNull String emoji, @NonNull MessageId messageId) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
try {
|
||||
MessageDatabase db = isMms ? DatabaseFactory.getMmsDatabase(context) : DatabaseFactory.getSmsDatabase(context);
|
||||
MessageRecord messageRecord = db.getMessageRecord(messageId);
|
||||
ReactionRecord oldRecord = Stream.of(messageRecord.getReactions())
|
||||
.filter(record -> record.getAuthor().equals(Recipient.self().getId()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
ReactionRecord oldRecord = Stream.of(DatabaseFactory.getReactionDatabase(context).getReactions(messageId))
|
||||
.filter(record -> record.getAuthor().equals(Recipient.self().getId()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
if (oldRecord != null && oldRecord.getEmoji().equals(emoji)) {
|
||||
MessageSender.sendReactionRemoval(context, messageRecord.getId(), messageRecord.isMms(), oldRecord);
|
||||
} else {
|
||||
MessageSender.sendNewReaction(context, messageRecord.getId(), messageRecord.isMms(), emoji);
|
||||
ThreadUtil.runOnMain(() -> recentEmojiPageModel.onCodePointSelected(emoji));
|
||||
}
|
||||
} catch (NoSuchMessageException e) {
|
||||
Log.w(TAG, "Message not found! Ignoring.");
|
||||
if (oldRecord != null && oldRecord.getEmoji().equals(emoji)) {
|
||||
MessageSender.sendReactionRemoval(context, messageId, oldRecord);
|
||||
} else {
|
||||
MessageSender.sendNewReaction(context, messageId, emoji);
|
||||
ThreadUtil.runOnMain(() -> recentEmojiPageModel.onCodePointSelected(emoji));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,28 +4,29 @@ import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageModel;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter;
|
||||
import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel;
|
||||
import org.thoughtcrime.securesms.database.model.MessageId;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.emoji.EmojiCategory;
|
||||
import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardPageCategoryMappingModel;
|
||||
import org.thoughtcrime.securesms.keyboard.emoji.search.EmojiSearchRepository;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.reactions.ReactionsLoader;
|
||||
import org.thoughtcrime.securesms.reactions.ReactionsRepository;
|
||||
import org.thoughtcrime.securesms.util.MappingModelList;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import io.reactivex.rxjava3.subjects.BehaviorSubject;
|
||||
|
||||
public final class ReactWithAnyEmojiViewModel extends ViewModel {
|
||||
|
||||
private static final int SEARCH_LIMIT = 40;
|
||||
@@ -35,13 +36,12 @@ public final class ReactWithAnyEmojiViewModel extends ViewModel {
|
||||
private final boolean isMms;
|
||||
private final EmojiSearchRepository emojiSearchRepository;
|
||||
|
||||
private final LiveData<MappingModelList> categories;
|
||||
private final LiveData<MappingModelList> emojiList;
|
||||
private final MutableLiveData<EmojiSearchResult> searchResults;
|
||||
private final MutableLiveData<String> selectedKey;
|
||||
private final Observable<MappingModelList> categories;
|
||||
private final Observable<MappingModelList> emojiList;
|
||||
private final BehaviorSubject<EmojiSearchResult> searchResults;
|
||||
private final BehaviorSubject<String> selectedKey;
|
||||
|
||||
private ReactWithAnyEmojiViewModel(@NonNull ReactionsLoader reactionsLoader,
|
||||
@NonNull ReactWithAnyEmojiRepository repository,
|
||||
private ReactWithAnyEmojiViewModel(@NonNull ReactWithAnyEmojiRepository repository,
|
||||
long messageId,
|
||||
boolean isMms,
|
||||
@NonNull EmojiSearchRepository emojiSearchRepository)
|
||||
@@ -50,12 +50,13 @@ public final class ReactWithAnyEmojiViewModel extends ViewModel {
|
||||
this.messageId = messageId;
|
||||
this.isMms = isMms;
|
||||
this.emojiSearchRepository = emojiSearchRepository;
|
||||
this.searchResults = new MutableLiveData<>(new EmojiSearchResult());
|
||||
this.selectedKey = new MutableLiveData<>(getStartingKey());
|
||||
this.searchResults = BehaviorSubject.createDefault(new EmojiSearchResult());
|
||||
this.selectedKey = BehaviorSubject.createDefault(getStartingKey());
|
||||
|
||||
LiveData<List<ReactWithAnyEmojiPage>> emojiPages = Transformations.map(reactionsLoader.getReactions(), repository::getEmojiPageModels);
|
||||
Observable<List<ReactWithAnyEmojiPage>> emojiPages = new ReactionsRepository().getReactions(new MessageId(messageId, isMms))
|
||||
.map(repository::getEmojiPageModels);
|
||||
|
||||
LiveData<MappingModelList> emojiList = Transformations.map(emojiPages, (pages) -> {
|
||||
Observable<MappingModelList> emojiList = emojiPages.map(pages -> {
|
||||
MappingModelList list = new MappingModelList();
|
||||
|
||||
for (ReactWithAnyEmojiPage page : pages) {
|
||||
@@ -69,7 +70,7 @@ public final class ReactWithAnyEmojiViewModel extends ViewModel {
|
||||
return list;
|
||||
});
|
||||
|
||||
this.categories = LiveDataUtil.combineLatest(emojiPages, this.selectedKey, (pages, selectedKey) -> {
|
||||
this.categories = Observable.combineLatest(emojiPages, this.selectedKey.distinctUntilChanged(), (pages, selectedKey) -> {
|
||||
MappingModelList list = new MappingModelList();
|
||||
list.add(new EmojiKeyboardPageCategoryMappingModel.RecentsMappingModel(RecentEmojiPageModel.KEY.equals(selectedKey)));
|
||||
list.addAll(pages.stream()
|
||||
@@ -82,7 +83,7 @@ public final class ReactWithAnyEmojiViewModel extends ViewModel {
|
||||
return list;
|
||||
});
|
||||
|
||||
this.emojiList = LiveDataUtil.combineLatest(emojiList, searchResults, (all, search) -> {
|
||||
this.emojiList = Observable.combineLatest(emojiList, searchResults.distinctUntilChanged(), (all, search) -> {
|
||||
if (TextUtils.isEmpty(search.query)) {
|
||||
return all;
|
||||
} else {
|
||||
@@ -94,35 +95,31 @@ public final class ReactWithAnyEmojiViewModel extends ViewModel {
|
||||
});
|
||||
}
|
||||
|
||||
LiveData<MappingModelList> getCategories() {
|
||||
return categories;
|
||||
@NonNull Observable<MappingModelList> getCategories() {
|
||||
return categories.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
LiveData<String> getSelectedKey() {
|
||||
return selectedKey;
|
||||
@NonNull Observable<String> getSelectedKey() {
|
||||
return selectedKey.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
@NonNull Observable<MappingModelList> getEmojiList() {
|
||||
return emojiList.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
void onEmojiSelected(@NonNull String emoji) {
|
||||
if (messageId > 0) {
|
||||
SignalStore.emojiValues().setPreferredVariation(emoji);
|
||||
repository.addEmojiToMessage(emoji, messageId, isMms);
|
||||
repository.addEmojiToMessage(emoji, new MessageId(messageId, isMms));
|
||||
}
|
||||
}
|
||||
|
||||
LiveData<MappingModelList> getEmojiList() {
|
||||
return emojiList;
|
||||
}
|
||||
|
||||
public void onQueryChanged(String query) {
|
||||
emojiSearchRepository.submitQuery(query, false, SEARCH_LIMIT, m -> searchResults.postValue(new EmojiSearchResult(query, m)));
|
||||
emojiSearchRepository.submitQuery(query, false, SEARCH_LIMIT, m -> searchResults.onNext(new EmojiSearchResult(query, m)));
|
||||
}
|
||||
|
||||
public void selectPage(@NonNull String key) {
|
||||
if (key.equals(selectedKey.getValue())) {
|
||||
return;
|
||||
}
|
||||
|
||||
selectedKey.setValue(key);
|
||||
selectedKey.onNext(key);
|
||||
}
|
||||
|
||||
private static @NonNull MappingModelList toMappingModels(@NonNull EmojiPageModel model) {
|
||||
@@ -156,22 +153,20 @@ public final class ReactWithAnyEmojiViewModel extends ViewModel {
|
||||
|
||||
static class Factory implements ViewModelProvider.Factory {
|
||||
|
||||
private final ReactionsLoader reactionsLoader;
|
||||
private final ReactWithAnyEmojiRepository repository;
|
||||
private final long messageId;
|
||||
private final boolean isMms;
|
||||
|
||||
Factory(@NonNull ReactionsLoader reactionsLoader, @NonNull ReactWithAnyEmojiRepository repository, long messageId, boolean isMms) {
|
||||
this.reactionsLoader = reactionsLoader;
|
||||
this.repository = repository;
|
||||
this.messageId = messageId;
|
||||
this.isMms = isMms;
|
||||
Factory(@NonNull ReactWithAnyEmojiRepository repository, long messageId, boolean isMms) {
|
||||
this.repository = repository;
|
||||
this.messageId = messageId;
|
||||
this.isMms = isMms;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
//noinspection ConstantConditions
|
||||
return modelClass.cast(new ReactWithAnyEmojiViewModel(reactionsLoader, repository, messageId, isMms, new EmojiSearchRepository(ApplicationDependencies.getApplication())));
|
||||
return modelClass.cast(new ReactWithAnyEmojiViewModel(repository, messageId, isMms, new EmojiSearchRepository(ApplicationDependencies.getApplication())));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageId;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||
@@ -320,27 +321,23 @@ public class MessageSender {
|
||||
}
|
||||
}
|
||||
|
||||
public static void sendNewReaction(@NonNull Context context, long messageId, boolean isMms, @NonNull String emoji) {
|
||||
MessageDatabase db = isMms ? DatabaseFactory.getMmsDatabase(context) : DatabaseFactory.getSmsDatabase(context);
|
||||
ReactionRecord reaction = new ReactionRecord(emoji, Recipient.self().getId(), System.currentTimeMillis(), System.currentTimeMillis());
|
||||
|
||||
db.addReaction(messageId, reaction);
|
||||
public static void sendNewReaction(@NonNull Context context, @NonNull MessageId messageId, @NonNull String emoji) {
|
||||
ReactionRecord reaction = new ReactionRecord(emoji, Recipient.self().getId(), System.currentTimeMillis(), System.currentTimeMillis());
|
||||
DatabaseFactory.getReactionDatabase(context).addReaction(messageId, reaction);
|
||||
|
||||
try {
|
||||
ApplicationDependencies.getJobManager().add(ReactionSendJob.create(context, messageId, isMms, reaction, false));
|
||||
ApplicationDependencies.getJobManager().add(ReactionSendJob.create(context, messageId, reaction, false));
|
||||
onMessageSent();
|
||||
} catch (NoSuchMessageException e) {
|
||||
Log.w(TAG, "[sendNewReaction] Could not find message! Ignoring.");
|
||||
}
|
||||
}
|
||||
|
||||
public static void sendReactionRemoval(@NonNull Context context, long messageId, boolean isMms, @NonNull ReactionRecord reaction) {
|
||||
MessageDatabase db = isMms ? DatabaseFactory.getMmsDatabase(context) : DatabaseFactory.getSmsDatabase(context);
|
||||
|
||||
db.deleteReaction(messageId, reaction.getAuthor());
|
||||
public static void sendReactionRemoval(@NonNull Context context, @NonNull MessageId messageId, @NonNull ReactionRecord reaction) {
|
||||
DatabaseFactory.getReactionDatabase(context).deleteReaction(messageId, reaction.getAuthor());
|
||||
|
||||
try {
|
||||
ApplicationDependencies.getJobManager().add(ReactionSendJob.create(context, messageId, isMms, reaction, true));
|
||||
ApplicationDependencies.getJobManager().add(ReactionSendJob.create(context, messageId, reaction, true));
|
||||
onMessageSent();
|
||||
} catch (NoSuchMessageException e) {
|
||||
Log.w(TAG, "[sendReactionRemoval] Could not find message! Ignoring.");
|
||||
|
||||
@@ -12,6 +12,10 @@ import io.reactivex.rxjava3.disposables.Disposable
|
||||
class LifecycleDisposable : DefaultLifecycleObserver {
|
||||
val disposables: CompositeDisposable = CompositeDisposable()
|
||||
|
||||
fun bindTo(lifecycleOwner: LifecycleOwner): LifecycleDisposable {
|
||||
return bindTo(lifecycleOwner.lifecycle)
|
||||
}
|
||||
|
||||
fun bindTo(lifecycle: Lifecycle): LifecycleDisposable {
|
||||
lifecycle.addObserver(this)
|
||||
return this
|
||||
|
||||
Reference in New Issue
Block a user