Merge branch '46-improve-message-status-indicators' into 'master'

Improve how the status of messages is indicated.

Remove the Toast that always says 'Message Sent' and show graphical
indicators instead that show either:
* message is waiting to be sent
* message was sent (or requested to be sent)
* message was delivered

Please note that I didn't change the icons and did not migrate the UI to XML files to keep my change minimally invasive.

Closes #46

See merge request !9
This commit is contained in:
akwizgran
2015-12-10 13:36:14 +00:00
18 changed files with 87 additions and 29 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 284 B

After

Width:  |  Height:  |  Size: 479 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 238 B

After

Width:  |  Height:  |  Size: 313 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 392 B

After

Width:  |  Height:  |  Size: 627 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 683 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 969 B

View File

@@ -8,7 +8,6 @@ import static android.view.View.GONE;
import static android.view.View.VISIBLE; import static android.view.View.VISIBLE;
import static android.widget.LinearLayout.HORIZONTAL; import static android.widget.LinearLayout.HORIZONTAL;
import static android.widget.LinearLayout.VERTICAL; import static android.widget.LinearLayout.VERTICAL;
import static android.widget.Toast.LENGTH_SHORT;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static org.briarproject.android.contact.ReadPrivateMessageActivity.RESULT_PREV_NEXT; import static org.briarproject.android.contact.ReadPrivateMessageActivity.RESULT_PREV_NEXT;
@@ -45,6 +44,7 @@ import org.briarproject.api.crypto.CryptoExecutor;
import org.briarproject.api.db.DatabaseComponent; import org.briarproject.api.db.DatabaseComponent;
import org.briarproject.api.db.DbException; import org.briarproject.api.db.DbException;
import org.briarproject.api.db.MessageHeader; import org.briarproject.api.db.MessageHeader;
import org.briarproject.api.db.MessageHeader.State;
import org.briarproject.api.db.NoSuchContactException; import org.briarproject.api.db.NoSuchContactException;
import org.briarproject.api.db.NoSuchMessageException; import org.briarproject.api.db.NoSuchMessageException;
import org.briarproject.api.db.NoSuchSubscriptionException; import org.briarproject.api.db.NoSuchSubscriptionException;
@@ -55,6 +55,7 @@ import org.briarproject.api.event.EventListener;
import org.briarproject.api.event.MessageAddedEvent; import org.briarproject.api.event.MessageAddedEvent;
import org.briarproject.api.event.MessageExpiredEvent; import org.briarproject.api.event.MessageExpiredEvent;
import org.briarproject.api.event.MessagesAckedEvent; import org.briarproject.api.event.MessagesAckedEvent;
import org.briarproject.api.event.MessagesSentEvent;
import org.briarproject.api.messaging.Group; import org.briarproject.api.messaging.Group;
import org.briarproject.api.messaging.GroupId; import org.briarproject.api.messaging.GroupId;
import org.briarproject.api.messaging.Message; import org.briarproject.api.messaging.Message;
@@ -76,7 +77,6 @@ import android.widget.ImageButton;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.ListView; import android.widget.ListView;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast;
public class ConversationActivity extends BriarActivity public class ConversationActivity extends BriarActivity
implements EventListener, OnClickListener, OnItemClickListener { implements EventListener, OnClickListener, OnItemClickListener {
@@ -384,25 +384,31 @@ implements EventListener, OnClickListener, OnItemClickListener {
} else if (e instanceof MessageExpiredEvent) { } else if (e instanceof MessageExpiredEvent) {
LOG.info("Message expired, reloading"); LOG.info("Message expired, reloading");
loadHeaders(); loadHeaders();
} else if (e instanceof MessagesSentEvent) {
MessagesSentEvent m = (MessagesSentEvent) e;
if (m.getContactId().equals(contactId)) {
LOG.info("Messages sent");
markMessages(m.getMessageIds(), State.SENT);
}
} else if (e instanceof MessagesAckedEvent) { } else if (e instanceof MessagesAckedEvent) {
MessagesAckedEvent m = (MessagesAckedEvent) e; MessagesAckedEvent m = (MessagesAckedEvent) e;
if (m.getContactId().equals(contactId)) { if (m.getContactId().equals(contactId)) {
LOG.info("Messages acked"); LOG.info("Messages acked");
markMessagesDelivered(m.getMessageIds()); markMessages(m.getMessageIds(), State.DELIVERED);
} }
} }
} }
private void markMessagesDelivered(final Collection<MessageId> acked) { private void markMessages(final Collection<MessageId> messageIds, final State state) {
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
public void run() { public void run() {
Set<MessageId> ackedSet = new HashSet<MessageId>(acked); Set<MessageId> messages = new HashSet<MessageId>(messageIds);
boolean changed = false; boolean changed = false;
int count = adapter.getCount(); int count = adapter.getCount();
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
ConversationItem item = adapter.getItem(i); ConversationItem item = adapter.getItem(i);
if (ackedSet.contains(item.getHeader().getId())) { if (messages.contains(item.getHeader().getId())) {
item.setDelivered(true); item.setStatus(state);
changed = true; changed = true;
} }
} }
@@ -417,7 +423,6 @@ implements EventListener, OnClickListener, OnItemClickListener {
long timestamp = System.currentTimeMillis(); long timestamp = System.currentTimeMillis();
timestamp = Math.max(timestamp, getMinTimestampForNewMessage()); timestamp = Math.max(timestamp, getMinTimestampForNewMessage());
createMessage(StringUtils.toUtf8(message), timestamp); createMessage(StringUtils.toUtf8(message), timestamp);
Toast.makeText(this, R.string.message_sent_toast, LENGTH_SHORT).show();
content.setText(""); content.setText("");
hideSoftKeyboard(); hideSoftKeyboard();
} }

View File

@@ -13,6 +13,7 @@ import org.briarproject.R;
import org.briarproject.android.util.ElasticHorizontalSpace; import org.briarproject.android.util.ElasticHorizontalSpace;
import org.briarproject.android.util.LayoutUtils; import org.briarproject.android.util.LayoutUtils;
import org.briarproject.api.db.MessageHeader; import org.briarproject.api.db.MessageHeader;
import org.briarproject.api.db.MessageHeader.State;
import org.briarproject.util.StringUtils; import org.briarproject.util.StringUtils;
import android.content.Context; import android.content.Context;
@@ -79,11 +80,16 @@ class ConversationAdapter extends ArrayAdapter<ConversationItem> {
footer.addView(new ElasticHorizontalSpace(ctx)); footer.addView(new ElasticHorizontalSpace(ctx));
ImageView delivered = new ImageView(ctx); ImageView status = new ImageView(ctx);
delivered.setPadding(0, 0, pad, 0); status.setPadding(0, 0, pad, 0);
delivered.setImageResource(R.drawable.message_delivered); if (item.getStatus() == State.DELIVERED) {
if (!item.isDelivered()) delivered.setVisibility(INVISIBLE); status.setImageResource(R.drawable.message_delivered);
footer.addView(delivered); } else if (item.getStatus() == State.SENT) {
status.setImageResource(R.drawable.message_sent);
} else {
status.setImageResource(R.drawable.message_stored);
}
footer.addView(status);
TextView date = new TextView(ctx); TextView date = new TextView(ctx);
date.setTextColor(res.getColor(R.color.private_message_date)); date.setTextColor(res.getColor(R.color.private_message_date));

View File

@@ -1,18 +1,19 @@
package org.briarproject.android.contact; package org.briarproject.android.contact;
import org.briarproject.api.db.MessageHeader; import org.briarproject.api.db.MessageHeader;
import org.briarproject.api.db.MessageHeader.State;
// This class is not thread-safe // This class is not thread-safe
class ConversationItem { class ConversationItem {
private final MessageHeader header; private final MessageHeader header;
private byte[] body; private byte[] body;
private boolean delivered; private State status;
ConversationItem(MessageHeader header) { ConversationItem(MessageHeader header) {
this.header = header; this.header = header;
body = null; body = null;
delivered = header.isDelivered(); status = header.getStatus();
} }
MessageHeader getHeader() { MessageHeader getHeader() {
@@ -27,11 +28,11 @@ class ConversationItem {
this.body = body; this.body = body;
} }
boolean isDelivered() { State getStatus() {
return delivered; return status;
} }
void setDelivered(boolean delivered) { void setStatus(State state) {
this.delivered = delivered; this.status = state;
} }
} }

View File

@@ -6,17 +6,20 @@ import org.briarproject.api.messaging.MessageId;
public class MessageHeader { public class MessageHeader {
public enum State { STORED, SENT, DELIVERED };
private final MessageId id, parent; private final MessageId id, parent;
private final GroupId groupId; private final GroupId groupId;
private final Author author; private final Author author;
private final Author.Status authorStatus; private final Author.Status authorStatus;
private final String contentType; private final String contentType;
private final long timestamp; private final long timestamp;
private final boolean local, read, delivered; private final boolean local, read;
private final State status;
public MessageHeader(MessageId id, MessageId parent, GroupId groupId, public MessageHeader(MessageId id, MessageId parent, GroupId groupId,
Author author, Author.Status authorStatus, String contentType, Author author, Author.Status authorStatus, String contentType,
long timestamp, boolean local, boolean read, boolean delivered) { long timestamp, boolean local, boolean read, State status) {
this.id = id; this.id = id;
this.parent = parent; this.parent = parent;
this.groupId = groupId; this.groupId = groupId;
@@ -26,7 +29,7 @@ public class MessageHeader {
this.timestamp = timestamp; this.timestamp = timestamp;
this.local = local; this.local = local;
this.read = read; this.read = read;
this.delivered = delivered; this.status = status;
} }
/** Returns the message's unique identifier. */ /** Returns the message's unique identifier. */
@@ -82,10 +85,9 @@ public class MessageHeader {
} }
/** /**
* Returns true if the message has been delivered. (This only applies to * Returns message status. (This only applies to locally generated private messages.)
* locally generated private messages.)
*/ */
public boolean isDelivered() { public State getStatus() {
return delivered; return status;
} }
} }

View File

@@ -0,0 +1,27 @@
package org.briarproject.api.event;
import java.util.Collection;
import org.briarproject.api.ContactId;
import org.briarproject.api.messaging.MessageId;
/** An event that is broadcast when messages are sent to a contact. */
public class MessagesSentEvent extends Event {
private final ContactId contactId;
private final Collection<MessageId> messageIds;
public MessagesSentEvent(ContactId contactId,
Collection<MessageId> messageIds) {
this.contactId = contactId;
this.messageIds = messageIds;
}
public ContactId getContactId() {
return contactId;
}
public Collection<MessageId> getMessageIds() {
return messageIds;
}
}

View File

@@ -52,6 +52,7 @@ import org.briarproject.api.event.MessageRequestedEvent;
import org.briarproject.api.event.MessageToAckEvent; import org.briarproject.api.event.MessageToAckEvent;
import org.briarproject.api.event.MessageToRequestEvent; import org.briarproject.api.event.MessageToRequestEvent;
import org.briarproject.api.event.MessagesAckedEvent; import org.briarproject.api.event.MessagesAckedEvent;
import org.briarproject.api.event.MessagesSentEvent;
import org.briarproject.api.event.RemoteRetentionTimeUpdatedEvent; import org.briarproject.api.event.RemoteRetentionTimeUpdatedEvent;
import org.briarproject.api.event.RemoteSubscriptionsUpdatedEvent; import org.briarproject.api.event.RemoteSubscriptionsUpdatedEvent;
import org.briarproject.api.event.RemoteTransportsUpdatedEvent; import org.briarproject.api.event.RemoteTransportsUpdatedEvent;
@@ -380,6 +381,7 @@ DatabaseCleaner.Callback {
lock.writeLock().unlock(); lock.writeLock().unlock();
} }
if (messages.isEmpty()) return null; if (messages.isEmpty()) return null;
if (!ids.isEmpty()) eventBus.broadcast(new MessagesSentEvent(c, ids));
return Collections.unmodifiableList(messages); return Collections.unmodifiableList(messages);
} }
@@ -455,6 +457,7 @@ DatabaseCleaner.Callback {
lock.writeLock().unlock(); lock.writeLock().unlock();
} }
if (messages.isEmpty()) return null; if (messages.isEmpty()) return null;
if (!ids.isEmpty()) eventBus.broadcast(new MessagesSentEvent(c, ids));
return Collections.unmodifiableList(messages); return Collections.unmodifiableList(messages);
} }

View File

@@ -44,6 +44,7 @@ import org.briarproject.api.TransportProperties;
import org.briarproject.api.db.DbClosedException; import org.briarproject.api.db.DbClosedException;
import org.briarproject.api.db.DbException; import org.briarproject.api.db.DbException;
import org.briarproject.api.db.MessageHeader; import org.briarproject.api.db.MessageHeader;
import org.briarproject.api.db.MessageHeader.State;
import org.briarproject.api.messaging.Group; import org.briarproject.api.messaging.Group;
import org.briarproject.api.messaging.GroupId; import org.briarproject.api.messaging.GroupId;
import org.briarproject.api.messaging.Message; import org.briarproject.api.messaging.Message;
@@ -1452,7 +1453,7 @@ abstract class JdbcDatabase implements Database<Connection> {
if (rs.next()) throw new DbException(); if (rs.next()) throw new DbException();
// Get the message headers // Get the message headers
sql = "SELECT m.messageId, parentId, m.groupId, contentType," sql = "SELECT m.messageId, parentId, m.groupId, contentType,"
+ " timestamp, local, read, seen" + " timestamp, local, read, seen, s.txCount"
+ " FROM messages AS m" + " FROM messages AS m"
+ " JOIN groups AS g" + " JOIN groups AS g"
+ " ON m.groupId = g.groupId" + " ON m.groupId = g.groupId"
@@ -1478,8 +1479,15 @@ abstract class JdbcDatabase implements Database<Connection> {
boolean read = rs.getBoolean(7); boolean read = rs.getBoolean(7);
boolean seen = rs.getBoolean(8); boolean seen = rs.getBoolean(8);
Author author = local ? localAuthor : remoteAuthor; Author author = local ? localAuthor : remoteAuthor;
// initialize message status
State status;
if (seen) status = State.DELIVERED;
else if (rs.getInt(9) > 0) status = State.SENT;
else status = State.STORED;
headers.add(new MessageHeader(id, parent, groupId, author, headers.add(new MessageHeader(id, parent, groupId, author,
VERIFIED, contentType, timestamp, local, read, seen)); VERIFIED, contentType, timestamp, local, read, status));
} }
rs.close(); rs.close();
ps.close(); ps.close();
@@ -1631,6 +1639,10 @@ abstract class JdbcDatabase implements Database<Connection> {
} }
} }
/**
* This method is used to get group messages.
* The message status won't be used.
*/
public Collection<MessageHeader> getMessageHeaders(Connection txn, public Collection<MessageHeader> getMessageHeaders(Connection txn,
GroupId g) throws DbException { GroupId g) throws DbException {
PreparedStatement ps = null; PreparedStatement ps = null;
@@ -1669,12 +1681,14 @@ abstract class JdbcDatabase implements Database<Connection> {
boolean read = rs.getBoolean(9); boolean read = rs.getBoolean(9);
boolean isSelf = rs.getBoolean(10); boolean isSelf = rs.getBoolean(10);
boolean isContact = rs.getBoolean(11); boolean isContact = rs.getBoolean(11);
Author.Status status; Author.Status status;
if (author == null) status = ANONYMOUS; if (author == null) status = ANONYMOUS;
else if (isSelf || isContact) status = VERIFIED; else if (isSelf || isContact) status = VERIFIED;
else status = UNKNOWN; else status = UNKNOWN;
headers.add(new MessageHeader(id, parent, g, author, status, headers.add(new MessageHeader(id, parent, g, author, status,
contentType, timestamp, local, read, false)); contentType, timestamp, local, read, State.STORED));
} }
rs.close(); rs.close();
ps.close(); ps.close();