diff --git a/res/drawable/reaction_pill_background.xml b/res/drawable/reaction_pill_background.xml
new file mode 100644
index 0000000000..bad81d54c5
--- /dev/null
+++ b/res/drawable/reaction_pill_background.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/res/drawable/reaction_pill_background_selected.xml b/res/drawable/reaction_pill_background_selected.xml
new file mode 100644
index 0000000000..b52af9b3d2
--- /dev/null
+++ b/res/drawable/reaction_pill_background_selected.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/res/layout/conversation_item_received.xml b/res/layout/conversation_item_received.xml
index 0cf64875f4..99863ad7c7 100644
--- a/res/layout/conversation_item_received.xml
+++ b/res/layout/conversation_item_received.xml
@@ -220,5 +220,15 @@
+
+
diff --git a/res/layout/conversation_item_sent.xml b/res/layout/conversation_item_sent.xml
index 9cc41ebcb1..6937a1bbfe 100644
--- a/res/layout/conversation_item_sent.xml
+++ b/res/layout/conversation_item_sent.xml
@@ -197,6 +197,16 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/values-night/colors.xml b/res/values-night/colors.xml
new file mode 100644
index 0000000000..9c1530a47e
--- /dev/null
+++ b/res/values-night/colors.xml
@@ -0,0 +1,9 @@
+
+
+ @color/black
+ #80414347
+ @color/core_dark_05
+ #ff282828
+ @color/reaction_pill_border
+ @color/reaction_pill_text_color
+
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 93e3cc8bd7..923f7b103a 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -250,6 +250,13 @@
+
+
+
+
+
+
+
diff --git a/res/values/colors.xml b/res/values/colors.xml
index ea62242605..9c2fb900a3 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -75,4 +75,11 @@
#ff999999
+
+ #ffffff
+ #80f1f1f1
+ #545863
+ @color/gray5
+ @color/reaction_pill_border
+ @color/reaction_pill_text_color
diff --git a/res/values/strings.xml b/res/values/strings.xml
index f7ead36a3a..ab83e4e4b3 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -722,6 +722,8 @@
Symbols
Flags
+
+ +%1$d
Delete Old Messages
diff --git a/src/com/b44t/messenger/DcContext.java b/src/com/b44t/messenger/DcContext.java
index caa7ca647b..e612f5046f 100644
--- a/src/com/b44t/messenger/DcContext.java
+++ b/src/com/b44t/messenger/DcContext.java
@@ -11,6 +11,7 @@ public class DcContext {
public final static int DC_EVENT_ERROR = 400;
public final static int DC_EVENT_ERROR_SELF_NOT_IN_GROUP = 410;
public final static int DC_EVENT_MSGS_CHANGED = 2000;
+ public final static int DC_EVENT_REACTIONS_CHANGED = 2001;
public final static int DC_EVENT_INCOMING_MSG = 2005;
public final static int DC_EVENT_MSGS_NOTICED = 2008;
public final static int DC_EVENT_MSG_DELIVERED = 2010;
diff --git a/src/com/b44t/messenger/rpc/Reaction.java b/src/com/b44t/messenger/rpc/Reaction.java
new file mode 100644
index 0000000000..493cb4ec50
--- /dev/null
+++ b/src/com/b44t/messenger/rpc/Reaction.java
@@ -0,0 +1,39 @@
+package com.b44t.messenger.rpc;
+
+import androidx.annotation.Nullable;
+
+public class Reaction {
+ // The reaction emoji string.
+ private final String emoji;
+ // The count of users that have reacted with this reaction.
+ private final int count;
+ // true if self-account reacted with this reaction, false otherwise.
+ private final boolean isFromSelf;
+
+ public Reaction(String emoji, int count, boolean isFromSelf) {
+ this.emoji = emoji;
+ this.count = count;
+ this.isFromSelf = isFromSelf;
+ }
+
+ public String getEmoji() {
+ return emoji;
+ }
+
+ public int getCount() {
+ return count;
+ }
+
+ public boolean isFromSelf() {
+ return isFromSelf;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (obj instanceof Reaction) {
+ Reaction reaction = (Reaction) obj;
+ return emoji.equals(reaction.getEmoji()) && count == reaction.getCount() && isFromSelf == reaction.isFromSelf();
+ }
+ return false;
+ }
+}
diff --git a/src/com/b44t/messenger/rpc/Reactions.java b/src/com/b44t/messenger/rpc/Reactions.java
new file mode 100644
index 0000000000..7b88498683
--- /dev/null
+++ b/src/com/b44t/messenger/rpc/Reactions.java
@@ -0,0 +1,26 @@
+package com.b44t.messenger.rpc;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class Reactions {
+ // Map from a contact to it's reaction to message.
+ private final HashMap reactionsByContact;
+ // Unique reactions, sorted in descending order.
+ private final ArrayList reactions;
+
+ public Reactions(HashMap reactionsByContact, ArrayList reactions) {
+ this.reactionsByContact = reactionsByContact;
+ this.reactions = reactions;
+ }
+
+ public Map getReactionsByContact() {
+ return reactionsByContact;
+ }
+
+ public List getReactions() {
+ return reactions;
+ }
+}
diff --git a/src/com/b44t/messenger/rpc/Rpc.java b/src/com/b44t/messenger/rpc/Rpc.java
index 8f255c68c6..ab47023872 100644
--- a/src/com/b44t/messenger/rpc/Rpc.java
+++ b/src/com/b44t/messenger/rpc/Rpc.java
@@ -8,7 +8,6 @@
import com.google.gson.JsonSyntaxException;
import com.google.gson.reflect.TypeToken;
-import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
@@ -41,7 +40,7 @@ private void processResponse() throws JsonSyntaxException {
} else if (response.result != null) {
future.set(response.result);
} else {
- future.setException(new RpcException("Got JSON-RPC response witout result or error: " + jsonResponse));
+ future.setException(new RpcException("Got JSON-RPC response without result or error: " + jsonResponse));
}
}
@@ -100,9 +99,13 @@ public HttpResponse getHttpResponse(int accountId, String url) throws RpcExcepti
return gson.fromJson(getResult("get_http_response", accountId, url), HttpResponse.class);
}
+ public Reactions getMsgReactions(int accountId, int msgId) throws RpcException {
+ return gson.fromJson(getResult("get_message_reactions", accountId, msgId), Reactions.class);
+ }
+
private static class Request {
- public String jsonrpc = "2.0";
+ private final String jsonrpc = "2.0";
public String method;
public Object[] params;
public int id;
@@ -115,7 +118,7 @@ public Request(String method, Object[] params, int id) {
}
private static class Response {
- public int id = 0;
+ public int id;
public JsonElement result;
public JsonElement error;
diff --git a/src/org/thoughtcrime/securesms/BaseConversationItem.java b/src/org/thoughtcrime/securesms/BaseConversationItem.java
index f9fa5b2d75..f76d81bb5f 100644
--- a/src/org/thoughtcrime/securesms/BaseConversationItem.java
+++ b/src/org/thoughtcrime/securesms/BaseConversationItem.java
@@ -13,6 +13,7 @@
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcMsg;
+import com.b44t.messenger.rpc.Rpc;
import org.thoughtcrime.securesms.connect.DcHelper;
@@ -30,6 +31,7 @@ public abstract class BaseConversationItem extends LinearLayout
protected final Context context;
protected final DcContext dcContext;
+ protected final Rpc rpc;
protected @NonNull Set batchSelected = new HashSet<>();
@@ -39,6 +41,7 @@ public BaseConversationItem(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
this.dcContext = DcHelper.getContext(context);
+ this.rpc = DcHelper.getRpc(context);
}
protected void bind(@NonNull DcMsg messageRecord,
@@ -92,7 +95,7 @@ public void onClick(View v) {
}
protected class ClickListener implements View.OnClickListener {
- private OnClickListener parent;
+ private final OnClickListener parent;
ClickListener(@Nullable OnClickListener parent) {
this.parent = parent;
diff --git a/src/org/thoughtcrime/securesms/ConversationFragment.java b/src/org/thoughtcrime/securesms/ConversationFragment.java
index f18c42c1f0..ebafcb8d94 100644
--- a/src/org/thoughtcrime/securesms/ConversationFragment.java
+++ b/src/org/thoughtcrime/securesms/ConversationFragment.java
@@ -119,6 +119,7 @@ public void onCreate(Bundle icicle) {
DcEventCenter eventCenter = DcHelper.getEventCenter(getContext());
eventCenter.addObserver(DcContext.DC_EVENT_INCOMING_MSG, this);
eventCenter.addObserver(DcContext.DC_EVENT_MSGS_CHANGED, this);
+ eventCenter.addObserver(DcContext.DC_EVENT_REACTIONS_CHANGED, this);
eventCenter.addObserver(DcContext.DC_EVENT_MSG_DELIVERED, this);
eventCenter.addObserver(DcContext.DC_EVENT_MSG_FAILED, this);
eventCenter.addObserver(DcContext.DC_EVENT_MSG_READ, this);
@@ -972,6 +973,7 @@ public void handleEvent(@NonNull DcEvent event) {
}
break;
+ case DcContext.DC_EVENT_REACTIONS_CHANGED:
case DcContext.DC_EVENT_INCOMING_MSG:
case DcContext.DC_EVENT_MSG_DELIVERED:
case DcContext.DC_EVENT_MSG_FAILED:
diff --git a/src/org/thoughtcrime/securesms/ConversationItem.java b/src/org/thoughtcrime/securesms/ConversationItem.java
index f49a9923ac..bb1320a86d 100644
--- a/src/org/thoughtcrime/securesms/ConversationItem.java
+++ b/src/org/thoughtcrime/securesms/ConversationItem.java
@@ -39,6 +39,8 @@
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcMsg;
+import com.b44t.messenger.rpc.Reactions;
+import com.b44t.messenger.rpc.RpcException;
import org.thoughtcrime.securesms.audio.AudioSlidePlayer;
import org.thoughtcrime.securesms.components.AudioView;
@@ -59,6 +61,7 @@
import org.thoughtcrime.securesms.mms.SlideClickListener;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.mms.StickerSlide;
+import org.thoughtcrime.securesms.reactions.ReactionsConversationView;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.LongClickMovementMethod;
import org.thoughtcrime.securesms.util.MediaUtil;
@@ -97,6 +100,7 @@ public class ConversationItem extends BaseConversationItem
@Nullable private QuoteView quoteView;
private ConversationItemFooter footer;
private ConversationItemFooter stickerFooter;
+ private ReactionsConversationView reactionsView;
private TextView groupSender;
private View groupSenderHolder;
private AvatarImageView contactPhoto;
@@ -134,6 +138,7 @@ protected void onFinishInflate() {
this.bodyText = findViewById(R.id.conversation_item_body);
this.footer = findViewById(R.id.conversation_item_footer);
this.stickerFooter = findViewById(R.id.conversation_item_sticker_footer);
+ this.reactionsView = findViewById(R.id.reactions_view);
this.groupSender = findViewById(R.id.group_message_sender);
this.contactPhoto = findViewById(R.id.contact_photo);
this.contactPhotoHolder = findViewById(R.id.contact_photo_container);
@@ -185,6 +190,7 @@ public void bind(@NonNull DcMsg messageRecord,
setGroupMessageStatus();
setAuthor(messageRecord, showSender);
setMessageSpacing(context);
+ setReactions(messageRecord);
setFooter(messageRecord, locale);
setQuote(messageRecord);
}
@@ -667,6 +673,19 @@ private void setFooter(@NonNull DcMsg current, @NonNull Locale locale) {
activeFooter.setMessageRecord(current, locale);
}
+ private void setReactions(@NonNull DcMsg current) {
+ try {
+ Reactions reactions = rpc.getMsgReactions(dcContext.getAccountId(), current.getId());
+ if (reactions == null) {
+ reactionsView.clear();
+ } else {
+ reactionsView.setReactions(reactions.getReactions());
+ }
+ } catch (RpcException e) {
+ reactionsView.clear();
+ }
+ }
+
private ConversationItemFooter getActiveFooter(@NonNull DcMsg messageRecord) {
if (hasSticker(messageRecord)) {
return stickerFooter;
diff --git a/src/org/thoughtcrime/securesms/reactions/ReactionsConversationView.java b/src/org/thoughtcrime/securesms/reactions/ReactionsConversationView.java
new file mode 100644
index 0000000000..aa3651d625
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/reactions/ReactionsConversationView.java
@@ -0,0 +1,124 @@
+package org.thoughtcrime.securesms.reactions;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
+
+import com.b44t.messenger.rpc.Reaction;
+
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.components.emoji.EmojiImageView;
+import org.thoughtcrime.securesms.util.ViewUtil;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ReactionsConversationView extends LinearLayout {
+
+ // Normally 6dp, but we have 1dp left+right margin on the pills themselves
+ private static final int OUTER_MARGIN = ViewUtil.dpToPx(5);
+
+ private final List reactions = new ArrayList<>();
+ private boolean isIncoming;
+
+ public ReactionsConversationView(Context context) {
+ super(context);
+ init(null);
+ }
+
+ public ReactionsConversationView(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ init(attrs);
+ }
+
+ private void init(@Nullable AttributeSet attrs) {
+ if (attrs != null) {
+ TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.ReactionsConversationView, 0, 0);
+ isIncoming = typedArray.getInt(R.styleable.ReactionsConversationView_reaction_type, 0) == 2;
+ }
+ }
+
+ public void clear() {
+ this.reactions.clear();
+ removeAllViews();
+ }
+
+ public void setReactions(List reactions) {
+ if (reactions.equals(this.reactions)) {
+ return;
+ }
+
+ clear();
+ this.reactions.addAll(reactions);
+
+ for (Reaction reaction : buildShortenedReactionsList(this.reactions)) {
+ View pill = buildPill(getContext(), this, reaction);
+ addView(pill);
+ }
+
+ if (isIncoming) {
+ ViewUtil.setLeftMargin(this, OUTER_MARGIN);
+ } else {
+ ViewUtil.setRightMargin(this, OUTER_MARGIN);
+ }
+ }
+
+ private static @NonNull List buildShortenedReactionsList(@NonNull List reactions) {
+ if (reactions.size() > 3) {
+ List shortened = new ArrayList<>(3);
+ shortened.add(reactions.get(0));
+ shortened.add(reactions.get(1));
+ int count = 0;
+ boolean isFromSelf = false;
+ for (int index = 2; index < reactions.size(); index++) {
+ count += reactions.get(index).getCount();
+ isFromSelf = isFromSelf || reactions.get(index).isFromSelf();
+ }
+ shortened.add(new Reaction(null, count, isFromSelf));
+
+ return shortened;
+ } else {
+ return reactions;
+ }
+ }
+
+ private static View buildPill(@NonNull Context context, @NonNull ViewGroup parent, @NonNull Reaction reaction) {
+ View root = LayoutInflater.from(context).inflate(R.layout.reactions_pill, parent, false);
+ EmojiImageView emojiView = root.findViewById(R.id.reactions_pill_emoji);
+ TextView countView = root.findViewById(R.id.reactions_pill_count);
+ View spacer = root.findViewById(R.id.reactions_pill_spacer);
+
+ if (reaction.getEmoji() != null) {
+ emojiView.setImageEmoji(reaction.getEmoji());
+
+ if (reaction.getCount() > 1) {
+ countView.setText(String.valueOf(reaction.getCount()));
+ } else {
+ countView.setVisibility(GONE);
+ spacer.setVisibility(GONE);
+ }
+ } else {
+ emojiView.setVisibility(GONE);
+ spacer.setVisibility(GONE);
+ countView.setText(context.getString(R.string.ReactionsConversationView_plus, reaction.getCount()));
+ }
+
+ if (reaction.isFromSelf()) {
+ root.setBackground(ContextCompat.getDrawable(context, R.drawable.reaction_pill_background_selected));
+ countView.setTextColor(ContextCompat.getColor(context, R.color.reaction_pill_text_color_selected));
+ } else {
+ root.setBackground(ContextCompat.getDrawable(context, R.drawable.reaction_pill_background));
+ }
+
+ return root;
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/util/ViewUtil.java b/src/org/thoughtcrime/securesms/util/ViewUtil.java
index 22383668dc..7c032a5e68 100644
--- a/src/org/thoughtcrime/securesms/util/ViewUtil.java
+++ b/src/org/thoughtcrime/securesms/util/ViewUtil.java
@@ -254,6 +254,16 @@ public static void setLeftMargin(@NonNull View view, int margin) {
view.requestLayout();
}
+ public static void setRightMargin(@NonNull View view, int margin) {
+ if (ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_LTR) {
+ ((ViewGroup.MarginLayoutParams) view.getLayoutParams()).rightMargin = margin;
+ } else {
+ ((ViewGroup.MarginLayoutParams) view.getLayoutParams()).leftMargin = margin;
+ }
+ view.forceLayout();
+ view.requestLayout();
+ }
+
public static void setTopMargin(@NonNull View view, int margin) {
((ViewGroup.MarginLayoutParams) view.getLayoutParams()).topMargin = margin;
view.requestLayout();