mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-13 11:19:04 +01:00
Use a RecyclerView for the ConversationView and
properly notify the view adapter of dataset changes in order to avoid invalidating the entire dataset when not absolutely necessary. This change also shows unread messages in a different color, so users do not fail to notice delayed messages.
This commit is contained in:
BIN
briar-android/res/drawable-hdpi/msg_in_unread.9.png
Normal file
BIN
briar-android/res/drawable-hdpi/msg_in_unread.9.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
BIN
briar-android/res/drawable-mdpi/msg_in_unread.9.png
Normal file
BIN
briar-android/res/drawable-mdpi/msg_in_unread.9.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1014 B |
BIN
briar-android/res/drawable-xhdpi/msg_in.9.png
Normal file
BIN
briar-android/res/drawable-xhdpi/msg_in.9.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
briar-android/res/drawable-xhdpi/msg_in_unread.9.png
Normal file
BIN
briar-android/res/drawable-xhdpi/msg_in_unread.9.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
BIN
briar-android/res/drawable-xhdpi/msg_out.9.png
Normal file
BIN
briar-android/res/drawable-xhdpi/msg_out.9.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
BIN
briar-android/res/drawable-xxhdpi/msg_in_unread.9.png
Normal file
BIN
briar-android/res/drawable-xxhdpi/msg_in_unread.9.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
@@ -1,12 +1,18 @@
|
||||
<?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:id="@+id/layout"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<!-- ListView will get inserted here -->
|
||||
<android.support.v7.widget.RecyclerView
|
||||
android:id="@+id/conversationView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:scrollbars="vertical"/>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/listLoadingProgressBar"
|
||||
@@ -15,7 +21,8 @@
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:layout_weight="1"
|
||||
android:indeterminate="true"/>
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/emptyView"
|
||||
@@ -26,7 +33,8 @@
|
||||
android:gravity="center"
|
||||
android:padding="@dimen/margin_large"
|
||||
android:textSize="@dimen/text_size_large"
|
||||
android:text="@string/no_private_messages"/>
|
||||
android:text="@string/no_private_messages"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
android:paddingBottom="@dimen/margin_small">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/msgLayout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="left|start"
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
android:paddingBottom="@dimen/margin_small">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/msgLayout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="right|end"
|
||||
|
||||
@@ -2,30 +2,26 @@ package org.briarproject.android.contact;
|
||||
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.os.Bundle;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AdapterView.OnItemClickListener;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ListView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.briarproject.R;
|
||||
import org.briarproject.android.BriarActivity;
|
||||
import org.briarproject.android.util.LayoutUtils;
|
||||
import org.briarproject.api.android.AndroidNotificationManager;
|
||||
import org.briarproject.api.contact.Contact;
|
||||
import org.briarproject.api.contact.ContactId;
|
||||
@@ -77,12 +73,11 @@ import static android.widget.Toast.LENGTH_SHORT;
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.android.contact.ReadPrivateMessageActivity.RESULT_PREV_NEXT;
|
||||
import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP_1;
|
||||
import static org.briarproject.api.messaging.PrivateMessageHeader.Status.DELIVERED;
|
||||
import static org.briarproject.api.messaging.PrivateMessageHeader.Status.SENT;
|
||||
|
||||
public class ConversationActivity extends BriarActivity
|
||||
implements EventListener, OnClickListener, OnItemClickListener {
|
||||
implements EventListener, OnClickListener {
|
||||
|
||||
private static final int REQUEST_READ = 2;
|
||||
private static final Logger LOG =
|
||||
@@ -95,7 +90,7 @@ implements EventListener, OnClickListener, OnItemClickListener {
|
||||
private TextView empty = null;
|
||||
private ProgressBar loading = null;
|
||||
private ConversationAdapter adapter = null;
|
||||
private ListView list = null;
|
||||
private RecyclerView list = null;
|
||||
private EditText content = null;
|
||||
private ImageButton sendButton = null;
|
||||
|
||||
@@ -133,20 +128,26 @@ implements EventListener, OnClickListener, OnItemClickListener {
|
||||
loading.setVisibility(VISIBLE);
|
||||
|
||||
adapter = new ConversationAdapter(this);
|
||||
list = new ListView(this) {
|
||||
@Override
|
||||
public void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
// Scroll to the bottom when the keyboard is shown
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
setSelection(getCount() - 1);
|
||||
}
|
||||
};
|
||||
list.setLayoutParams(MATCH_WRAP_1);
|
||||
list.setDivider(null);
|
||||
list = (RecyclerView) findViewById(R.id.conversationView);
|
||||
list.setLayoutManager(new LinearLayoutManager(this));
|
||||
list.setAdapter(adapter);
|
||||
list.setOnItemClickListener(this);
|
||||
list.setEmptyView(loading);
|
||||
layout.addView(list, 0);
|
||||
list.setVisibility(GONE);
|
||||
// scroll down when opening keyboard
|
||||
list.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
|
||||
@Override
|
||||
public void onLayoutChange(View v,
|
||||
int left, int top, int right, int bottom,
|
||||
int oldLeft, int oldTop, int oldRight, int oldBottom) {
|
||||
if (bottom < oldBottom) {
|
||||
list.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
list.scrollToPosition(adapter.getItemCount() - 1);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
content = (EditText) findViewById(R.id.contentView);
|
||||
sendButton = (ImageButton) findViewById(R.id.sendButton);
|
||||
@@ -260,12 +261,10 @@ implements EventListener, OnClickListener, OnItemClickListener {
|
||||
runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
loading.setVisibility(GONE);
|
||||
empty.setVisibility(VISIBLE);
|
||||
list.setEmptyView(empty);
|
||||
displayContactDetails();
|
||||
sendButton.setEnabled(true);
|
||||
adapter.clear();
|
||||
if (!headers.isEmpty()) {
|
||||
list.setVisibility(VISIBLE);
|
||||
empty.setVisibility(GONE);
|
||||
for (PrivateMessageHeader h : headers) {
|
||||
ConversationItem item = new ConversationItem(h);
|
||||
byte[] body = bodyCache.get(h.getId());
|
||||
@@ -273,11 +272,12 @@ implements EventListener, OnClickListener, OnItemClickListener {
|
||||
else item.setBody(body);
|
||||
adapter.add(item);
|
||||
}
|
||||
adapter.sort(ConversationItemComparator.INSTANCE);
|
||||
// Scroll to the bottom
|
||||
list.setSelection(adapter.getCount() - 1);
|
||||
list.scrollToPosition(adapter.getItemCount() - 1);
|
||||
} else {
|
||||
empty.setVisibility(VISIBLE);
|
||||
list.setVisibility(GONE);
|
||||
}
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -306,14 +306,18 @@ implements EventListener, OnClickListener, OnItemClickListener {
|
||||
runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
bodyCache.put(m, body);
|
||||
int count = adapter.getCount();
|
||||
int count = adapter.getItemCount();
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
ConversationItem item = adapter.getItem(i);
|
||||
|
||||
if (item.getHeader().getId().equals(m)) {
|
||||
item.setBody(body);
|
||||
adapter.notifyDataSetChanged();
|
||||
adapter.notifyItemChanged(i);
|
||||
|
||||
// Scroll to the bottom
|
||||
list.setSelection(count - 1);
|
||||
list.scrollToPosition(count - 1);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -326,7 +330,7 @@ implements EventListener, OnClickListener, OnItemClickListener {
|
||||
super.onActivityResult(request, result, data);
|
||||
if (request == REQUEST_READ && result == RESULT_PREV_NEXT) {
|
||||
int position = data.getIntExtra("briar.POSITION", -1);
|
||||
if (position >= 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<MessageId> unread = new ArrayList<MessageId>();
|
||||
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<MessageId> messages = new HashSet<MessageId>(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();
|
||||
|
||||
@@ -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<ConversationItem> {
|
||||
class ConversationAdapter extends
|
||||
RecyclerView.Adapter<ConversationAdapter.MessageHolder> {
|
||||
|
||||
ConversationAdapter(Context ctx) {
|
||||
super(ctx, android.R.layout.simple_expandable_list_item_1,
|
||||
new ArrayList<ConversationItem>());
|
||||
private static final int MSG_OUT = 0;
|
||||
private static final int MSG_IN = 1;
|
||||
private static final int MSG_IN_UNREAD = 2;
|
||||
|
||||
private SortedList<ConversationItem> messages =
|
||||
new SortedList<ConversationItem>(ConversationItem.class,
|
||||
new SortedList.Callback<ConversationItem>() {
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package org.briarproject.android.contact;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
class ConversationItemComparator implements Comparator<ConversationItem> {
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user