package com.zulip.android.activities;
import android.content.Context;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.support.annotation.ColorInt;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.ViewCompat;
import android.support.v7.app.AppCompatDelegate;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.text.method.LinkMovementMethod;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TableRow;
import android.widget.TextView;
import com.j256.ormlite.stmt.UpdateBuilder;
import com.squareup.picasso.Callback;
import com.squareup.picasso.Picasso;
import com.zulip.android.R;
import com.zulip.android.ZulipApp;
import com.zulip.android.filters.NarrowFilterPM;
import com.zulip.android.filters.NarrowFilterStream;
import com.zulip.android.filters.NarrowListener;
import com.zulip.android.models.Message;
import com.zulip.android.models.MessageDateSeparator;
import com.zulip.android.models.MessageType;
import com.zulip.android.models.Person;
import com.zulip.android.models.Reaction;
import com.zulip.android.models.Stream;
import com.zulip.android.util.ActivityTransitionAnim;
import com.zulip.android.util.Constants;
import com.zulip.android.util.ConvertDpPx;
import com.zulip.android.util.DateMethods;
import com.zulip.android.util.MutedTopics;
import com.zulip.android.util.OnItemClickListener;
import com.zulip.android.util.UrlHelper;
import com.zulip.android.util.ZLog;
import com.zulip.android.viewholders.LoadingHolder;
import com.zulip.android.viewholders.MessageHeaderParent;
import com.zulip.android.viewholders.MessageHolder;
import com.zulip.android.viewholders.stickyheaders.RetrieveHeaderView;
import com.zulip.android.viewholders.stickyheaders.interfaces.StickyHeaderHandler;
import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Random;
import static com.zulip.android.util.ConvertDpPx.convertDpToPixel;
/**
* An adapter to bind the messages to a RecyclerView.
* This has two main ViewTypes {@link MessageHeaderParent.MessageHeaderHolder} and {@link MessageHolder}
* Each Message is inserted to its MessageHeader which are distinguished by the {@link Message#getIdForHolder()}
* saved in {@link MessageHeaderParent#getId()}
* <p>
* There are two ways to insert a message in this adapter one {@link RecyclerMessageAdapter#addOldMessage(Message, int, StringBuilder,MessageDateSeparator)}
* and second one {@link RecyclerMessageAdapter#addNewMessage(Message)}
* The first one is used to add old messages from the databases with {@link com.zulip.android.util.MessageListener.LoadPosition#BELOW}
* and {@link com.zulip.android.util.MessageListener.LoadPosition#INITIAL}. Messages are added from 1st index of the adapter and new
* headerParents are created if it doesn't matches the current header where the adding is being placed, this is done to match the UI as the web.
* In addNewMessages the messages are loaded in the bottom and new headers are created if it does not matches the last header.
*/
public class RecyclerMessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements StickyHeaderHandler{
public static final int VIEWTYPE_MESSAGE_HEADER = 1;
public static final int VIEWTYPE_HEADER = 3; //At position 0
private static final int VIEWTYPE_MESSAGE = 2;
private static final int VIEWTYPE_FOOTER = 4; //At end position
private static final int VIEWTYPE_DATE_SEPARATOR = 5;
private static final float HEIGHT_IN_DP = 48;
private static String privateHuddleText;
private boolean startedFromFilter;
private List<Object> items;
private ZulipApp zulipApp;
private MutedTopics mMutedTopics;
private Context context;
private NarrowListener narrowListener;
private
@ColorInt
int mDefaultStreamHeaderColor;
@ColorInt
private int streamMessageBackground;
@ColorInt
private int privateMessageBackground;
private OnItemClickListener onItemClickListener;
private int contextMenuItemSelectedPosition = RecyclerView.NO_POSITION;
private View footerView;
private View headerView;
private UpdateBuilder<Message, Object> updateBuilder;
private boolean isCurrentThemeNight;
private HashMap<Integer, Integer> defaultAvatarColorHMap;
// position of view (MessageHeaderParent) which float's on top
private int attachedHeaderAdapterPosition = RetrieveHeaderView.DEFAULT_VIEW_TYPE;
RecyclerMessageAdapter(List<Message> messageList, final Context context, boolean startedFromFilter) {
super();
items = new ArrayList<>();
zulipApp = ZulipApp.get();
mMutedTopics = MutedTopics.get();
this.context = context;
narrowListener = (NarrowListener) context;
this.startedFromFilter = startedFromFilter;
isCurrentThemeNight = (AppCompatDelegate.getDefaultNightMode() == AppCompatDelegate.MODE_NIGHT_YES);
mDefaultStreamHeaderColor = ContextCompat.getColor(context, R.color.stream_header);
privateMessageBackground = ContextCompat.getColor(context, R.color.private_background);
streamMessageBackground = ContextCompat.getColor(context, R.color.stream_background);
defaultAvatarColorHMap = new HashMap<>();
privateHuddleText = context.getResources().getString(R.string.huddle_text);
setupHeaderAndFooterViews();
onItemClickListener = new OnItemClickListener() {
@Override
public void onItemClick(int viewId, int position) {
switch (viewId) {
case R.id.displayRecipient: //StreamTV
if (position == RetrieveHeaderView.DEFAULT_VIEW_TYPE) {
//clicked on floating header
if (attachedHeaderAdapterPosition != RetrieveHeaderView.DEFAULT_VIEW_TYPE) {
position = attachedHeaderAdapterPosition;
} else {
return;
}
}
MessageHeaderParent messageHeaderParent = (MessageHeaderParent) getItem(position);
if (messageHeaderParent.getMessageType() == MessageType.PRIVATE_MESSAGE) {
Person[] recipientArray = messageHeaderParent.getRecipients(zulipApp);
narrowListener.onNarrow(new NarrowFilterPM(Arrays.asList(recipientArray)),
messageHeaderParent.getMessageId());
narrowListener.onNarrowFillSendBoxPrivate(recipientArray, false);
} else {
narrowListener.onNarrow(new NarrowFilterStream(Stream.getByName(zulipApp,
messageHeaderParent.getStream()), null),
messageHeaderParent.getMessageId());
narrowListener.onNarrowFillSendBoxStream(messageHeaderParent.getStream(), "", false);
}
break;
case R.id.instance: //Topic
if (position == RetrieveHeaderView.DEFAULT_VIEW_TYPE) {
//clicked on floating header
if (attachedHeaderAdapterPosition != RetrieveHeaderView.DEFAULT_VIEW_TYPE) {
position = attachedHeaderAdapterPosition;
} else {
return;
}
}
MessageHeaderParent messageParent = (MessageHeaderParent) getItem(position);
if (messageParent.getMessageType() == MessageType.STREAM_MESSAGE) {
narrowListener.onNarrow(new NarrowFilterStream(Stream.getByName(zulipApp,
messageParent.getStream()), messageParent.getSubject()),
messageParent.getMessageId());
narrowListener.onNarrowFillSendBoxStream(messageParent.getStream(), messageParent.getSubject(), false);
} else {
Person[] recipentArray = messageParent.getRecipients(zulipApp);
narrowListener.onNarrow(new NarrowFilterPM(Arrays.asList(recipentArray)),
messageParent.getMessageId());
narrowListener.onNarrowFillSendBoxPrivate(recipentArray, false);
}
break;
case R.id.senderTile: // Sender Tile
case R.id.contentView: //Main message
Message message = (Message) getItem(position);
narrowListener.onNarrowFillSendBox(message, true);
break;
case R.id.messageTile:
Message msg = (Message) getItem(position);
try {
int mID = msg.getID();
if (zulipApp.getPointer() < mID) {
zulipApp.syncPointer(mID);
}
} catch (NullPointerException e) {
ZLog.logException(e);
}
break;
default:
Log.e("onItemClick", "Click listener not setup for: " + context.getResources().getResourceName(viewId) + " at position - " + position);
}
}
@Override
public Message getMessageAtPosition(int position) {
if (getItem(position) instanceof Message) {
return (Message) getItem(position);
}
return null;
}
@Override
public MessageHeaderParent getMessageHeaderParentAtPosition(int position) {
if (getItem(position) instanceof MessageHeaderParent) {
return (MessageHeaderParent) getItem(position);
}
return null;
}
@Override
public void setContextItemSelectedPosition(int adapterPosition) {
contextMenuItemSelectedPosition = adapterPosition;
}
};
setupLists(messageList);
updateBuilder = zulipApp.getDao(Message.class).updateBuilder();
}
int getContextMenuItemSelectedPosition() {
return contextMenuItemSelectedPosition;
}
/**
* Add's a placeHolder value for Header and footer loading with values of 3-{@link #VIEWTYPE_HEADER} and 4-{@link #VIEWTYPE_FOOTER} respectively.
* So that for these placeHolder can be created a ViewHolder in {@link #onCreateViewHolder(ViewGroup, int)}
*/
private void setupHeaderAndFooterViews() {
items.add(0, VIEWTYPE_HEADER); //Placeholder for header
items.add(VIEWTYPE_FOOTER); //Placeholder for footer
notifyItemInserted(0);
notifyItemInserted(items.size() - 1);
}
private int[] getHeaderAndNextIndex(String id) {
//Return the next header, if this is the last header then returns the last index (loading view)
int indices[] = {-1, -1};
for (int i = 0; i < getItemCount(false); i++) {
if (items.get(i) instanceof MessageHeaderParent) {
MessageHeaderParent item = (MessageHeaderParent) items.get(i);
if (indices[0] != -1) {
indices[1] = i;
return indices;
}
if (item.getId().equals(id)) {
indices[0] = i;
}
}
}
return indices;
}
private void setupLists(List<Message> messageList) {
int headerParents = 0;
int dateSeparator = 0;
Calendar calendar = null;
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < messageList.size() - 1; i++) {
Message message = messageList.get(i);
//check for date separator
if (calendar == null || !DateMethods.isSameDay(calendar.getTime(), message.getTimestamp())) {
MessageDateSeparator separator = new MessageDateSeparator((calendar == null) ? null : calendar.getTime(), message.getTimestamp());
calendar = Calendar.getInstance();
calendar.setTime(message.getTimestamp());
headerParents = (addOldMessage(message, dateSeparator + i + headerParents, stringBuilder, separator)) ? headerParents + 1 : headerParents;
dateSeparator++;
} else {
headerParents = (addOldMessage(message, dateSeparator + i + headerParents, stringBuilder, null)) ? headerParents + 1 : headerParents;
}
}
setFooterShowing(false);
setHeaderShowing(false);
}
@Override
public int getItemViewType(int position) {
if (items.get(position) instanceof MessageHeaderParent)
return VIEWTYPE_MESSAGE_HEADER;
else if (items.get(position) instanceof Message)
return VIEWTYPE_MESSAGE;
else if (getItem(position) instanceof Integer && (Integer) getItem(position) == VIEWTYPE_HEADER)
return VIEWTYPE_HEADER;
else if (getItem(position) instanceof Integer && (Integer) getItem(position) == VIEWTYPE_FOOTER)
return VIEWTYPE_FOOTER;
else if (items.get(position) instanceof MessageDateSeparator)
return VIEWTYPE_DATE_SEPARATOR;
else {
Log.e("ItemError", "object: " + items.get(position).toString());
throw new RuntimeException("MESSAGE TYPE NOT KNOWN & Position:" + position);
}
}
/**
* Add an old message to the current list and add those messages to the existing messageHeaders if no
* messageHeader is found then create a new messageHeader
*
* @param message Message to be added
* @param messageAndHeadersCount Count of the (messages + messageHeaderParent) added in the loop from where this function is being called
* @param lastHolderId This is StringBuilder so as to make pass by reference work, the new lastHolderId is saved here if the value changes
* @return returns true if a new messageHeaderParent is created for this message so as to increment the count by where this function is being called.
*/
public boolean addOldMessage(Message message, int messageAndHeadersCount, StringBuilder lastHolderId, MessageDateSeparator dateSeparator) {
//check for date separator
if (dateSeparator != null) {
addNewDateSeparator(dateSeparator, messageAndHeadersCount + 1); //1 for LoadingHeader
}
if (!lastHolderId.toString().equals(message.getIdForHolder()) || lastHolderId.toString().equals("") || dateSeparator != null) {
MessageHeaderParent messageHeaderParent = new MessageHeaderParent((message.getStream() == null) ? null : message.getStream().getName(), message.getSubject(), message.getIdForHolder(), message);
messageHeaderParent.setMessageType(message.getType());
messageHeaderParent.setMessagesDate(message.getTimestamp());
messageHeaderParent.setDisplayRecipent(message.getDisplayRecipient(zulipApp));
if (message.getType() == MessageType.STREAM_MESSAGE) {
messageHeaderParent.setMute(mMutedTopics.isTopicMute(message));
}
messageHeaderParent.setColor((message.getStream() == null) ? mDefaultStreamHeaderColor : message.getStream().getParsedColor());
//1 for LoadingHeader
//check for date separator
items.add((dateSeparator != null) ? messageAndHeadersCount + 2 : messageAndHeadersCount + 1, messageHeaderParent);
notifyItemInserted((dateSeparator != null) ? messageAndHeadersCount + 2 : messageAndHeadersCount + 1);
items.add((dateSeparator != null) ? messageAndHeadersCount + 3 : messageAndHeadersCount + 2, message);
notifyItemInserted((dateSeparator != null) ? messageAndHeadersCount + 3 : messageAndHeadersCount + 2);
lastHolderId.setLength(0);
lastHolderId.append(messageHeaderParent.getId());
return true;
} else {
items.add(messageAndHeadersCount + 1, message);
notifyItemInserted(messageAndHeadersCount + 1);
return false;
}
}
/**
* Add a new message to the bottom of the list and create a new messageHeaderParent if last did not match this message
* Stream/subject or private recipients.
*
* @param message Message to be added
*/
public void addNewMessage(Message message) {
//check for date separator
Date lastSeparatorDate = getLastSeparatorRightDate();
if (lastSeparatorDate == null || !DateMethods.isSameDay(lastSeparatorDate, message.getTimestamp())) {
addNewDateSeparator(new MessageDateSeparator(lastSeparatorDate, message.getTimestamp()), getItemCount(true) - 1);
}
MessageHeaderParent item = null;
for (int i = getItemCount(false) - 1; i >= 1; i--) {
//Find the last header and check if it belongs to this message!
if (items.get(i) instanceof MessageHeaderParent) {
item = (MessageHeaderParent) items.get(i);
if (!item.getId().equals(message.getIdForHolder())
|| item.getMessagesTimestamp() == null
|| !DateMethods.isSameDay(item.getMessagesTimestamp(), message.getTimestamp())) {
item = null;
}
break;
}
}
if (item == null) {
item = createMessageHeader(message);
items.add(getItemCount(true) - 1, item);
notifyItemInserted(getItemCount(true) - 1);
}
items.add(getItemCount(true) - 1, message);
notifyItemInserted(getItemCount(true) - 1);
}
public void addNewHeader(int position, Message message) {
MessageHeaderParent item = createMessageHeader(message);
items.add(position, item);
notifyItemInserted(position);
if (getItem(position + 2) instanceof Message) {
// insert header with old topic for this message
Message prevMessage = (Message) getItem(position + 2);
MessageHeaderParent prevHeader = createMessageHeader(prevMessage);
items.add(position + 2, prevHeader);
notifyItemInserted(position + 2);
}
}
private MessageHeaderParent createMessageHeader(Message message) {
MessageHeaderParent header = new MessageHeaderParent((message.getStream() == null) ? null :
message.getStream().getName(), message.getSubject(), message.getIdForHolder(), message);
header.setMessageType(message.getType());
header.setDisplayRecipent(message.getDisplayRecipient(zulipApp));
header.setMessagesDate(message.getTimestamp());
if (message.getType() == MessageType.STREAM_MESSAGE)
header.setMute(mMutedTopics.isTopicMute(message));
header.setColor((message.getStream() == null) ? mDefaultStreamHeaderColor : message.getStream().getParsedColor());
return header;
}
/**
* Add's date separator at position
*
* @param separator which we want to add
* @param position add separator at this position in list
*/
private void addNewDateSeparator(MessageDateSeparator separator, int position) {
items.add(position, separator);
notifyItemInserted(position);
}
private Date getLastSeparatorRightDate() {
//get last date separator
MessageDateSeparator separator;
for (int i = items.size() - 1; i >= 0; i--) {
if (items.get(i) instanceof MessageDateSeparator) {
separator = (MessageDateSeparator) items.get(i);
return separator.getBelowMessageDate();
}
}
return null;
}
public RecyclerView.ViewHolder onCreateViewHolder(final ViewGroup parent, int viewType) {
switch (viewType) {
case VIEWTYPE_MESSAGE_HEADER:
MessageHeaderParent.MessageHeaderHolder holder = new MessageHeaderParent.MessageHeaderHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.message_header, parent, false));
holder.streamTextView.setText(privateHuddleText);
holder.streamTextView.setTextColor(Color.WHITE);
holder.setOnItemClickListener(onItemClickListener);
return holder;
case VIEWTYPE_MESSAGE:
View messageView = LayoutInflater.from(parent.getContext()).inflate(R.layout.message_tile, parent, false);
MessageHolder messageHolder = new MessageHolder(messageView);
messageHolder.setItemClickListener(onItemClickListener);
if (isCurrentThemeNight) {
messageHolder.leftBar.setVisibility(View.GONE);
}
return messageHolder;
case VIEWTYPE_FOOTER:
footerView = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_loading, parent, false);
return new LoadingHolder(footerView);
case VIEWTYPE_HEADER:
headerView = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_loading, parent, false);
LoadingHolder headerLoadingHolder = new LoadingHolder(headerView);
setHeaderShowing(false);
return headerLoadingHolder;
case VIEWTYPE_DATE_SEPARATOR:
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.message_date_separator, parent, false);
return new DateSeparatorHolder(view);
}
return null;
}
@Override
public void onBindViewHolder(final RecyclerView.ViewHolder holder, int pos) {
final int position = pos;
switch (getItemViewType(position)) {
case VIEWTYPE_MESSAGE_HEADER:
final MessageHeaderParent messageHeaderParent = (MessageHeaderParent) getItem(position);
final MessageHeaderParent.MessageHeaderHolder messageHeaderHolder = ((MessageHeaderParent.MessageHeaderHolder) holder);
//set date
messageHeaderHolder.timestamp.setText(DateMethods.getStringDate(messageHeaderParent.getMessagesTimestamp()));
if (messageHeaderParent.getMessageType() == MessageType.STREAM_MESSAGE) {
messageHeaderHolder.streamTextView.setText(messageHeaderParent.getStream());
// update MessageHeaderParent subject when topic is updated
if (!messageHeaderParent.getSubject().equalsIgnoreCase(messageHeaderParent.getMessage().getSubject())) {
messageHeaderParent.setSubject(messageHeaderParent.getMessage().getSubject());
}
messageHeaderHolder.topicTextView.setText(messageHeaderParent.getSubject());
//set on long press
messageHeaderHolder.streamTextView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
messageHeaderHolder.streamTextView.setMaxLines(Integer.MAX_VALUE);
messageHeaderHolder.streamTextView.setEllipsize(null);
((MessageHeaderParent) getItem(position)).setStreamExpanded(true);
return true;
}
});
messageHeaderHolder.topicTextView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
messageHeaderHolder.topicTextView.setMaxLines(Integer.MAX_VALUE);
messageHeaderHolder.topicTextView.setEllipsize(null);
((MessageHeaderParent) getItem(position)).setTopicExpanded(true);
return true;
}
});
//if user have expanded, preserve them
if (messageHeaderParent.isStreamExpanded()) {
messageHeaderHolder.streamTextView.setMaxLines(Integer.MAX_VALUE);
messageHeaderHolder.streamTextView.setEllipsize(null);
} else {
messageHeaderHolder.streamTextView.setMaxLines(1);
messageHeaderHolder.streamTextView.setEllipsize(TextUtils.TruncateAt.END);
}
if (messageHeaderParent.isTopicExpanded()) {
messageHeaderHolder.topicTextView.setMaxLines(Integer.MAX_VALUE);
messageHeaderHolder.topicTextView.setEllipsize(null);
} else {
messageHeaderHolder.topicTextView.setMaxLines(1);
messageHeaderHolder.topicTextView.setEllipsize(TextUtils.TruncateAt.END);
}
ViewCompat.setBackgroundTintList(messageHeaderHolder.arrowHead, ColorStateList.valueOf(messageHeaderParent.getColor()));
messageHeaderHolder.streamTextView.setBackgroundColor(messageHeaderParent.getColor());
if (messageHeaderParent.isMute()) {
messageHeaderHolder.muteMessageImage.setVisibility(View.VISIBLE);
}
} else { //PRIVATE MESSAGE
messageHeaderHolder.streamTextView.setText(privateHuddleText);
messageHeaderHolder.streamTextView.setTextColor(Color.WHITE);
messageHeaderHolder.topicTextView.setText(messageHeaderParent.getDisplayRecipent());
ViewCompat.setBackgroundTintList(messageHeaderHolder.arrowHead, ColorStateList.valueOf(mDefaultStreamHeaderColor));
messageHeaderHolder.streamTextView.setBackgroundColor(mDefaultStreamHeaderColor);
}
break;
case VIEWTYPE_MESSAGE:
MessageHolder messageHolder = ((MessageHolder) holder);
final Message message = ((Message) items.get(position));
messageHolder.contentView.setText(message.getFormattedContent(zulipApp));
messageHolder.contentView.setLinkTextColor(ContextCompat.getColor(context, R.color.link_color));
messageHolder.contentView.setMovementMethod(LinkMovementMethod.getInstance());
int padding = convertDpToPixel(4);
messageHolder.contentView.setShadowLayer(padding, 0, 0, 0);
final String url = message.extractImageUrl(zulipApp);
if (url != null) {
messageHolder.contentImageContainer.setVisibility(View.VISIBLE);
Picasso.with(context).load(url)
.into(messageHolder.contentImage);
messageHolder.contentImageContainer
.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent i = new Intent(zulipApp.getApplicationContext(), PhotoViewActivity.class);
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
i.putExtra(Intent.EXTRA_TEXT, url);
zulipApp.startActivity(i);
// activity transition animation
ActivityTransitionAnim.transition(context);
}
});
} else {
messageHolder.contentImageContainer.setVisibility(View.GONE);
messageHolder.contentImage.setImageDrawable(null);
}
Message lastMessage = getLastMessage(position);
boolean isDifferentSender = (position == 0 || lastMessage == null || !lastMessage.getSender().equals(message.getSender()));
if (message.getType() == MessageType.STREAM_MESSAGE) {
if (isDifferentSender) {
messageHolder.senderName.setVisibility(View.VISIBLE);
messageHolder.senderName.setText(message.getSender().getName());
} else {
messageHolder.senderName.setVisibility(View.GONE);
}
if (!isCurrentThemeNight)
messageHolder.leftBar.setBackgroundColor(message.getStream().getParsedColor());
messageHolder.messageTile.setBackgroundColor(streamMessageBackground);
} else {
if (isDifferentSender) {
messageHolder.senderName.setVisibility(View.VISIBLE);
messageHolder.senderName.setText(message.getSender().getName());
} else {
messageHolder.senderName.setVisibility(View.GONE);
}
if (!isCurrentThemeNight) {
messageHolder.leftBar.setBackgroundColor(privateMessageBackground);
}
messageHolder.messageTile.setBackgroundColor(privateMessageBackground);
}
Boolean isEdited = message.isHasBeenEdited();
if (isDifferentSender) {
messageHolder.gravatar.setVisibility(View.VISIBLE);
setUpGravatar(message, messageHolder);
// set visibility of edited tag
if (isEdited != null && isEdited) {
messageHolder.edited.setVisibility(View.VISIBLE);
} else {
messageHolder.edited.setVisibility(View.GONE);
}
messageHolder.leftEdited.setVisibility(View.GONE);
setUpTime(message, messageHolder.timestamp);
setUpStar(message, messageHolder.starImage);
//hide other one's
messageHolder.leftTimestamp.setVisibility(View.GONE);
messageHolder.leftStarImage.setVisibility(View.GONE);
} else {
messageHolder.gravatar.setVisibility(View.GONE);
// set visibility of edited tag
if (isEdited != null && isEdited) {
messageHolder.leftEdited.setVisibility(View.VISIBLE);
} else {
messageHolder.leftEdited.setVisibility(View.GONE);
}
messageHolder.edited.setVisibility(View.GONE);
//check if duration between last and this message is less then hide
if (Math.abs(message.getTimestamp().getTime() - lastMessage.getTimestamp().getTime()) < Constants.HIDE_TIMESTAMP_THRESHOLD) {
messageHolder.leftTimestamp.setVisibility(View.GONE);
} else {
setUpTime(message, messageHolder.leftTimestamp);
}
setUpStar(message, messageHolder.leftStarImage);
//hide other one's
messageHolder.timestamp.setVisibility(View.GONE);
messageHolder.starImage.setVisibility(View.GONE);
}
setUpReactions(messageHolder, message);
break;
case VIEWTYPE_DATE_SEPARATOR:
MessageDateSeparator messageDateSeparator = (MessageDateSeparator) items.get(position);
DateSeparatorHolder dateSeparatorHolder = (DateSeparatorHolder) holder;
if (!TextUtils.isEmpty(messageDateSeparator.getRightText())) {
dateSeparatorHolder.tvBelowMessagesDate.setText(messageDateSeparator.getRightText());
} else {
dateSeparatorHolder.tvBelowMessagesDate.setVisibility(View.GONE);
}
if (!TextUtils.isEmpty(messageDateSeparator.getLeftText())) {
dateSeparatorHolder.tvAboveMessagesDate.setText(messageDateSeparator.getLeftText());
} else {
dateSeparatorHolder.tvAboveMessagesDate.setVisibility(View.GONE);
}
}
}
@Override
public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
super.onViewRecycled(holder);
if (holder.getItemViewType() == VIEWTYPE_MESSAGE)
// mark fields as read in homeview and streams narrow
markThisMessageAsRead((Message) getItem(holder.getAdapterPosition()));
}
/**
* This is called when the Message is bind to the Holder and attached, displayed in the window.
*
* @param message Mark this message read
*/
private void markThisMessageAsRead(Message message) {
try {
int mID = message.getID();
if (!startedFromFilter && zulipApp.getPointer() < mID) {
zulipApp.syncPointer(mID);
}
boolean isMessageRead = false;
if (message.getMessageRead() != null) {
isMessageRead = message.getMessageRead();
}
if (!isMessageRead) {
try {
updateBuilder.where().eq(Message.ID_FIELD, message.getID());
updateBuilder.updateColumnValue(Message.MESSAGE_READ_FIELD, true);
updateBuilder.update();
} catch (SQLException e) {
ZLog.logException(e);
}
zulipApp.markMessageAsRead(message);
}
} catch (NullPointerException e) {
Log.w("scrolling", "Could not find a location to scroll to!");
}
}
private void setUpTime(Message message, TextView timestamp) {
timestamp.setText(DateUtils.formatDateTime(context, message
.getTimestamp().getTime(), DateUtils.FORMAT_SHOW_TIME));
timestamp.setVisibility(View.VISIBLE);
}
private void setUpStar(Message message, ImageView starImage) {
if (message.getFlags() != null) {
if (message.getFlags().contains("starred")) {
message.setMessageStar(true);
}
}
//Checking for a starred message
if (message.getMessageStar()) {
//Make star's imageView visibility to VISIBLE
starImage.setVisibility(View.VISIBLE);
} else {
//Make star's imageView visibility to GONE
starImage.setVisibility(View.GONE);
}
}
private void setUpReactions(MessageHolder messageHolder, Message message) {
try {
messageHolder.reactionsTable.removeAllViews();
if (message.getReactions().isEmpty()) {
return;
}
// Calculate number of reactions in each row
int messageWidth = messageHolder.messageTile.getContext().getResources().getDisplayMetrics().widthPixels;
int reactionWidth = convertDpToPixel(Constants.REACTION_MARGIN);
int numOfReactions = messageWidth / reactionWidth;
TableRow row = new TableRow(messageHolder.messageTile.getContext());
// set margin of 6dp between reactions in a table row
TableRow.LayoutParams layoutParams = new TableRow.LayoutParams();
int margin = ConvertDpPx.convertDpToPixel(6);
layoutParams.setMargins(margin, margin, margin, margin);
// table row index
int index = 0;
for (Map.Entry<String, Integer> reaction : getDisplayReactions(message.getReactions()).entrySet()) {
if (numOfReactions == 0) {
// current row is full
messageHolder.reactionsTable.addView(row, index++);
row = new TableRow(messageHolder.messageTile.getContext());
numOfReactions = messageWidth / reactionWidth;
}
// inflate reaction layout
LinearLayout reactionTile = (LinearLayout) LayoutInflater.from(messageHolder.messageTile.getContext()).inflate(R.layout.reaction_tile, null);
reactionTile.setLayoutParams(layoutParams);
// emoji view in reaction
ImageView imageView = (ImageView) reactionTile.findViewById(R.id.reaction_emoji);
// emoji count view
TextView textView = (TextView) reactionTile.findViewById(R.id.reaction_count);
// get emoji drawable from assets
String emojiName = reaction.getKey() + ".png";
Drawable drawable = Drawable.createFromStream(zulipApp.getAssets().open("emoji/" + emojiName),
"emoji/" + emojiName);
// shrink drawable resource
Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
int size = ConvertDpPx.convertDpToPixel(20);
drawable = new BitmapDrawable(context.getResources(), Bitmap.createScaledBitmap(bitmap, size, size, true));
imageView.setImageDrawable(drawable);
textView.setText(String.format(Locale.getDefault(), "%d", reaction.getValue()));
row.addView(reactionTile);
numOfReactions--;
}
messageHolder.reactionsTable.addView(row, index);
} catch (NullPointerException e) {
Log.e("adapter", "message reactions are null");
} catch (IOException e) {
ZLog.logException(e);
}
}
private void setUpGravatar(final Message message, final MessageHolder messageHolder) {
//Setup Gravatar
Bitmap gravatarImg = ((ZulipActivity) context).getGravatars().get(message.getSender().getEmail());
if (gravatarImg != null) {
// Gravatar already exists for this image, set the ImageView to it
messageHolder.gravatar.setImageBitmap(gravatarImg);
} else {
// From http://stackoverflow.com/questions/4605527/
Resources resources = context.getResources();
float px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
35, resources.getDisplayMetrics());
String url = message.getSender().getAvatarURL() + "&s=" + px;
url = UrlHelper.addHost(url);
Picasso.with(context)
.load(url)
.placeholder(android.R.drawable.stat_notify_error)
.error(android.R.drawable.presence_online)
.into(messageHolder.gravatar, new Callback() {
@Override
public void onSuccess() {
}
@Override
public void onError() {
int hMapKey = message.getSender().getId();
int avatarColor;
// check if current sender has already been allotted a randomly generated color
if (defaultAvatarColorHMap.containsKey(hMapKey)) {
avatarColor = defaultAvatarColorHMap.get(hMapKey);
} else {
// generate a random color for current sender id
avatarColor = getRandomColor(Color.rgb(255, 255, 255));
// add sender id and randomly generated color to hashmap
defaultAvatarColorHMap.put(hMapKey, avatarColor);
}
// square default avatar drawable
final GradientDrawable defaultAvatar = (GradientDrawable) ContextCompat.getDrawable(context, R.drawable.default_avatar);
defaultAvatar.setColor(avatarColor);
messageHolder.gravatar.setImageDrawable(defaultAvatar);
}
});
}
}
/**
* Method to generate random saturated colors for default avatar {@link R.drawable#default_avatar}
*
* @param mix integer color is mixed with randomly generated red, blue, green colors
* @return a randomly generated color
*/
private int getRandomColor(int mix) {
Random random = new Random();
int red = random.nextInt(256);
int green = random.nextInt(256);
int blue = random.nextInt(256);
// mix the color
red = (red + Color.red(mix)) / 2;
green = (green + Color.green(mix)) / 2;
blue = (blue + Color.blue(mix)) / 2;
int color = Color.rgb(red, green, blue);
return color;
}
@Override
public int getItemCount() {
return items.size();
}
public void clear() {
items.clear();
setupHeaderAndFooterViews();
notifyDataSetChanged();
}
public int getItemIndex(Message message) {
return items.indexOf(message);
}
public int getItemIndex(int id) {
for (int i = 0; i < items.size(); i++) {
if (items.get(i) instanceof Message && ((Message) items.get(i)).getId() == id) {
return i;
}
}
return -1;
}
public Object getItem(int position) {
return items.get(position);
}
/**
* Return the size of the list with including or excluding footer
*
* @param includeFooter true to return the size including footer or false to return size excluding footer.
* @return size of list
*/
public int getItemCount(boolean includeFooter) {
if (includeFooter) return getItemCount();
else return getItemCount() - 1;
}
public void setFooterShowing(boolean show) {
if (footerView == null) return;
if (show) {
final float scale = footerView.getContext().getResources().getDisplayMetrics().density;
footerView.getLayoutParams().height = (int) (HEIGHT_IN_DP * scale + 0.5f);
footerView.setVisibility(View.VISIBLE);
} else {
footerView.getLayoutParams().height = 0;
footerView.setVisibility(View.GONE);
}
}
public void setHeaderShowing(boolean show) {
if (headerView == null) return;
if (show) {
final float scale = headerView.getContext().getResources().getDisplayMetrics().density;
headerView.getLayoutParams().height = (int) (HEIGHT_IN_DP * scale + 0.5f);
headerView.setVisibility(View.VISIBLE);
} else {
headerView.getLayoutParams().height = 0;
headerView.setVisibility(View.GONE);
}
}
public HashMap<String, Integer> getDisplayReactions(List<Reaction> reactions) {
HashMap<String, Integer> hashMap = new HashMap<>();
for (Reaction reaction : reactions) {
Integer count = hashMap.get(reaction.getEmoji());
hashMap.put(reaction.getEmoji(), (count != null) ? count + 1 : 1);
}
return hashMap;
}
@Override
public List<?> getAdapterData() {
return items;
}
@Override
public void setAttachedHeader(int adapterPosition) {
this.attachedHeaderAdapterPosition = adapterPosition;
}
private Message getLastMessage(int position) {
if (position == 0)
return null;
Object object = getItem(position - 1);
if (object instanceof Message) {
return (Message) object;
} else {
return null;
}
}
private class DateSeparatorHolder extends RecyclerView.ViewHolder {
private TextView tvAboveMessagesDate, tvBelowMessagesDate;
DateSeparatorHolder(View itemView) {
super(itemView);
tvAboveMessagesDate = (TextView) itemView.findViewById(R.id.tvAboveMessagesDate);
tvBelowMessagesDate = (TextView) itemView.findViewById(R.id.tvBelowMessagesDate);
}
}
}