reactions

This commit is contained in:
kosyak 2023-12-27 01:07:08 +01:00
parent 9467fc1789
commit 305ae7a288
8 changed files with 221 additions and 11 deletions

View file

@ -79,10 +79,12 @@ import java.time.format.DateTimeParseException;
import java.time.format.FormatStyle; import java.time.format.FormatStyle;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.ListIterator; import java.util.ListIterator;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
import java.util.Timer; import java.util.Timer;
import java.util.TimerTask; import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@ -518,6 +520,23 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
return null; return null;
} }
public Message findMessageWithRemoteIdAndCounterpart(String id, Jid counterpart) {
synchronized (this.messages) {
for (int i = this.messages.size() - 1; i >= 0; --i) {
final Message message = messages.get(i);
final Jid mcp = message.getCounterpart();
if (mcp == null && counterpart != null) {
continue;
}
if (counterpart == null || mcp.equals(counterpart) || mcp.asBareJid().equals(counterpart)) {
final boolean idMatch = id.equals(message.getUuid()) || id.equals(message.getRemoteMsgId()) || message.remoteMsgIdMatchInEdit(id) || (getMode() == MODE_MULTI && id.equals(message.getServerMsgId()));
if (idMatch) return message;
}
}
}
return null;
}
public Message findMessageWithRemoteIdAndCounterpart(String id, Jid counterpart, boolean received, boolean carbon) { public Message findMessageWithRemoteIdAndCounterpart(String id, Jid counterpart, boolean received, boolean carbon) {
synchronized (this.messages) { synchronized (this.messages) {
for (int i = this.messages.size() - 1; i >= 0; --i) { for (int i = this.messages.size() - 1; i >= 0; --i) {
@ -585,6 +604,40 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
return false; return false;
} }
public Message findMessageReactingTo(String id, Jid reactor) {
if (id == null) return null;
synchronized (this.messages) {
for (int i = this.messages.size() - 1; i >= 0; --i) {
final Message message = messages.get(i);
if (reactor == null && message.getStatus() < Message.STATUS_SEND) continue;
if (reactor != null && message.getCounterpart() == null) continue;
if (reactor != null && !(message.getCounterpart().equals(reactor) || message.getCounterpart().asBareJid().equals(reactor))) continue;
final Element r = message.getReactions();
if (r != null && r.getAttribute("id") != null && id.equals(r.getAttribute("id"))) {
return message;
}
}
}
return null;
}
public Set<String> findReactionsTo(String id, Jid reactor) {
Set<String> reactionEmoji = new HashSet<>();
Message reactM = findMessageReactingTo(id, reactor);
Element reactions = reactM == null ? null : reactM.getReactions();
if (reactions != null) {
for (Element el : reactions.getChildren()) {
if (el.getName().equals("reaction") && el.getNamespace().equals("urn:xmpp:reactions:0")) {
reactionEmoji.add(el.getContent());
}
}
}
return reactionEmoji;
}
public void populateWithMessages(final List<Message> messages) { public void populateWithMessages(final List<Message> messages) {
if (historyPartMessages.size() > 0) { if (historyPartMessages.size() > 0) {
messages.clear(); messages.clear();

View file

@ -16,6 +16,7 @@ import java.io.IOException;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@ -362,6 +363,42 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
return m; return m;
} }
public Message react(String emoji) {
Set<String> emojis = new HashSet<>();
if (conversation instanceof Conversation) emojis = ((Conversation) conversation).findReactionsTo(replyId(), null);
emojis.add(emoji);
final Message m = reply();
m.appendBody(emoji);
final Element fallback = new Element("fallback", "urn:xmpp:fallback:0").setAttribute("for", "urn:xmpp:reactions:0");
fallback.addChild("body", "urn:xmpp:fallback:0");
m.addPayload(fallback);
final Element reactions = new Element("reactions", "urn:xmpp:reactions:0").setAttribute("id", replyId());
for (String oneEmoji : emojis) {
reactions.addChild("reaction", "urn:xmpp:reactions:0").setContent(oneEmoji);
}
m.addPayload(reactions);
return m;
}
public void setReactions(Element reactions) {
if (this.payloads != null) {
this.payloads.remove(getReactions());
}
addPayload(reactions);
}
public Element getReactions() {
if (this.payloads == null) return null;
for (Element el : this.payloads) {
if (el.getName().equals("reactions") && el.getNamespace().equals("urn:xmpp:reactions:0")) {
return el;
}
}
return null;
}
public Element getReply() { public Element getReply() {
if (this.payloads == null) return null; if (this.payloads == null) return null;
@ -863,6 +900,31 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
return new ArrayList<>(this.payloads); return new ArrayList<>(this.payloads);
} }
public List<Element> getFallbacks(String... includeFor) {
List<Element> fallbacks = new ArrayList<>();
if (this.payloads == null) return fallbacks;
for (Element el : this.payloads) {
if (el.getName().equals("fallback") && el.getNamespace().equals("urn:xmpp:fallback:0")) {
final String fallbackFor = el.getAttribute("for");
if (fallbackFor == null) continue;
for (String includeOne : includeFor) {
if (fallbackFor.equals(includeOne)) {
fallbacks.add(el);
break;
}
}
}
}
return fallbacks;
}
public synchronized void clearFallbacks(String... includeFor) {
this.payloads.removeAll(getFallbacks(includeFor));
}
public void setOob(boolean isOob) { public void setOob(boolean isOob) {
this.oob = isOob; this.oob = isOob;
} }

View file

@ -410,13 +410,21 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
if (timestamp == null) { if (timestamp == null) {
timestamp = AbstractParser.parseTimestamp(original, AbstractParser.parseTimestamp(packet)); timestamp = AbstractParser.parseTimestamp(original, AbstractParser.parseTimestamp(packet));
} }
final LocalizedContent body = packet.getBody();
LocalizedContent body = packet.getBody();
final Element mucUserElement = packet.findChild("x", Namespace.MUC_USER); final Element mucUserElement = packet.findChild("x", Namespace.MUC_USER);
final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted"); final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted");
final Element replaceElement = packet.findChild("replace", "urn:xmpp:message-correct:0"); final Element replaceElement = packet.findChild("replace", "urn:xmpp:message-correct:0");
final Element oob = packet.findChild("x", Namespace.OOB); final Element oob = packet.findChild("x", Namespace.OOB);
final String oobUrl = oob != null ? oob.findChildContent("url") : null; final String oobUrl = oob != null ? oob.findChildContent("url") : null;
final String replacementId = replaceElement == null ? null : replaceElement.getAttribute("id"); String replacementId = replaceElement == null ? null : replaceElement.getAttribute("id");
if (replacementId == null) {
Element fasten = packet.findChild("apply-to", "urn:xmpp:fasten:0");
if (fasten != null && fasten.findChild("retract", "urn:xmpp:message-retract:0") != null) {
replacementId = fasten.getAttribute("id");
packet.setBody("");
}
}
final Element axolotlEncrypted = packet.findChildEnsureSingle(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX); final Element axolotlEncrypted = packet.findChildEnsureSingle(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX);
int status; int status;
final Jid counterpart; final Jid counterpart;
@ -479,6 +487,27 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
nextCounterpart = counterpart; nextCounterpart = counterpart;
} }
final Element reactions = packet.findChild("reactions", "urn:xmpp:reactions:0");
if (body == null) {
if (reactions != null && reactions.getAttribute("id") != null) {
final Conversation conversation = mXmppConnectionService.find(account, counterpart.asBareJid(), nextCounterpart);
if (conversation != null) {
final Message reactionTo = conversation.findMessageWithRemoteIdAndCounterpart(reactions.getAttribute("id"), null);
if (reactionTo != null) {
String bodyS = reactionTo.reply().getBody();
for (Element el : reactions.getChildren()) {
if (el.getName().equals("reaction") && el.getNamespace().equals("urn:xmpp:reactions:0")) {
bodyS += el.getContent();
}
}
body = new LocalizedContent(bodyS, "en", 1);
final Message previousReaction = conversation.findMessageReactingTo(reactions.getAttribute("id"), counterpart);
if (previousReaction != null) replacementId = previousReaction.replyId();
}
}
}
}
if (nextCounterpart != null && mXmppConnectionService.checkIsArchived(account, counterpart.asBareJid(), nextCounterpart)) { if (nextCounterpart != null && mXmppConnectionService.checkIsArchived(account, counterpart.asBareJid(), nextCounterpart)) {
return; return;
} }
@ -605,7 +634,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
} }
} }
message.markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0"); message.markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0");
if (reactions != null) message.addPayload(reactions);
for (Element el : packet.getChildren()) { for (Element el : packet.getChildren()) {
String name = el.getName(); String name = el.getName();
String ns = el.getNamespace(); String ns = el.getNamespace();

View file

@ -1083,7 +1083,12 @@ public class XmppConnectionService extends Service {
private void directReply(final Conversation conversation, final String body, final String lastMessageUuid, final boolean dismissAfterReply) {final Message inReplyTo = lastMessageUuid == null ? null : conversation.findMessageWithUuid(lastMessageUuid); private void directReply(final Conversation conversation, final String body, final String lastMessageUuid, final boolean dismissAfterReply) {final Message inReplyTo = lastMessageUuid == null ? null : conversation.findMessageWithUuid(lastMessageUuid);
Message message = new Message(conversation, body, conversation.getNextEncryption()); Message message = new Message(conversation, body, conversation.getNextEncryption());
if (inReplyTo != null) { if (inReplyTo != null) {
message = inReplyTo.reply(); if (Emoticons.isEmoji(body.toString().replaceAll("\\s", ""))) {
message = conversation.getReplyTo().react(body.toString().replaceAll("\\s", ""));
} else {
message = inReplyTo.reply();
}
message.clearFallbacks("urn:xmpp:reply:0");
message.setBody(body); message.setBody(body);
message.setEncryption(conversation.getNextEncryption()); message.setEncryption(conversation.getNextEncryption());
} }

View file

@ -146,6 +146,7 @@ import eu.siacs.conversations.ui.widget.HighlighterView;
import eu.siacs.conversations.ui.widget.TabLayout; import eu.siacs.conversations.ui.widget.TabLayout;
import eu.siacs.conversations.utils.AccountUtils; import eu.siacs.conversations.utils.AccountUtils;
import eu.siacs.conversations.utils.Compatibility; import eu.siacs.conversations.utils.Compatibility;
import eu.siacs.conversations.utils.Emoticons;
import eu.siacs.conversations.utils.GeoHelper; import eu.siacs.conversations.utils.GeoHelper;
import eu.siacs.conversations.utils.MessageUtils; import eu.siacs.conversations.utils.MessageUtils;
import eu.siacs.conversations.utils.NickValidityChecker; import eu.siacs.conversations.utils.NickValidityChecker;
@ -1046,8 +1047,12 @@ public class ConversationFragment extends XmppFragment
final Message message; final Message message;
if (conversation.getCorrectingMessage() == null) { if (conversation.getCorrectingMessage() == null) {
if (conversation.getReplyTo() != null) { if (conversation.getReplyTo() != null) {
message = conversation.getReplyTo().reply(); if (Emoticons.isEmoji(body.toString().replaceAll("\\s", ""))) {
message.appendBody(body); message = conversation.getReplyTo().react(body.toString().replaceAll("\\s", ""));
} else {
message = conversation.getReplyTo().reply();
message.appendBody(body);
}
message.setEncryption(conversation.getNextEncryption()); message.setEncryption(conversation.getNextEncryption());
} else { } else {
message = new Message(conversation, body, conversation.getNextEncryption()); message = new Message(conversation, body, conversation.getNextEncryption());
@ -1708,6 +1713,7 @@ public class ConversationFragment extends XmppFragment
MenuItem quoteMessage = menu.findItem(R.id.quote_message); MenuItem quoteMessage = menu.findItem(R.id.quote_message);
MenuItem retryDecryption = menu.findItem(R.id.retry_decryption); MenuItem retryDecryption = menu.findItem(R.id.retry_decryption);
MenuItem correctMessage = menu.findItem(R.id.correct_message); MenuItem correctMessage = menu.findItem(R.id.correct_message);
MenuItem retractMessage = menu.findItem(R.id.retract_message);
MenuItem shareWith = menu.findItem(R.id.share_with); MenuItem shareWith = menu.findItem(R.id.share_with);
MenuItem sendAgain = menu.findItem(R.id.send_again); MenuItem sendAgain = menu.findItem(R.id.send_again);
MenuItem copyUrl = menu.findItem(R.id.copy_url); MenuItem copyUrl = menu.findItem(R.id.copy_url);
@ -1752,6 +1758,7 @@ public class ConversationFragment extends XmppFragment
if (m.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED && !deleted) { if (m.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED && !deleted) {
retryDecryption.setVisible(true); retryDecryption.setVisible(true);
} }
if (!showError if (!showError
&& m.getType() == Message.TYPE_TEXT && m.getType() == Message.TYPE_TEXT
&& !m.isGeoUri() && !m.isGeoUri()
@ -1759,6 +1766,24 @@ public class ConversationFragment extends XmppFragment
&& m.getConversation() instanceof Conversation) { && m.getConversation() instanceof Conversation) {
correctMessage.setVisible(true); correctMessage.setVisible(true);
} }
if (!showError
&& m.getType() == Message.TYPE_TEXT
&& !m.isGeoUri()
&& m.isLastCorrectableMessage()
&& m.getConversation() instanceof Conversation) {
correctMessage.setVisible(true);
if (!m.getBody().equals("") && !m.getBody().equals(" ")) {
retractMessage.setVisible(true);
}
}
if (m.getReactions() != null) {
correctMessage.setVisible(false);
retractMessage.setVisible(true);
}
if ((m.isFileOrImage() && !deleted && !receiving) if ((m.isFileOrImage() && !deleted && !receiving)
|| (m.getType() == Message.TYPE_TEXT && !m.treatAsDownloadable()) || (m.getType() == Message.TYPE_TEXT && !m.treatAsDownloadable())
&& !unInitiatedButKnownSize && !unInitiatedButKnownSize
@ -1830,6 +1855,36 @@ public class ConversationFragment extends XmppFragment
case R.id.correct_message: case R.id.correct_message:
correctMessage(selectedMessage); correctMessage(selectedMessage);
return true; return true;
case R.id.retract_message:
new AlertDialog.Builder(activity)
.setTitle(R.string.retract_message)
.setMessage(R.string.retract_message_alert_title)
.setPositiveButton(R.string.yes, (dialog, whichButton) -> {
Message message = selectedMessage;
Element reactions = message.getReactions();
if (reactions != null) {
final Message previousReaction = conversation.findMessageReactingTo(reactions.getAttribute("id"), null);
if (previousReaction != null) reactions = previousReaction.getReactions();
for (Element el : reactions.getChildren()) {
if (message.getBody().endsWith(el.getContent())) {
reactions.removeChild(el);
}
}
message.setReactions(reactions);
if (previousReaction != null) {
previousReaction.setReactions(reactions);
activity.xmppConnectionService.updateMessage(previousReaction);
}
}
message.setBody(" ");
message.putEdited(message.getUuid(), message.getServerMsgId());
message.setServerMsgId(null);
message.setUuid(UUID.randomUUID().toString());
sendMessage(message);
})
.setNegativeButton(R.string.no, null).show();
return true;
case R.id.copy_message: case R.id.copy_message:
ShareUtil.copyToClipboard(activity, selectedMessage); ShareUtil.copyToClipboard(activity, selectedMessage);
return true; return true;

View file

@ -14,7 +14,7 @@ public class LocalizedContent {
public final String language; public final String language;
public final int count; public final int count;
private LocalizedContent(String content, String language, int count) { public LocalizedContent(String content, String language, int count) {
this.content = content; this.content = content;
this.language = language; this.language = language;
this.count = count; this.count = count;
@ -23,14 +23,14 @@ public class LocalizedContent {
public static LocalizedContent get(final Element element, String name) { public static LocalizedContent get(final Element element, String name) {
final HashMap<String, String> contents = new HashMap<>(); final HashMap<String, String> contents = new HashMap<>();
final String parentLanguage = element.getAttribute("xml:lang"); final String parentLanguage = element.getAttribute("xml:lang");
for(Element child : element.children) { for(Element child : element.getChildren()) {
if (name.equals(child.getName())) { if (name.equals(child.getName())) {
final String namespace = child.getNamespace(); final String namespace = child.getNamespace();
final String childLanguage = child.getAttribute("xml:lang"); final String childLanguage = child.getAttribute("xml:lang");
final String lang = childLanguage == null ? parentLanguage : childLanguage; final String lang = childLanguage == null ? parentLanguage : childLanguage;
final String content = child.getContent(); final String content = child.getContent();
if (content != null && (namespace == null || Namespace.JABBER_CLIENT.equals(namespace))) { if (namespace == null || "jabber:client".equals(namespace)) {
if (contents.put(lang, content) != null) { if (contents.put(lang, content == null ? "" : content) != null) {
//anything that has multiple contents for the same language is invalid //anything that has multiple contents for the same language is invalid
return null; return null;
} }

View file

@ -27,7 +27,7 @@
android:visible="false" /> android:visible="false" />
<item <item
android:id="@+id/quote_message" android:id="@+id/quote_message"
android:title="@string/quote" android:title="@string/reply"
android:visible="false" /> android:visible="false" />
<item <item
@ -38,6 +38,10 @@
android:id="@+id/correct_message" android:id="@+id/correct_message"
android:title="@string/correct_message" android:title="@string/correct_message"
android:visible="false" /> android:visible="false" />
<item
android:id="@+id/retract_message"
android:title="@string/retract_message"
android:visible="false" />
<item <item
android:id="@+id/copy_url" android:id="@+id/copy_url"
android:title="@string/copy_original_url" android:title="@string/copy_original_url"

View file

@ -544,6 +544,7 @@
<string name="no_accounts">(No activated accounts)</string> <string name="no_accounts">(No activated accounts)</string>
<string name="this_field_is_required">This field is required</string> <string name="this_field_is_required">This field is required</string>
<string name="correct_message">Correct message</string> <string name="correct_message">Correct message</string>
<string name="retract_message">Retract message</string>
<string name="send_corrected_message">Send corrected message</string> <string name="send_corrected_message">Send corrected message</string>
<string name="no_keys_just_confirm">You have already trusted this persons fingerprint. By selecting “Done” you are just confirming that %s is part of this group chat.</string> <string name="no_keys_just_confirm">You have already trusted this persons fingerprint. By selecting “Done” you are just confirming that %s is part of this group chat.</string>
<string name="this_account_is_disabled">You have disabled this account</string> <string name="this_account_is_disabled">You have disabled this account</string>
@ -1072,4 +1073,5 @@
<string name="contact_tag_general">General</string> <string name="contact_tag_general">General</string>
<string name="contact_tag_with_total">%1$s (%2$d)</string> <string name="contact_tag_with_total">%1$s (%2$d)</string>
<string name="refresh_feature_discovery">Refresh Feature Discovery</string> <string name="refresh_feature_discovery">Refresh Feature Discovery</string>
<string name="retract_message_alert_title">Do you really want to retract this message?</string>
</resources> </resources>