diff --git a/briar-android/res/drawable-hdpi/msg_in_unread.9.png b/briar-android/res/drawable-hdpi/msg_in_unread.9.png new file mode 100644 index 000000000..c22cc8632 Binary files /dev/null and b/briar-android/res/drawable-hdpi/msg_in_unread.9.png differ diff --git a/briar-android/res/drawable-mdpi/msg_in_unread.9.png b/briar-android/res/drawable-mdpi/msg_in_unread.9.png new file mode 100644 index 000000000..6e5418856 Binary files /dev/null and b/briar-android/res/drawable-mdpi/msg_in_unread.9.png differ diff --git a/briar-android/res/drawable-xhdpi/msg_in.9.png b/briar-android/res/drawable-xhdpi/msg_in.9.png new file mode 100644 index 000000000..f5db8372d Binary files /dev/null and b/briar-android/res/drawable-xhdpi/msg_in.9.png differ diff --git a/briar-android/res/drawable-xhdpi/msg_in_unread.9.png b/briar-android/res/drawable-xhdpi/msg_in_unread.9.png new file mode 100644 index 000000000..341ec4f72 Binary files /dev/null and b/briar-android/res/drawable-xhdpi/msg_in_unread.9.png differ diff --git a/briar-android/res/drawable-xhdpi/msg_out.9.png b/briar-android/res/drawable-xhdpi/msg_out.9.png new file mode 100644 index 000000000..d7c2816f1 Binary files /dev/null and b/briar-android/res/drawable-xhdpi/msg_out.9.png differ diff --git a/briar-android/res/drawable-xxhdpi/msg_in_unread.9.png b/briar-android/res/drawable-xxhdpi/msg_in_unread.9.png new file mode 100644 index 000000000..3a3bb3e7e Binary files /dev/null and b/briar-android/res/drawable-xxhdpi/msg_in_unread.9.png differ diff --git a/briar-android/res/layout/activity_conversation.xml b/briar-android/res/layout/activity_conversation.xml index 8bc064d2a..673dd14f9 100644 --- a/briar-android/res/layout/activity_conversation.xml +++ b/briar-android/res/layout/activity_conversation.xml @@ -1,12 +1,18 @@ - + + android:indeterminate="true" + android:visibility="gone"/> + android:text="@string/no_private_messages" + android:visibility="gone"/> = 0 && position < adapter.getCount()) + if (position >= 0 && position < adapter.getItemCount()) displayMessage(position); } } @@ -341,7 +345,7 @@ implements EventListener, OnClickListener, OnItemClickListener { private void markMessagesRead() { notificationManager.clearPrivateMessageNotification(contactId); List unread = new ArrayList(); - int count = adapter.getCount(); + int count = adapter.getItemCount(); for (int i = 0; i < count; i++) { PrivateMessageHeader h = adapter.getItem(i).getHeader(); if (!h.isRead()) unread.add(h.getId()); @@ -381,6 +385,8 @@ implements EventListener, OnClickListener, OnItemClickListener { GroupId g = ((MessageAddedEvent) e).getGroupId(); if (g.equals(groupId)) { LOG.info("Message added, reloading"); + // TODO: find a way of not needing to reload the entire + // conversation just because one message was added loadHeaders(); } } else if (e instanceof MessagesSentEvent) { @@ -417,16 +423,14 @@ implements EventListener, OnClickListener, OnItemClickListener { runOnUiThread(new Runnable() { public void run() { Set messages = new HashSet(messageIds); - boolean changed = false; - int count = adapter.getCount(); + int count = adapter.getItemCount(); for (int i = 0; i < count; i++) { ConversationItem item = adapter.getItem(i); if (messages.contains(item.getHeader().getId())) { item.setStatus(status); - changed = true; + adapter.notifyItemChanged(i); } } - if (changed) adapter.notifyDataSetChanged(); } }); } @@ -444,7 +448,7 @@ implements EventListener, OnClickListener, OnItemClickListener { private long getMinTimestampForNewMessage() { // Don't use an earlier timestamp than the newest message long timestamp = 0; - int count = adapter.getCount(); + int count = adapter.getItemCount(); for (int i = 0; i < count; i++) { long t = adapter.getItem(i).getHeader().getTimestamp(); if (t > timestamp) timestamp = t; @@ -485,11 +489,6 @@ implements EventListener, OnClickListener, OnItemClickListener { }); } - public void onItemClick(AdapterView parent, View view, int position, - long id) { - displayMessage(position); - } - private void displayMessage(int position) { ConversationItem item = adapter.getItem(position); PrivateMessageHeader header = item.getHeader(); diff --git a/briar-android/src/org/briarproject/android/contact/ConversationAdapter.java b/briar-android/src/org/briarproject/android/contact/ConversationAdapter.java index 0e2499c7e..4b945f7d3 100644 --- a/briar-android/src/org/briarproject/android/contact/ConversationAdapter.java +++ b/briar-android/src/org/briarproject/android/contact/ConversationAdapter.java @@ -1,11 +1,12 @@ package org.briarproject.android.contact; import android.content.Context; +import android.support.v7.util.SortedList; +import android.support.v7.widget.RecyclerView; import android.text.format.DateUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.ArrayAdapter; import android.widget.ImageView; import android.widget.TextView; @@ -13,57 +14,177 @@ import org.briarproject.R; import org.briarproject.api.messaging.PrivateMessageHeader; import org.briarproject.util.StringUtils; -import java.util.ArrayList; - import static org.briarproject.api.messaging.PrivateMessageHeader.Status.DELIVERED; import static org.briarproject.api.messaging.PrivateMessageHeader.Status.SENT; -class ConversationAdapter extends ArrayAdapter { +class ConversationAdapter extends + RecyclerView.Adapter { - ConversationAdapter(Context ctx) { - super(ctx, android.R.layout.simple_expandable_list_item_1, - new ArrayList()); + private static final int MSG_OUT = 0; + private static final int MSG_IN = 1; + private static final int MSG_IN_UNREAD = 2; + + private SortedList messages = + new SortedList(ConversationItem.class, + new SortedList.Callback() { + @Override + public void onInserted(int position, int count) { + notifyItemRangeInserted(position, count); + } + + @Override + public void onChanged(int position, int count) { + notifyItemRangeChanged(position, count); + } + + @Override + public void onMoved(int fromPosition, int toPosition) { + notifyItemMoved(fromPosition, toPosition); + } + + @Override + public void onRemoved(int position, int count) { + notifyItemRangeRemoved(position, count); + } + + @Override + public int compare(ConversationItem c1, + ConversationItem c2) { + long time1 = c1.getHeader().getTimestamp(); + long time2 = c2.getHeader().getTimestamp(); + if (time1 < time2) return -1; + if (time1 > time2) return 1; + return 0; + } + + @Override + public boolean areItemsTheSame(ConversationItem c1, + ConversationItem c2) { + return c1.getHeader().getId() + .equals(c2.getHeader().getId()); + } + + @Override + public boolean areContentsTheSame(ConversationItem c1, + ConversationItem c2) { + return c1.equals(c2); + } + }); + private Context ctx; + + public ConversationAdapter(Context context) { + ctx = context; } @Override - public View getView(int position, View convertView, ViewGroup parent) { - ConversationItem item = getItem(position); - PrivateMessageHeader header = item.getHeader(); - Context ctx = getContext(); - - LayoutInflater inflater = (LayoutInflater) ctx.getSystemService - (Context.LAYOUT_INFLATER_SERVICE); - - View v; + public int getItemViewType(int position) { + // return different type for incoming and outgoing (local) messages + PrivateMessageHeader header = getItem(position).getHeader(); if (header.isLocal()) { - v = inflater.inflate(R.layout.list_item_msg_out, null); - - ImageView status = (ImageView) v.findViewById(R.id.msgStatus); - if (item.getStatus() == DELIVERED) { - status.setImageResource(R.drawable.message_delivered); - } else if (item.getStatus() == SENT) { - status.setImageResource(R.drawable.message_sent); - } else { - status.setImageResource(R.drawable.message_stored); - } + return MSG_OUT; + } else if (header.isRead()) { + return MSG_IN; } else { - v = inflater.inflate(R.layout.list_item_msg_in, null); + return MSG_IN_UNREAD; + } + } + + @Override + public MessageHolder onCreateViewHolder(ViewGroup viewGroup, int type) { + View v; + + // outgoing message (local) + if (type == MSG_OUT) { + v = LayoutInflater.from(viewGroup.getContext()) + .inflate(R.layout.list_item_msg_out, viewGroup, false); + } + // incoming message (non-local) + else { + v = LayoutInflater.from(viewGroup.getContext()) + .inflate(R.layout.list_item_msg_in, viewGroup, false); } - TextView body = (TextView) v.findViewById(R.id.msgBody); + return new MessageHolder(v, type); + } + + @Override + public void onBindViewHolder(final MessageHolder ui, final int position) { + ConversationItem item = getItem(position); + PrivateMessageHeader header = item.getHeader(); + + if (header.isLocal()) { + if (item.getStatus() == DELIVERED) { + ui.status.setImageResource(R.drawable.message_delivered); + } else if (item.getStatus() == SENT) { + ui.status.setImageResource(R.drawable.message_sent); + } else { + ui.status.setImageResource(R.drawable.message_stored); + } + } else if (!header.isRead()) { + // show unread messages in different color to not miss them + ui.layout.setBackgroundResource(R.drawable.msg_in_unread); + } if (item.getBody() == null) { - body.setText("\u2026"); + ui.body.setText("\u2026"); } else if (header.getContentType().equals("text/plain")) { - body.setText(StringUtils.fromUtf8(item.getBody())); + ui.body.setText(StringUtils.fromUtf8(item.getBody())); } else { // TODO support other content types } - TextView date = (TextView) v.findViewById(R.id.msgTime); long timestamp = header.getTimestamp(); - date.setText(DateUtils.getRelativeTimeSpanString(ctx, timestamp)); + ui.date.setText(DateUtils.getRelativeTimeSpanString(ctx, timestamp)); + } - return v; + @Override + public int getItemCount() { + return messages == null ? 0 : messages.size(); + } + + public boolean isEmpty() { + return messages == null || messages.size() == 0; + } + + public ConversationItem getItem(int position) { + return messages.get(position); + } + + public void add(final ConversationItem contact) { + this.messages.add(contact); + } + + public void remove(final ConversationItem contact) { + this.messages.remove(contact); + } + + public void clear() { + this.messages.beginBatchedUpdates(); + + while(messages.size() != 0) { + messages.removeItemAt(0); + } + + this.messages.endBatchedUpdates(); + } + + public static class MessageHolder extends RecyclerView.ViewHolder { + public ViewGroup layout; + public TextView body; + public TextView date; + public ImageView status; + + public MessageHolder(View v, int type) { + super(v); + + layout = (ViewGroup) v.findViewById(R.id.msgLayout); + body = (TextView) v.findViewById(R.id.msgBody); + date = (TextView) v.findViewById(R.id.msgTime); + + // outgoing message (local) + if (type == MSG_OUT) { + status = (ImageView) v.findViewById(R.id.msgStatus); + } + } } } \ No newline at end of file diff --git a/briar-android/src/org/briarproject/android/contact/ConversationItemComparator.java b/briar-android/src/org/briarproject/android/contact/ConversationItemComparator.java deleted file mode 100644 index 76eb1d28d..000000000 --- a/briar-android/src/org/briarproject/android/contact/ConversationItemComparator.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.briarproject.android.contact; - -import java.util.Comparator; - -class ConversationItemComparator implements Comparator { - - static final ConversationItemComparator INSTANCE = - new ConversationItemComparator(); - - public int compare(ConversationItem a, ConversationItem b) { - // The oldest message comes first - long aTime = a.getHeader().getTimestamp(); - long bTime = b.getHeader().getTimestamp(); - if (aTime < bTime) return -1; - if (aTime > bTime) return 1; - return 0; - } -}