diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index 0cf1f4364..fdf514cd2 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -70,6 +70,7 @@ public final class Namespace { public static final String MUC = "http://jabber.org/protocol/muc"; public static final String MUC_USER = MUC + "#user"; public static final String NICK = "http://jabber.org/protocol/nick"; + public static final String REACTIONS = "urn:xmpp:reactions:0"; public static final String OCCUPANT_ID = "urn:xmpp:occupant-id:0"; public static final String OMEMO_DTLS_SRTP_VERIFICATION = "http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification"; diff --git a/src/main/java/im/conversations/android/database/ConversationsDatabase.java b/src/main/java/im/conversations/android/database/ConversationsDatabase.java index d2dc8c6d1..c3df1f8c0 100644 --- a/src/main/java/im/conversations/android/database/ConversationsDatabase.java +++ b/src/main/java/im/conversations/android/database/ConversationsDatabase.java @@ -38,11 +38,11 @@ import im.conversations.android.database.entity.DiscoIdentityEntity; import im.conversations.android.database.entity.DiscoItemEntity; import im.conversations.android.database.entity.MessageContentEntity; import im.conversations.android.database.entity.MessageEntity; +import im.conversations.android.database.entity.MessageReactionEntity; import im.conversations.android.database.entity.MessageStateEntity; import im.conversations.android.database.entity.MessageVersionEntity; import im.conversations.android.database.entity.NickEntity; import im.conversations.android.database.entity.PresenceEntity; -import im.conversations.android.database.entity.ReactionEntity; import im.conversations.android.database.entity.RosterItemEntity; import im.conversations.android.database.entity.RosterItemGroupEntity; @@ -74,7 +74,7 @@ import im.conversations.android.database.entity.RosterItemGroupEntity; MessageVersionEntity.class, NickEntity.class, PresenceEntity.class, - ReactionEntity.class, + MessageReactionEntity.class, RosterItemEntity.class, RosterItemGroupEntity.class }, diff --git a/src/main/java/im/conversations/android/database/dao/MessageDao.java b/src/main/java/im/conversations/android/database/dao/MessageDao.java index a1b0abe09..bc68a7b30 100644 --- a/src/main/java/im/conversations/android/database/dao/MessageDao.java +++ b/src/main/java/im/conversations/android/database/dao/MessageDao.java @@ -6,10 +6,12 @@ import androidx.room.Insert; import androidx.room.Query; import androidx.room.Transaction; import com.google.common.base.Preconditions; +import com.google.common.collect.Collections2; import com.google.common.collect.Lists; import eu.siacs.conversations.xmpp.Jid; import im.conversations.android.database.entity.MessageContentEntity; import im.conversations.android.database.entity.MessageEntity; +import im.conversations.android.database.entity.MessageReactionEntity; import im.conversations.android.database.entity.MessageStateEntity; import im.conversations.android.database.entity.MessageVersionEntity; import im.conversations.android.database.model.Account; @@ -19,6 +21,8 @@ import im.conversations.android.database.model.MessageIdentifier; import im.conversations.android.database.model.MessageState; import im.conversations.android.database.model.Modification; import im.conversations.android.transformer.Transformation; +import im.conversations.android.xmpp.model.reactions.Reactions; +import im.conversations.android.xmpp.model.stanza.Message; import java.util.Collection; import java.util.List; import org.slf4j.Logger; @@ -105,12 +109,12 @@ public abstract class MessageDao { // when found by stanzaId the stanzaId must either by verified or belonging to a stub // when found by messageId the from must either match (for corrections) or not be set (null) and // we only look up stubs - // TODO the from matcher should be in the outer condition @Query( "SELECT id,stanzaId,messageId,fromBare,latestVersion as version FROM message WHERE" - + " chatId=:chatId AND (fromBare=:fromBare OR fromBare=NULL) AND ((stanzaId !=" - + " NULL AND stanzaId=:stanzaId AND (stanzaIdVerified=1 OR latestVersion=NULL)) OR" - + " (stanzaId = NULL AND messageId=:messageId AND latestVersion = NULL))") + + " chatId=:chatId AND (fromBare=:fromBare OR fromBare IS NULL) AND ((stanzaId IS" + + " NOT NULL AND stanzaId=:stanzaId AND (stanzaIdVerified=1 OR latestVersion IS" + + " NULL)) OR (stanzaId IS NULL AND messageId=:messageId AND latestVersion IS" + + " NULL))") abstract MessageIdentifier get(long chatId, Jid fromBare, String stanzaId, String messageId); public MessageIdentifier getOrCreateVersion( @@ -200,14 +204,39 @@ public abstract class MessageDao { protected abstract void setLatestMessageId( final long messageEntityId, final long messageVersionId); - public Long getOrCreateStub(final Transformation transformation) { - // TODO look up where parentId matches messageId (or stanzaId for group chats) - - // when creating stub either set from (correction) or don’t (other attachment) - - return null; + public MessageIdentifier getOrCreateStub( + final ChatIdentifier chat, final Message.Type messageType, final String parentId) { + final MessageIdentifier existing; + if (messageType == Message.Type.GROUPCHAT) { + existing = getByStanzaId(chat.id, parentId); + } else { + existing = getByMessageId(chat.id, parentId); + } + if (existing != null) { + return existing; + } + final MessageEntity messageEntity; + if (messageType == Message.Type.GROUPCHAT) { + LOGGER.info("Create stub for stanza id {}", parentId); + messageEntity = MessageEntity.stubOfStanzaId(chat.id, parentId); + } else { + LOGGER.info("Create stub for message id {}", parentId); + messageEntity = MessageEntity.stubOfMessageId(chat.id, parentId); + } + final long messageEntityId = insert(messageEntity); + return new MessageIdentifier(messageEntityId, null, null, null, null); } + @Query( + "SELECT id,stanzaId,messageId,fromBare,latestVersion as version FROM message WHERE" + + " chatId=:chatId AND messageId=:messageId") + protected abstract MessageIdentifier getByMessageId(final long chatId, final String messageId); + + @Query( + "SELECT id,stanzaId,messageId,fromBare,latestVersion as version FROM message WHERE" + + " chatId=:chatId AND stanzaId=:stanzaId") + protected abstract MessageIdentifier getByStanzaId(final long chatId, final String stanzaId); + public void insertMessageContent(Long latestVersion, List contents) { Preconditions.checkNotNull( latestVersion, "Contents can only be inserted for a specific version"); @@ -245,4 +274,19 @@ public abstract class MessageDao { @Insert protected abstract void insert(MessageStateEntity messageStateEntity); + + @Insert + protected abstract void insertReactions(Collection reactionEntities); + + public void insertReactions( + ChatIdentifier chat, Reactions reactions, Transformation transformation) { + final Message.Type messageType = transformation.type; + final MessageIdentifier messageIdentifier = + getOrCreateStub(chat, messageType, reactions.getId()); + // TODO delete old reactions + insertReactions( + Collections2.transform( + reactions.getReactions(), + r -> MessageReactionEntity.of(messageIdentifier.id, r, transformation))); + } } diff --git a/src/main/java/im/conversations/android/database/dao/RosterDao.java b/src/main/java/im/conversations/android/database/dao/RosterDao.java index b04f36cf3..62dbcf6a6 100644 --- a/src/main/java/im/conversations/android/database/dao/RosterDao.java +++ b/src/main/java/im/conversations/android/database/dao/RosterDao.java @@ -50,6 +50,7 @@ public abstract class RosterDao { } final RosterItemEntity entity = RosterItemEntity.of(account.id, item); final long id = insert(entity); + // TODO groups } setRosterVersion(account.id, version); } diff --git a/src/main/java/im/conversations/android/database/entity/MessageEntity.java b/src/main/java/im/conversations/android/database/entity/MessageEntity.java index 0eef6aa5b..4adc26236 100644 --- a/src/main/java/im/conversations/android/database/entity/MessageEntity.java +++ b/src/main/java/im/conversations/android/database/entity/MessageEntity.java @@ -81,4 +81,18 @@ public class MessageEntity { entity.stanzaIdVerified = false; return entity; } + + public static MessageEntity stubOfStanzaId(final long chatId, String stanzaId) { + final var entity = new MessageEntity(); + entity.stanzaIdVerified = false; + entity.stanzaId = stanzaId; + return entity; + } + + public static MessageEntity stubOfMessageId(final long chatId, String messageId) { + final var entity = new MessageEntity(); + entity.stanzaIdVerified = false; + entity.messageId = messageId; + return entity; + } } diff --git a/src/main/java/im/conversations/android/database/entity/ReactionEntity.java b/src/main/java/im/conversations/android/database/entity/MessageReactionEntity.java similarity index 52% rename from src/main/java/im/conversations/android/database/entity/ReactionEntity.java rename to src/main/java/im/conversations/android/database/entity/MessageReactionEntity.java index 85b463449..7fbbbf0ec 100644 --- a/src/main/java/im/conversations/android/database/entity/ReactionEntity.java +++ b/src/main/java/im/conversations/android/database/entity/MessageReactionEntity.java @@ -5,6 +5,8 @@ import androidx.room.Entity; import androidx.room.ForeignKey; import androidx.room.Index; import androidx.room.PrimaryKey; +import eu.siacs.conversations.xmpp.Jid; +import im.conversations.android.transformer.Transformation; import java.time.Instant; @Entity( @@ -16,7 +18,7 @@ import java.time.Instant; childColumns = {"messageEntityId"}, onDelete = ForeignKey.CASCADE), indices = {@Index(value = "messageEntityId")}) -public class ReactionEntity { +public class MessageReactionEntity { @PrimaryKey(autoGenerate = true) public Long id; @@ -25,11 +27,25 @@ public class ReactionEntity { public String stanzaId; public String messageId; - public String reactionBy; + public Jid reactionBy; public String reactionByResource; public String occupantId; public Instant receivedAt; public String reaction; + + public static MessageReactionEntity of( + long messageEntityId, final String reaction, final Transformation transformation) { + final var entity = new MessageReactionEntity(); + entity.messageEntityId = messageEntityId; + entity.reaction = reaction; + entity.stanzaId = transformation.stanzaId; + entity.messageId = transformation.messageId; + entity.reactionBy = transformation.fromBare(); + entity.reactionByResource = transformation.fromResource(); + entity.occupantId = transformation.occupantId; + entity.receivedAt = transformation.receivedAt; + return entity; + } } diff --git a/src/main/java/im/conversations/android/transformer/Transformation.java b/src/main/java/im/conversations/android/transformer/Transformation.java index b037aecc1..801615f12 100644 --- a/src/main/java/im/conversations/android/transformer/Transformation.java +++ b/src/main/java/im/conversations/android/transformer/Transformation.java @@ -16,6 +16,7 @@ import im.conversations.android.xmpp.model.jabber.Thread; import im.conversations.android.xmpp.model.markers.Displayed; import im.conversations.android.xmpp.model.muc.user.MultiUserChat; import im.conversations.android.xmpp.model.oob.OutOfBandData; +import im.conversations.android.xmpp.model.reactions.Reactions; import im.conversations.android.xmpp.model.stanza.Message; import java.time.Instant; import java.util.Arrays; @@ -34,7 +35,8 @@ public class Transformation { DeliveryReceipt.class, MultiUserChat.class, Displayed.class, - Replace.class); + Replace.class, + Reactions.class); public final Instant receivedAt; public final Jid to; diff --git a/src/main/java/im/conversations/android/transformer/Transformer.java b/src/main/java/im/conversations/android/transformer/Transformer.java index 8d194ad3f..946a1bfdd 100644 --- a/src/main/java/im/conversations/android/transformer/Transformer.java +++ b/src/main/java/im/conversations/android/transformer/Transformer.java @@ -18,6 +18,7 @@ import im.conversations.android.xmpp.model.jabber.Body; import im.conversations.android.xmpp.model.markers.Displayed; import im.conversations.android.xmpp.model.muc.user.MultiUserChat; import im.conversations.android.xmpp.model.oob.OutOfBandData; +import im.conversations.android.xmpp.model.reactions.Reactions; import im.conversations.android.xmpp.model.stanza.Message; import java.util.Arrays; import java.util.Collection; @@ -70,11 +71,16 @@ public class Transformer { return false; } final Replace messageCorrection = transformation.getExtension(Replace.class); + final Reactions reactions = transformation.getExtension(Reactions.class); final List contents = parseContent(transformation); final boolean identifiableSender = Arrays.asList(Message.Type.NORMAL, Message.Type.CHAT).contains(messageType) || Objects.nonNull(transformation.occupantId); + final boolean isReaction = + Objects.nonNull(reactions) + && Objects.nonNull(reactions.getId()) + && identifiableSender; final boolean isMessageCorrection = Objects.nonNull(messageCorrection) && messageCorrection.getId() != null @@ -83,7 +89,9 @@ public class Transformer { if (contents.isEmpty()) { LOGGER.info("Received message from {} w/o contents", transformation.from); transformMessageState(chat, transformation); - // TODO apply reactions + if (isReaction) { + database.messageDao().insertReactions(chat, reactions, transformation); + } } else { final MessageIdentifier messageIdentifier; try { diff --git a/src/main/java/im/conversations/android/xmpp/model/reactions/Reaction.java b/src/main/java/im/conversations/android/xmpp/model/reactions/Reaction.java new file mode 100644 index 000000000..de9bd7f2a --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/reactions/Reaction.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.reactions; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Reaction extends Extension { + + public Reaction() { + super(Reaction.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/reactions/Reactions.java b/src/main/java/im/conversations/android/xmpp/model/reactions/Reactions.java new file mode 100644 index 000000000..4d71420a3 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/reactions/Reactions.java @@ -0,0 +1,26 @@ +package im.conversations.android.xmpp.model.reactions; + +import com.google.common.base.Strings; +import com.google.common.collect.Collections2; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import java.util.Collection; +import java.util.Objects; + +@XmlElement +public class Reactions extends Extension { + + public Reactions() { + super(Reactions.class); + } + + public Collection getReactions() { + return Collections2.filter( + Collections2.transform(getExtensions(Reaction.class), Reaction::getContent), + r -> Objects.nonNull(Strings.nullToEmpty(r))); + } + + public String getId() { + return this.getAttribute("id"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/reactions/package-info.java b/src/main/java/im/conversations/android/xmpp/model/reactions/package-info.java new file mode 100644 index 000000000..5254dde5c --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/reactions/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.REACTIONS) +package im.conversations.android.xmpp.model.reactions; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; \ No newline at end of file