mirror of
https://github.com/oxen-io/session-android.git
synced 2025-06-09 17:48:33 +00:00
Update reactions UI.
This commit is contained in:
parent
1dd2a4e9c5
commit
73160d4d26
@ -42,6 +42,7 @@ import android.util.AttributeSet;
|
|||||||
import android.util.TypedValue;
|
import android.util.TypedValue;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.view.ViewTreeObserver;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
@ -97,6 +98,7 @@ import org.thoughtcrime.securesms.mms.Slide;
|
|||||||
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
||||||
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
|
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
|
||||||
import org.thoughtcrime.securesms.mms.TextSlide;
|
import org.thoughtcrime.securesms.mms.TextSlide;
|
||||||
|
import org.thoughtcrime.securesms.reactions.ReactionsConversationView;
|
||||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
|
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
|
||||||
@ -158,7 +160,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
|||||||
private AvatarImageView contactPhoto;
|
private AvatarImageView contactPhoto;
|
||||||
private AlertView alertView;
|
private AlertView alertView;
|
||||||
private ViewGroup container;
|
private ViewGroup container;
|
||||||
protected ViewGroup reactionsContainer;
|
protected ReactionsConversationView reactionsView;
|
||||||
|
|
||||||
private @NonNull Set<MessageRecord> batchSelected = new HashSet<>();
|
private @NonNull Set<MessageRecord> batchSelected = new HashSet<>();
|
||||||
private @NonNull Outliner outliner = new Outliner();
|
private @NonNull Outliner outliner = new Outliner();
|
||||||
@ -171,7 +173,6 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
|||||||
private Stub<StickerView> stickerStub;
|
private Stub<StickerView> stickerStub;
|
||||||
private Stub<ViewOnceMessageView> revealableStub;
|
private Stub<ViewOnceMessageView> revealableStub;
|
||||||
private @Nullable EventListener eventListener;
|
private @Nullable EventListener eventListener;
|
||||||
private ConversationItemReactionBubbles conversationItemReactionBubbles;
|
|
||||||
|
|
||||||
private int defaultBubbleColor;
|
private int defaultBubbleColor;
|
||||||
private int measureCalls;
|
private int measureCalls;
|
||||||
@ -226,9 +227,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
|||||||
this.quoteView = findViewById(R.id.quote_view);
|
this.quoteView = findViewById(R.id.quote_view);
|
||||||
this.container = findViewById(R.id.container);
|
this.container = findViewById(R.id.container);
|
||||||
this.reply = findViewById(R.id.reply_icon);
|
this.reply = findViewById(R.id.reply_icon);
|
||||||
this.reactionsContainer = findViewById(R.id.reactions_bubbles_container);
|
this.reactionsView = findViewById(R.id.reactions_view);
|
||||||
|
|
||||||
this.conversationItemReactionBubbles = new ConversationItemReactionBubbles(this.reactionsContainer);
|
|
||||||
|
|
||||||
setOnClickListener(new ClickListener(null));
|
setOnClickListener(new ClickListener(null));
|
||||||
|
|
||||||
@ -906,8 +905,23 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void setReactions(@NonNull MessageRecord current) {
|
private void setReactions(@NonNull MessageRecord current) {
|
||||||
conversationItemReactionBubbles.setReactions(current.getReactions());
|
if (current.getReactions().isEmpty()) {
|
||||||
reactionsContainer.setOnClickListener(v -> {
|
reactionsView.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyBubble.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
|
||||||
|
@Override
|
||||||
|
public void onGlobalLayout() {
|
||||||
|
setReactionsWithWidth(current);
|
||||||
|
bodyBubble.getViewTreeObserver().removeOnGlobalLayoutListener(this);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setReactionsWithWidth(@NonNull MessageRecord current) {
|
||||||
|
reactionsView.setReactions(current.getReactions(), bodyBubble.getWidth());
|
||||||
|
reactionsView.setOnClickListener(v -> {
|
||||||
if (eventListener == null) return;
|
if (eventListener == null) return;
|
||||||
|
|
||||||
eventListener.onReactionClicked(current.getId(), current.isMms());
|
eventListener.onReactionClicked(current.getId(), current.isMms());
|
||||||
|
@ -1,177 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.conversation;
|
|
||||||
|
|
||||||
import android.graphics.drawable.Drawable;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.core.util.Pair;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.R;
|
|
||||||
import org.thoughtcrime.securesms.components.emoji.EmojiImageView;
|
|
||||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
|
||||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
final class ConversationItemReactionBubbles {
|
|
||||||
|
|
||||||
private final ViewGroup reactionsContainer;
|
|
||||||
private final EmojiImageView primaryEmojiReaction;
|
|
||||||
private final EmojiImageView secondaryEmojiReaction;
|
|
||||||
|
|
||||||
ConversationItemReactionBubbles(@NonNull ViewGroup reactionsContainer) {
|
|
||||||
this.reactionsContainer = reactionsContainer;
|
|
||||||
this.primaryEmojiReaction = reactionsContainer.findViewById(R.id.reactions_bubbles_primary);
|
|
||||||
this.secondaryEmojiReaction = reactionsContainer.findViewById(R.id.reactions_bubbles_secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
void setReactions(@NonNull List<ReactionRecord> reactions) {
|
|
||||||
if (reactions.size() == 0) {
|
|
||||||
hideAllReactions();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Collection<ReactionInfo> reactionInfos = getReactionInfos(reactions);
|
|
||||||
|
|
||||||
if (reactionInfos.size() == 1) {
|
|
||||||
displaySingleReaction(reactionInfos.iterator().next());
|
|
||||||
} else {
|
|
||||||
displayMultipleReactions(reactionInfos);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static @NonNull Collection<ReactionInfo> getReactionInfos(@NonNull List<ReactionRecord> reactions) {
|
|
||||||
final Map<String, ReactionInfo> counters = new HashMap<>();
|
|
||||||
|
|
||||||
for (ReactionRecord reaction : reactions) {
|
|
||||||
|
|
||||||
ReactionInfo info = counters.get(reaction.getEmoji());
|
|
||||||
if (info == null) {
|
|
||||||
info = new ReactionInfo(reaction.getEmoji(),
|
|
||||||
1,
|
|
||||||
reaction.getDateReceived(),
|
|
||||||
Recipient.self().getId().equals(reaction.getAuthor()));
|
|
||||||
} else {
|
|
||||||
info = new ReactionInfo(reaction.getEmoji(),
|
|
||||||
info.count + 1,
|
|
||||||
Math.max(info.lastSeen, reaction.getDateReceived()),
|
|
||||||
info.userWasSender || Recipient.self().getId().equals(reaction.getAuthor()));
|
|
||||||
}
|
|
||||||
|
|
||||||
counters.put(reaction.getEmoji(), info);
|
|
||||||
}
|
|
||||||
|
|
||||||
return counters.values();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void hideAllReactions() {
|
|
||||||
reactionsContainer.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void displaySingleReaction(@NonNull ReactionInfo reactionInfo) {
|
|
||||||
reactionsContainer.setVisibility(View.VISIBLE);
|
|
||||||
primaryEmojiReaction.setVisibility(View.VISIBLE);
|
|
||||||
secondaryEmojiReaction.setVisibility(View.GONE);
|
|
||||||
|
|
||||||
primaryEmojiReaction.setImageEmoji(reactionInfo.emoji);
|
|
||||||
primaryEmojiReaction.setBackground(getBackgroundDrawableForReactionBubble(reactionInfo));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void displayMultipleReactions(@NonNull Collection<ReactionInfo> reactionInfos) {
|
|
||||||
reactionsContainer.setVisibility(View.VISIBLE);
|
|
||||||
primaryEmojiReaction.setVisibility(View.VISIBLE);
|
|
||||||
secondaryEmojiReaction.setVisibility(View.VISIBLE);
|
|
||||||
|
|
||||||
Pair<ReactionInfo, ReactionInfo> primaryAndSecondaryReactions = getPrimaryAndSecondaryReactions(reactionInfos);
|
|
||||||
primaryEmojiReaction.setImageEmoji(primaryAndSecondaryReactions.first.emoji);
|
|
||||||
primaryEmojiReaction.setBackground(getBackgroundDrawableForReactionBubble(primaryAndSecondaryReactions.first));
|
|
||||||
secondaryEmojiReaction.setImageEmoji(primaryAndSecondaryReactions.second.emoji);
|
|
||||||
secondaryEmojiReaction.setBackground(getBackgroundDrawableForReactionBubble(primaryAndSecondaryReactions.second));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Drawable getBackgroundDrawableForReactionBubble(@NonNull ReactionInfo reactionInfo) {
|
|
||||||
return ThemeUtil.getThemedDrawable(reactionsContainer.getContext(),
|
|
||||||
reactionInfo.userWasSender ? R.attr.reactions_sent_background : R.attr.reactions_recv_background);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Pair<ReactionInfo, ReactionInfo> getPrimaryAndSecondaryReactions(@NonNull Collection<ReactionInfo> reactionInfos) {
|
|
||||||
ReactionInfo mostPopular = null;
|
|
||||||
ReactionInfo latestReaction = null;
|
|
||||||
ReactionInfo secondLatestReaction = null;
|
|
||||||
ReactionInfo ourReaction = null;
|
|
||||||
|
|
||||||
for (ReactionInfo current : reactionInfos) {
|
|
||||||
if (current.userWasSender) {
|
|
||||||
ourReaction = current;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mostPopular == null) {
|
|
||||||
mostPopular = current;
|
|
||||||
} else if (mostPopular.count < current.count) {
|
|
||||||
mostPopular = current;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (latestReaction == null) {
|
|
||||||
latestReaction = current;
|
|
||||||
} else if (latestReaction.lastSeen < current.lastSeen) {
|
|
||||||
|
|
||||||
if (current.count == mostPopular.count) {
|
|
||||||
mostPopular = current;
|
|
||||||
}
|
|
||||||
|
|
||||||
secondLatestReaction = latestReaction;
|
|
||||||
latestReaction = current;
|
|
||||||
} else if (secondLatestReaction == null) {
|
|
||||||
secondLatestReaction = current;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mostPopular == null) {
|
|
||||||
throw new AssertionError("getPrimaryAndSecondaryReactions was called with an empty list.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ourReaction != null && !mostPopular.equals(ourReaction)) {
|
|
||||||
return Pair.create(mostPopular, ourReaction);
|
|
||||||
} else {
|
|
||||||
return Pair.create(mostPopular, mostPopular.equals(latestReaction) ? secondLatestReaction : latestReaction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class ReactionInfo {
|
|
||||||
private final String emoji;
|
|
||||||
private final int count;
|
|
||||||
private final long lastSeen;
|
|
||||||
private final boolean userWasSender;
|
|
||||||
|
|
||||||
private ReactionInfo(@NonNull String emoji, int count, long lastSeen, boolean userWasSender) {
|
|
||||||
this.emoji = emoji;
|
|
||||||
this.count = count;
|
|
||||||
this.lastSeen = lastSeen;
|
|
||||||
this.userWasSender = userWasSender;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
return Objects.hash(emoji, count, lastSeen, userWasSender);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(@Nullable Object obj) {
|
|
||||||
if (!(obj instanceof ReactionInfo)) return false;
|
|
||||||
|
|
||||||
ReactionInfo other = (ReactionInfo) obj;
|
|
||||||
|
|
||||||
return other.emoji.equals(emoji) &&
|
|
||||||
other.count == count &&
|
|
||||||
other.lastSeen == lastSeen &&
|
|
||||||
other.userWasSender == userWasSender;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -33,7 +33,7 @@ final class ConversationSwipeAnimationHelper {
|
|||||||
float progress = dx / TRIGGER_DX;
|
float progress = dx / TRIGGER_DX;
|
||||||
|
|
||||||
updateBodyBubbleTransition(conversationItem.bodyBubble, dx, sign);
|
updateBodyBubbleTransition(conversationItem.bodyBubble, dx, sign);
|
||||||
updateReactionsTransition(conversationItem.reactionsContainer, dx, sign);
|
updateReactionsTransition(conversationItem.reactionsView, dx, sign);
|
||||||
updateReplyIconTransition(conversationItem.reply, dx, progress, sign);
|
updateReplyIconTransition(conversationItem.reply, dx, progress, sign);
|
||||||
updateContactPhotoHolderTransition(conversationItem.contactPhotoHolder, progress, sign);
|
updateContactPhotoHolderTransition(conversationItem.contactPhotoHolder, progress, sign);
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,8 @@ import androidx.annotation.NonNull;
|
|||||||
|
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public class ReactionRecord {
|
public class ReactionRecord {
|
||||||
private final String emoji;
|
private final String emoji;
|
||||||
private final RecipientId author;
|
private final RecipientId author;
|
||||||
@ -36,4 +38,20 @@ public class ReactionRecord {
|
|||||||
public long getDateReceived() {
|
public long getDateReceived() {
|
||||||
return dateReceived;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,8 @@ public class MegaphoneRepository {
|
|||||||
@MainThread
|
@MainThread
|
||||||
public void onFirstEverAppLaunch() {
|
public void onFirstEverAppLaunch() {
|
||||||
executor.execute(() -> {
|
executor.execute(() -> {
|
||||||
// Future megaphones we don't want to show to new users should get marked as finished here.
|
database.markFinished(Event.REACTIONS);
|
||||||
|
resetDatabaseCache();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,198 @@
|
|||||||
|
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 com.annimon.stream.Stream;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||||
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
|
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||||
|
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class ReactionsConversationView extends LinearLayout {
|
||||||
|
|
||||||
|
private static final int OUTER_MARGIN = ViewUtil.dpToPx(6);
|
||||||
|
|
||||||
|
private boolean outgoing;
|
||||||
|
private List<ReactionRecord> records;
|
||||||
|
|
||||||
|
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) {
|
||||||
|
records = new ArrayList<>();
|
||||||
|
|
||||||
|
if (attrs != null) {
|
||||||
|
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.ReactionsConversationView, 0, 0);
|
||||||
|
outgoing = typedArray.getBoolean(R.styleable.ReactionsConversationView_rcv_outgoing, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear() {
|
||||||
|
removeAllViews();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setReactions(@NonNull List<ReactionRecord> records, int bubbleWidth) {
|
||||||
|
if (records.equals(this.records)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.records.clear();
|
||||||
|
this.records.addAll(records);
|
||||||
|
|
||||||
|
List<Reaction> reactions = buildSortedReactionsList(records);
|
||||||
|
|
||||||
|
removeAllViews();
|
||||||
|
|
||||||
|
for (Reaction reaction : reactions) {
|
||||||
|
addView(buildPill(getContext(), this, reaction));
|
||||||
|
}
|
||||||
|
|
||||||
|
measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
|
||||||
|
|
||||||
|
int railWidth = getMeasuredWidth();
|
||||||
|
|
||||||
|
if (railWidth < (bubbleWidth - OUTER_MARGIN)) {
|
||||||
|
int margin = (bubbleWidth - railWidth - OUTER_MARGIN);
|
||||||
|
|
||||||
|
if (outgoing) {
|
||||||
|
ViewUtil.setRightMargin(this, margin);
|
||||||
|
} else {
|
||||||
|
ViewUtil.setLeftMargin(this, margin);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (outgoing) {
|
||||||
|
ViewUtil.setRightMargin(this, OUTER_MARGIN);
|
||||||
|
} else {
|
||||||
|
ViewUtil.setLeftMargin(this, OUTER_MARGIN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static @NonNull List<Reaction> buildSortedReactionsList(@NonNull List<ReactionRecord> records) {
|
||||||
|
Map<String, Reaction> counters = new LinkedHashMap<>();
|
||||||
|
RecipientId selfId = Recipient.self().getId();
|
||||||
|
|
||||||
|
for (ReactionRecord record : records) {
|
||||||
|
Reaction info = counters.get(record.getEmoji());
|
||||||
|
|
||||||
|
if (info == null) {
|
||||||
|
info = new Reaction(record.getEmoji(), 1, record.getDateReceived(), selfId.equals(record.getAuthor()));
|
||||||
|
} else {
|
||||||
|
info.update(record.getDateReceived(), selfId.equals(record.getAuthor()));
|
||||||
|
}
|
||||||
|
|
||||||
|
counters.put(record.getEmoji(), info);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Reaction> reactions = new ArrayList<>(counters.values());
|
||||||
|
|
||||||
|
Collections.sort(reactions, Collections.reverseOrder());
|
||||||
|
|
||||||
|
if (reactions.size() > 3) {
|
||||||
|
List<Reaction> shortened = new ArrayList<>(3);
|
||||||
|
shortened.add(reactions.get(0));
|
||||||
|
shortened.add(reactions.get(1));
|
||||||
|
shortened.add(Stream.of(reactions).skip(2).reduce(new Reaction(null, 0, 0, false), Reaction::merge));
|
||||||
|
|
||||||
|
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);
|
||||||
|
TextView 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.emoji != null) {
|
||||||
|
emojiView.setText(reaction.emoji);
|
||||||
|
|
||||||
|
if (reaction.count > 1) {
|
||||||
|
countView.setText(String.valueOf(reaction.count));
|
||||||
|
} else {
|
||||||
|
countView.setVisibility(GONE);
|
||||||
|
spacer.setVisibility(GONE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emojiView.setVisibility(GONE);
|
||||||
|
spacer.setVisibility(GONE);
|
||||||
|
countView.setText(context.getString(R.string.ReactionsConversationView_plus, reaction.count));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reaction.userWasSender) {
|
||||||
|
root.setBackground(ThemeUtil.getThemedDrawable(context, R.attr.reactions_pill_selected_background));
|
||||||
|
countView.setTextColor(ThemeUtil.getThemedColor(context, R.attr.reactions_pill_selected_text_color));
|
||||||
|
} else {
|
||||||
|
root.setBackground(ThemeUtil.getThemedDrawable(context, R.attr.reactions_pill_background));
|
||||||
|
}
|
||||||
|
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Reaction implements Comparable<Reaction> {
|
||||||
|
private String emoji;
|
||||||
|
private int count;
|
||||||
|
private long lastSeen;
|
||||||
|
private boolean userWasSender;
|
||||||
|
|
||||||
|
Reaction(@Nullable String emoji, int count, long lastSeen, boolean userWasSender) {
|
||||||
|
this.emoji = emoji;
|
||||||
|
this.count = count;
|
||||||
|
this.lastSeen = lastSeen;
|
||||||
|
this.userWasSender = userWasSender;
|
||||||
|
}
|
||||||
|
|
||||||
|
void update(long lastSeen, boolean userWasSender) {
|
||||||
|
this.count = this.count + 1;
|
||||||
|
this.lastSeen = Math.max(this.lastSeen, lastSeen);
|
||||||
|
this.userWasSender = this.userWasSender || userWasSender;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull Reaction merge(@NonNull Reaction other) {
|
||||||
|
this.count = this.count + other.count;
|
||||||
|
this.lastSeen = Math.max(this.lastSeen, other.lastSeen);
|
||||||
|
this.userWasSender = this.userWasSender || other.userWasSender;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(Reaction rhs) {
|
||||||
|
Reaction lhs = this;
|
||||||
|
|
||||||
|
if (lhs.count != rhs.count) {
|
||||||
|
return Integer.compare(lhs.count, rhs.count);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Long.compare(lhs.lastSeen, rhs.lastSeen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -233,6 +233,16 @@ public class ViewUtil {
|
|||||||
view.requestLayout();
|
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) {
|
public static void setTopMargin(@NonNull View view, int margin) {
|
||||||
((ViewGroup.MarginLayoutParams) view.getLayoutParams()).topMargin = margin;
|
((ViewGroup.MarginLayoutParams) view.getLayoutParams()).topMargin = margin;
|
||||||
view.requestLayout();
|
view.requestLayout();
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
android:shape="oval">
|
<corners android:radius="1000dp" />
|
||||||
<solid android:color="@color/core_grey_75" />
|
<solid android:color="@color/core_grey_75" />
|
||||||
<stroke
|
<stroke android:color="@color/core_grey_95" android:width="1dp" />
|
||||||
android:width="1dp"
|
|
||||||
android:color="@color/core_grey_95" />
|
|
||||||
</shape>
|
</shape>
|
@ -1,8 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
android:shape="oval">
|
<corners android:radius="1000dp" />
|
||||||
<solid android:color="@color/core_grey_05" />
|
<solid android:color="@color/core_grey_05" />
|
||||||
<stroke
|
<stroke android:color="@color/white" android:width="1dp" />
|
||||||
android:width="1dp"
|
|
||||||
android:color="@color/white" />
|
|
||||||
</shape>
|
</shape>
|
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<corners android:radius="1000dp" />
|
||||||
|
<solid android:color="@color/core_grey_60" />
|
||||||
|
<stroke android:color="@color/core_grey_95" android:width="1dp" />
|
||||||
|
</shape>
|
@ -1,8 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
android:shape="oval">
|
<corners android:radius="1000dp" />
|
||||||
<solid android:color="@color/core_grey_25" />
|
<solid android:color="@color/core_grey_25" />
|
||||||
<stroke
|
<stroke android:color="@color/white" android:width="1dp" />
|
||||||
android:width="1dp"
|
|
||||||
android:color="@color/white" />
|
|
||||||
</shape>
|
</shape>
|
@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:shape="oval">
|
|
||||||
<solid android:color="@color/core_grey_45" />
|
|
||||||
<stroke
|
|
||||||
android:width="1dp"
|
|
||||||
android:color="@color/core_grey_95" />
|
|
||||||
</shape>
|
|
@ -200,7 +200,7 @@
|
|||||||
android:layout_marginStart="@dimen/message_bubble_horizontal_padding"
|
android:layout_marginStart="@dimen/message_bubble_horizontal_padding"
|
||||||
android:layout_marginEnd="@dimen/message_bubble_horizontal_padding"
|
android:layout_marginEnd="@dimen/message_bubble_horizontal_padding"
|
||||||
android:layout_marginBottom="@dimen/message_bubble_bottom_padding"
|
android:layout_marginBottom="@dimen/message_bubble_bottom_padding"
|
||||||
android:gravity="end"
|
android:gravity="start"
|
||||||
android:clipChildren="false"
|
android:clipChildren="false"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
android:alpha="0.7"
|
android:alpha="0.7"
|
||||||
@ -214,7 +214,7 @@
|
|||||||
android:layout_marginTop="6dp"
|
android:layout_marginTop="6dp"
|
||||||
android:layout_marginStart="@dimen/message_bubble_horizontal_padding"
|
android:layout_marginStart="@dimen/message_bubble_horizontal_padding"
|
||||||
android:layout_marginEnd="@dimen/message_bubble_horizontal_padding"
|
android:layout_marginEnd="@dimen/message_bubble_horizontal_padding"
|
||||||
android:gravity="end"
|
android:gravity="start"
|
||||||
android:clipChildren="false"
|
android:clipChildren="false"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
@ -231,35 +231,15 @@
|
|||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:gravity="center_vertical"/>
|
android:gravity="center_vertical"/>
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.components.MaxHeightFrameLayout
|
<org.thoughtcrime.securesms.reactions.ReactionsConversationView
|
||||||
android:id="@+id/reactions_bubbles_container"
|
android:id="@+id/reactions_view"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="0dip"
|
android:layout_height="wrap_content"
|
||||||
android:layout_alignTop="@id/body_bubble"
|
android:orientation="horizontal"
|
||||||
android:layout_alignBottom="@id/body_bubble"
|
android:layout_marginTop="-8dp"
|
||||||
android:layout_marginStart="@dimen/reactions_bubble_margin"
|
android:layout_below="@id/body_bubble"
|
||||||
android:layout_toEndOf="@id/body_bubble"
|
android:layout_alignStart="@id/body_bubble"
|
||||||
android:visibility="gone"
|
app:rcv_outgoing="false"/>
|
||||||
app:mhfl_maxHeight="@dimen/reactions_bubble_container_max_height"
|
|
||||||
tools:visibility="visible">
|
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
|
||||||
android:id="@+id/reactions_bubbles_secondary"
|
|
||||||
android:layout_width="@dimen/reactions_bubble_size"
|
|
||||||
android:layout_height="@dimen/reactions_bubble_size"
|
|
||||||
android:layout_gravity="bottom"
|
|
||||||
android:padding="6dp"
|
|
||||||
android:background="?attr/reactions_recv_background" />
|
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
|
||||||
android:id="@+id/reactions_bubbles_primary"
|
|
||||||
android:layout_width="@dimen/reactions_bubble_size"
|
|
||||||
android:layout_height="@dimen/reactions_bubble_size"
|
|
||||||
android:layout_gravity="top"
|
|
||||||
android:padding="6dp"
|
|
||||||
android:background="?attr/reactions_recv_background" />
|
|
||||||
|
|
||||||
</org.thoughtcrime.securesms.components.MaxHeightFrameLayout>
|
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
</org.thoughtcrime.securesms.conversation.ConversationItem>
|
</org.thoughtcrime.securesms.conversation.ConversationItem>
|
||||||
|
@ -188,35 +188,15 @@
|
|||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:paddingBottom="4dp" />
|
android:paddingBottom="4dp" />
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.components.MaxHeightFrameLayout
|
<org.thoughtcrime.securesms.reactions.ReactionsConversationView
|
||||||
android:id="@+id/reactions_bubbles_container"
|
android:id="@+id/reactions_view"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="0dip"
|
android:layout_height="wrap_content"
|
||||||
android:layout_alignTop="@id/body_bubble"
|
android:orientation="horizontal"
|
||||||
android:layout_alignBottom="@id/body_bubble"
|
android:layout_marginTop="-8dp"
|
||||||
android:layout_marginEnd="@dimen/reactions_bubble_margin"
|
android:layout_below="@id/body_bubble"
|
||||||
android:layout_toStartOf="@id/body_bubble"
|
android:layout_alignEnd="@id/body_bubble"
|
||||||
android:visibility="gone"
|
app:rcv_outgoing="true"/>
|
||||||
app:mhfl_maxHeight="@dimen/reactions_bubble_container_max_height"
|
|
||||||
tools:visibility="visible">
|
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
|
||||||
android:id="@+id/reactions_bubbles_secondary"
|
|
||||||
android:layout_width="@dimen/reactions_bubble_size"
|
|
||||||
android:layout_height="@dimen/reactions_bubble_size"
|
|
||||||
android:layout_gravity="bottom"
|
|
||||||
android:padding="6dp"
|
|
||||||
android:background="?attr/reactions_recv_background" />
|
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
|
||||||
android:id="@+id/reactions_bubbles_primary"
|
|
||||||
android:layout_width="@dimen/reactions_bubble_size"
|
|
||||||
android:layout_height="@dimen/reactions_bubble_size"
|
|
||||||
android:layout_gravity="top"
|
|
||||||
android:padding="6dp"
|
|
||||||
android:background="?attr/reactions_recv_background" />
|
|
||||||
|
|
||||||
</org.thoughtcrime.securesms.components.MaxHeightFrameLayout>
|
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
|
35
app/src/main/res/layout/reactions_pill.xml
Normal file
35
app/src/main/res/layout/reactions_pill.xml
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingStart="7dp"
|
||||||
|
android:paddingEnd="7dp"
|
||||||
|
android:paddingTop="5dp"
|
||||||
|
android:paddingBottom="5dp"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||||
|
android:id="@+id/reactions_pill_emoji"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="16dp"/>
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:id="@+id/reactions_pill_spacer"
|
||||||
|
android:layout_width="3dp"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/reactions_pill_count"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="1dp"
|
||||||
|
android:textSize="14dp"
|
||||||
|
android:fontFamily="sans-serif-medium"
|
||||||
|
android:textColor="?reactions_pill_text_color"
|
||||||
|
tools:text="23" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
@ -97,11 +97,13 @@
|
|||||||
<attr name="emoji_category_emoticons" format="reference"/>
|
<attr name="emoji_category_emoticons" format="reference"/>
|
||||||
<attr name="emoji_variation_selector_background" format="reference|color" />
|
<attr name="emoji_variation_selector_background" format="reference|color" />
|
||||||
|
|
||||||
<attr name="reactions_sent_background" format="reference" />
|
|
||||||
<attr name="reactions_recv_background" format="reference" />
|
|
||||||
<attr name="reactions_overlay_old_background" format="reference" />
|
<attr name="reactions_overlay_old_background" format="reference" />
|
||||||
<attr name="reactions_overlay_scrubber_background" format="reference" />
|
<attr name="reactions_overlay_scrubber_background" format="reference" />
|
||||||
<attr name="reactions_bottom_dialog_fragment_emoji_selected" format="reference" />
|
<attr name="reactions_bottom_dialog_fragment_emoji_selected" format="reference" />
|
||||||
|
<attr name="reactions_pill_background" format="reference" />
|
||||||
|
<attr name="reactions_pill_selected_background" format="reference" />
|
||||||
|
<attr name="reactions_pill_text_color" format="color" />
|
||||||
|
<attr name="reactions_pill_selected_text_color" format="color" />
|
||||||
|
|
||||||
<attr name="camera_button_style" />
|
<attr name="camera_button_style" />
|
||||||
|
|
||||||
@ -447,4 +449,7 @@
|
|||||||
<attr name="otv_cornerRadius" format="dimension|reference" />
|
<attr name="otv_cornerRadius" format="dimension|reference" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
|
|
||||||
|
<declare-styleable name="ReactionsConversationView">
|
||||||
|
<attr name="rcv_outgoing" format="boolean" />
|
||||||
|
</declare-styleable>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -666,6 +666,9 @@
|
|||||||
<string name="RatingManager_later">Later</string>
|
<string name="RatingManager_later">Later</string>
|
||||||
<string name="RatingManager_whoops_the_play_store_app_does_not_appear_to_be_installed">Whoops, the Play Store app does not appear to be installed on your device.</string>
|
<string name="RatingManager_whoops_the_play_store_app_does_not_appear_to_be_installed">Whoops, the Play Store app does not appear to be installed on your device.</string>
|
||||||
|
|
||||||
|
<!-- ReactionsConversationView -->
|
||||||
|
<string name="ReactionsConversationView_plus">+%1$d</string>
|
||||||
|
|
||||||
<!-- RecipientPreferencesActivity -->
|
<!-- RecipientPreferencesActivity -->
|
||||||
<string name="RecipientPreferenceActivity_block_this_contact_question">Block this contact?</string>
|
<string name="RecipientPreferenceActivity_block_this_contact_question">Block this contact?</string>
|
||||||
<string name="RecipientPreferenceActivity_you_will_no_longer_receive_messages_and_calls_from_this_contact">You will no longer receive messages and calls from this contact.</string>
|
<string name="RecipientPreferenceActivity_you_will_no_longer_receive_messages_and_calls_from_this_contact">You will no longer receive messages and calls from this contact.</string>
|
||||||
|
@ -270,11 +270,13 @@
|
|||||||
<item name="emoji_category_emoticons">@drawable/ic_emoji_emoticon_light_20</item>
|
<item name="emoji_category_emoticons">@drawable/ic_emoji_emoticon_light_20</item>
|
||||||
<item name="emoji_variation_selector_background">@drawable/emoji_variation_selector_background_light</item>
|
<item name="emoji_variation_selector_background">@drawable/emoji_variation_selector_background_light</item>
|
||||||
|
|
||||||
<item name="reactions_recv_background">@drawable/reactions_recv_background_light</item>
|
|
||||||
<item name="reactions_sent_background">@drawable/reactions_send_background_light</item>
|
|
||||||
<item name="reactions_overlay_old_background">@drawable/reactions_old_background_dark</item>
|
<item name="reactions_overlay_old_background">@drawable/reactions_old_background_dark</item>
|
||||||
<item name="reactions_overlay_scrubber_background">@drawable/conversation_reaction_overlay_background_dark</item>
|
<item name="reactions_overlay_scrubber_background">@drawable/conversation_reaction_overlay_background_dark</item>
|
||||||
<item name="reactions_bottom_dialog_fragment_emoji_selected">@drawable/reactions_bottom_sheet_dialog_fragment_emoji_item_selected_light</item>
|
<item name="reactions_bottom_dialog_fragment_emoji_selected">@drawable/reactions_bottom_sheet_dialog_fragment_emoji_item_selected_light</item>
|
||||||
|
<item name="reactions_pill_background">@drawable/reaction_pill_background_light</item>
|
||||||
|
<item name="reactions_pill_selected_background">@drawable/reaction_pill_background_selected_light</item>
|
||||||
|
<item name="reactions_pill_text_color">@color/core_grey_60</item>
|
||||||
|
<item name="reactions_pill_selected_text_color">@color/core_grey_75</item>
|
||||||
|
|
||||||
<item name="conversation_item_bubble_background">@color/core_grey_05</item>
|
<item name="conversation_item_bubble_background">@color/core_grey_05</item>
|
||||||
<item name="conversation_item_sent_text_primary_color">@color/core_grey_90</item>
|
<item name="conversation_item_sent_text_primary_color">@color/core_grey_90</item>
|
||||||
@ -536,12 +538,13 @@
|
|||||||
<item name="conversation_title_color">@color/transparent_white_90</item>
|
<item name="conversation_title_color">@color/transparent_white_90</item>
|
||||||
<item name="conversation_subtitle_color">@color/transparent_white_80</item>
|
<item name="conversation_subtitle_color">@color/transparent_white_80</item>
|
||||||
|
|
||||||
<item name="reactions_recv_background">@drawable/reactions_recv_background_dark</item>
|
|
||||||
<item name="reactions_sent_background">@drawable/reactions_send_background_dark</item>
|
|
||||||
<item name="reactions_overlay_old_background">@drawable/reactions_old_background_dark</item>
|
<item name="reactions_overlay_old_background">@drawable/reactions_old_background_dark</item>
|
||||||
<item name="reactions_overlay_scrubber_background">@drawable/conversation_reaction_overlay_background_dark</item>
|
<item name="reactions_overlay_scrubber_background">@drawable/conversation_reaction_overlay_background_dark</item>
|
||||||
<item name="reactions_bottom_dialog_fragment_emoji_selected">@drawable/reactions_bottom_sheet_dialog_fragment_emoji_item_selected_dark</item>
|
<item name="reactions_bottom_dialog_fragment_emoji_selected">@drawable/reactions_bottom_sheet_dialog_fragment_emoji_item_selected_dark</item>
|
||||||
|
<item name="reactions_pill_background">@drawable/reaction_pill_background_dark</item>
|
||||||
|
<item name="reactions_pill_selected_background">@drawable/reaction_pill_background_selected_dark</item>
|
||||||
|
<item name="reactions_pill_text_color">@color/core_grey_35</item>
|
||||||
|
<item name="reactions_pill_selected_text_color">@color/core_grey_15</item>
|
||||||
|
|
||||||
<item name="emoji_tab_strip_background">@color/core_grey_85</item>
|
<item name="emoji_tab_strip_background">@color/core_grey_85</item>
|
||||||
<item name="emoji_tab_indicator">@color/core_grey_65</item>
|
<item name="emoji_tab_indicator">@color/core_grey_65</item>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user