Add XEP-0245 (/me command) support

Sam Whited created

Change summary

docs/XEPs.md                                                             |   1 
src/main/java/eu/siacs/conversations/entities/Message.java               |  47 
src/main/java/eu/siacs/conversations/ui/ConversationFragment.java        |  10 
src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java |  19 
src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java      | 288 
5 files changed, 206 insertions(+), 159 deletions(-)

Detailed changes

docs/XEPs.md 🔗

@@ -9,6 +9,7 @@
 * XEP-0198: Stream Management
 * XEP-0234: Jingle File Transfer
 * XEP-0237: Roster Versioning
+* XEP-0245: The /me Command
 * XEP-0249: Direct MUC Invitations (receiving only)
 * XEP-0260: Jingle SOCKS5 Bytestreams Transport Method
 * XEP-0261: Jingle In-Band Bytestreams Transport Method

src/main/java/eu/siacs/conversations/entities/Message.java 🔗

@@ -36,17 +36,18 @@ public class Message extends AbstractEntity {
 	public static final int TYPE_STATUS = 3;
 	public static final int TYPE_PRIVATE = 4;
 
-	public static String CONVERSATION = "conversationUuid";
-	public static String COUNTERPART = "counterpart";
-	public static String TRUE_COUNTERPART = "trueCounterpart";
-	public static String BODY = "body";
-	public static String TIME_SENT = "timeSent";
-	public static String ENCRYPTION = "encryption";
-	public static String STATUS = "status";
-	public static String TYPE = "type";
-	public static String REMOTE_MSG_ID = "remoteMsgId";
-	public static String SERVER_MSG_ID = "serverMsgId";
-	public static String RELATIVE_FILE_PATH = "relativeFilePath";
+	public static final String CONVERSATION = "conversationUuid";
+	public static final String COUNTERPART = "counterpart";
+	public static final String TRUE_COUNTERPART = "trueCounterpart";
+	public static final String BODY = "body";
+	public static final String TIME_SENT = "timeSent";
+	public static final String ENCRYPTION = "encryption";
+	public static final String STATUS = "status";
+	public static final String TYPE = "type";
+	public static final String REMOTE_MSG_ID = "remoteMsgId";
+	public static final String SERVER_MSG_ID = "serverMsgId";
+	public static final String RELATIVE_FILE_PATH = "relativeFilePath";
+
 	public boolean markable = false;
 	protected String conversationUuid;
 	protected Jid counterpart;
@@ -348,17 +349,35 @@ public class Message extends AbstractEntity {
 	}
 
 	public boolean mergeable(final Message message) {
-		return message != null && (message.getType() == Message.TYPE_TEXT && this.getDownloadable() == null && message.getDownloadable() == null && message.getEncryption() != Message.ENCRYPTION_PGP && this.getType() == message.getType() && this.getStatus() == message.getStatus() && this.getEncryption() == message.getEncryption() && this.getCounterpart() != null && this.getCounterpart().equals(message.getCounterpart()) && (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) && !message.bodyContainsDownloadable() && !this.bodyContainsDownloadable());
+		return message != null &&
+			(message.getType() == Message.TYPE_TEXT &&
+			 this.getDownloadable() == null &&
+			 message.getDownloadable() == null &&
+			 message.getEncryption() != Message.ENCRYPTION_PGP &&
+			 this.getType() == message.getType() &&
+			 this.getStatus() == message.getStatus() &&
+			 this.getEncryption() == message.getEncryption() &&
+			 this.getCounterpart() != null &&
+			 this.getCounterpart().equals(message.getCounterpart()) &&
+			 (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
+			 !message.bodyContainsDownloadable() &&
+			 !this.bodyContainsDownloadable() &&
+			 !this.body.startsWith("/me ")
+			);
 	}
 
 	public String getMergedBody() {
-		Message next = this.next();
+		final Message next = this.next();
 		if (this.mergeable(next)) {
-			return body.trim() + '\n' + next.getMergedBody();
+			return getBody() + '\n' + next.getMergedBody();
 		}
 		return body.trim();
 	}
 
+	public boolean hasMeCommand() {
+		return getMergedBody().startsWith("/me ");
+	}
+
 	public int getMergedStatus() {
 		return getStatus();
 	}

src/main/java/eu/siacs/conversations/ui/ConversationFragment.java 🔗

@@ -425,16 +425,16 @@ public class ConversationFragment extends Fragment {
 			if ((m.getType() != Message.TYPE_IMAGE && m.getDownloadable() == null)
 					|| m.getImageParams().url == null) {
 				copyUrl.setVisible(false);
-			}
+					}
 			if (m.getType() != Message.TYPE_TEXT
 					|| m.getDownloadable() != null
 					|| !m.bodyContainsDownloadable()) {
 				downloadImage.setVisible(false);
-			}
+					}
 			if (!((m.getDownloadable() != null && !(m.getDownloadable() instanceof DownloadablePlaceholder))
-					|| (m.isFileOrImage() && m.getStatus() == Message.STATUS_WAITING))) {
+						|| (m.isFileOrImage() && m.getStatus() == Message.STATUS_WAITING))) {
 				cancelTransmission.setVisible(false);
-			}
+						}
 		}
 	}
 
@@ -657,7 +657,7 @@ public class ConversationFragment extends Fragment {
 					}
 						}
 				conversation.populateWithMessages(ConversationFragment.this.messageList);
-				for (Message message : this.messageList) {
+				for (final Message message : this.messageList) {
 					if (message.getEncryption() == Message.ENCRYPTION_PGP
 							&& (message.getStatus() == Message.STATUS_RECEIVED || message
 								.getStatus() >= Message.STATUS_SEND)

src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java 🔗

@@ -1,5 +1,15 @@
 package eu.siacs.conversations.ui.adapter;
 
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Typeface;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+
 import java.util.List;
 
 import eu.siacs.conversations.R;
@@ -10,15 +20,6 @@ import eu.siacs.conversations.entities.Message;
 import eu.siacs.conversations.ui.ConversationActivity;
 import eu.siacs.conversations.ui.XmppActivity;
 import eu.siacs.conversations.utils.UIHelper;
-import android.content.Context;
-import android.graphics.Color;
-import android.graphics.Typeface;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ArrayAdapter;
-import android.widget.ImageView;
-import android.widget.TextView;
 
 public class ConversationAdapter extends ArrayAdapter<Conversation> {
 

src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java 🔗

@@ -52,14 +52,14 @@ public class MessageAdapter extends ArrayAdapter<Message> {
 	private OnContactPictureLongClicked mOnContactPictureLongClickedListener;
 
 	private OnLongClickListener openContextMenu = new OnLongClickListener() {
-		
+
 		@Override
 		public boolean onLongClick(View v) {
 			v.showContextMenu();
 			return true;
 		}
 	};
-	
+
 	public MessageAdapter(ConversationActivity activity, List<Message> messages) {
 		super(activity, 0, messages);
 		this.activity = activity;
@@ -73,7 +73,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
 	public void setOnContactPictureLongClicked(
 			OnContactPictureLongClicked listener) {
 		this.mOnContactPictureLongClickedListener = listener;
-	}
+			}
 
 	@Override
 	public int getViewTypeCount() {
@@ -101,7 +101,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
 			viewHolder.indicatorReceived.setVisibility(View.GONE);
 		}
 		boolean multiReceived = message.getConversation().getMode() == Conversation.MODE_MULTI
-				&& message.getMergedStatus() <= Message.STATUS_RECEIVED;
+			&& message.getMergedStatus() <= Message.STATUS_RECEIVED;
 		if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE || message.getDownloadable() != null) {
 			ImageParams params = message.getImageParams();
 			if (params.size > (1.5 * 1024 * 1024)) {
@@ -114,44 +114,39 @@ public class MessageAdapter extends ArrayAdapter<Message> {
 			}
 		}
 		switch (message.getMergedStatus()) {
-		case Message.STATUS_WAITING:
-			info = getContext().getString(R.string.waiting);
-			break;
-		case Message.STATUS_UNSEND:
-			Downloadable d = message.getDownloadable();
-			if (d!=null) {
-				info = getContext().getString(R.string.sending_file,d.getProgress());
-			} else {
-				info = getContext().getString(R.string.sending);
-			}
-			break;
-		case Message.STATUS_OFFERED:
-			info = getContext().getString(R.string.offering);
-			break;
-		case Message.STATUS_SEND_RECEIVED:
-			if (activity.indicateReceived()) {
-				viewHolder.indicatorReceived.setVisibility(View.VISIBLE);
-			}
-			break;
-		case Message.STATUS_SEND_DISPLAYED:
-			if (activity.indicateReceived()) {
-				viewHolder.indicatorReceived.setVisibility(View.VISIBLE);
-			}
-			break;
-		case Message.STATUS_SEND_FAILED:
-			info = getContext().getString(R.string.send_failed);
-			error = true;
-			break;
-		default:
-			if (multiReceived) {
-				Contact contact = message.getContact();
-				if (contact != null) {
-					info = contact.getDisplayName();
+			case Message.STATUS_WAITING:
+				info = getContext().getString(R.string.waiting);
+				break;
+			case Message.STATUS_UNSEND:
+				Downloadable d = message.getDownloadable();
+				if (d!=null) {
+					info = getContext().getString(R.string.sending_file,d.getProgress());
 				} else {
-					info = getDisplayedMucCounterpart(message.getCounterpart());
+					info = getContext().getString(R.string.sending);
 				}
-			}
-			break;
+				break;
+			case Message.STATUS_OFFERED:
+				info = getContext().getString(R.string.offering);
+				break;
+			case Message.STATUS_SEND_RECEIVED:
+				if (activity.indicateReceived()) {
+					viewHolder.indicatorReceived.setVisibility(View.VISIBLE);
+				}
+				break;
+			case Message.STATUS_SEND_DISPLAYED:
+				if (activity.indicateReceived()) {
+					viewHolder.indicatorReceived.setVisibility(View.VISIBLE);
+				}
+				break;
+			case Message.STATUS_SEND_FAILED:
+				info = getContext().getString(R.string.send_failed);
+				error = true;
+				break;
+			default:
+				if (multiReceived) {
+					info = getMessageDisplayName(message);
+				}
+				break;
 		}
 		if (error) {
 			viewHolder.time.setTextColor(activity.getWarningTextColor());
@@ -212,26 +207,53 @@ public class MessageAdapter extends ArrayAdapter<Message> {
 		viewHolder.image.setVisibility(View.GONE);
 		viewHolder.messageBody.setVisibility(View.VISIBLE);
 		viewHolder.messageBody.setText(getContext().getString(
-				R.string.decryption_failed));
+					R.string.decryption_failed));
 		viewHolder.messageBody.setTextColor(activity.getWarningTextColor());
 		viewHolder.messageBody.setTypeface(null, Typeface.NORMAL);
 		viewHolder.messageBody.setTextIsSelectable(false);
 	}
 
-	private void displayTextMessage(ViewHolder viewHolder, Message message) {
+	private String getMessageDisplayName(final Message message) {
+		if (message.getStatus() == Message.STATUS_RECEIVED) {
+			if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
+				return getDisplayedMucCounterpart(message.getCounterpart());
+			} else {
+				final Contact contact = message.getContact();
+				return contact != null ? contact.getDisplayName() : "";
+			}
+		} else {
+			if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
+				return getDisplayedMucCounterpart(message.getConversation().getJid());
+			} else {
+				final Jid jid = message.getConversation().getAccount().getJid();
+				return jid.hasLocalpart() ? jid.getLocalpart() : jid.toDomainJid().toString();
+			}
+		}
+	}
+
+	private void displayTextMessage(final ViewHolder viewHolder, final Message message) {
 		if (viewHolder.download_button != null) {
 			viewHolder.download_button.setVisibility(View.GONE);
 		}
 		viewHolder.image.setVisibility(View.GONE);
 		viewHolder.messageBody.setVisibility(View.VISIBLE);
 		if (message.getBody() != null) {
+			final String nick = getMessageDisplayName(message);
+			final String formattedBody = message.getMergedBody().replaceAll("^/me ", nick + " ");
 			if (message.getType() != Message.TYPE_PRIVATE) {
-				viewHolder.messageBody.setText(message.getMergedBody());
+				if (message.hasMeCommand()) {
+					final Spannable span = new SpannableString(formattedBody);
+					span.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), 0, nick.length(),
+							Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+					viewHolder.messageBody.setText(span);
+				} else {
+					viewHolder.messageBody.setText(message.getMergedBody());
+				}
 			} else {
 				String privateMarker;
 				if (message.getStatus() <= Message.STATUS_RECEIVED) {
 					privateMarker = activity
-							.getString(R.string.private_message);
+						.getString(R.string.private_message);
 				} else {
 					final String to;
 					if (message.getCounterpart() != null) {
@@ -241,15 +263,19 @@ public class MessageAdapter extends ArrayAdapter<Message> {
 					}
 					privateMarker = activity.getString(R.string.private_message_to, to);
 				}
-				SpannableString span = new SpannableString(privateMarker + " "
-						+ message.getBody());
-				span.setSpan(
-						new ForegroundColorSpan(activity
-								.getSecondaryTextColor()), 0, privateMarker
-								.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
-				span.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 0,
+				final Spannable span = new SpannableString(privateMarker + " "
+						+ formattedBody);
+				span.setSpan(new ForegroundColorSpan(activity
+							.getSecondaryTextColor()), 0, privateMarker
+						.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+				span.setSpan(new StyleSpan(Typeface.BOLD), 0,
 						privateMarker.length(),
 						Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+				if (message.hasMeCommand()) {
+					span.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), privateMarker.length() + 1,
+							privateMarker.length() + 1 + nick.length(),
+							Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+				}
 				viewHolder.messageBody.setText(span);
 			}
 		} else {
@@ -281,7 +307,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
 		viewHolder.image.setVisibility(View.GONE);
 		viewHolder.messageBody.setVisibility(View.GONE);
 		viewHolder.download_button.setVisibility(View.VISIBLE);
-		viewHolder.download_button.setText(activity.getString(R.string.open_file,file.getMimeType()));
+		viewHolder.download_button.setText(activity.getString(R.string.open_file, file.getMimeType()));
 		viewHolder.download_button.setOnClickListener(new OnClickListener() {
 
 			@Override
@@ -311,7 +337,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
 			scalledH = (int) (params.height / ((double) params.width / target));
 		}
 		viewHolder.image.setLayoutParams(new LinearLayout.LayoutParams(
-				scalledW, scalledH));
+					scalledW, scalledH));
 		activity.loadBitmap(message, viewHolder.image);
 		viewHolder.image.setOnClickListener(new OnClickListener() {
 
@@ -346,59 +372,59 @@ public class MessageAdapter extends ArrayAdapter<Message> {
 		if (view == null) {
 			viewHolder = new ViewHolder();
 			switch (type) {
-			case NULL:
-				view = activity.getLayoutInflater().inflate(
-						R.layout.message_null, parent, false);
-				break;
-			case SENT:
-				view = activity.getLayoutInflater().inflate(
-						R.layout.message_sent, parent, false);
-				viewHolder.message_box = (LinearLayout) view
+				case NULL:
+					view = activity.getLayoutInflater().inflate(
+							R.layout.message_null, parent, false);
+					break;
+				case SENT:
+					view = activity.getLayoutInflater().inflate(
+							R.layout.message_sent, parent, false);
+					viewHolder.message_box = (LinearLayout) view
 						.findViewById(R.id.message_box);
-				viewHolder.contact_picture = (ImageView) view
+					viewHolder.contact_picture = (ImageView) view
 						.findViewById(R.id.message_photo);
-				viewHolder.download_button = (Button) view
+					viewHolder.download_button = (Button) view
 						.findViewById(R.id.download_button);
-				viewHolder.indicator = (ImageView) view
+					viewHolder.indicator = (ImageView) view
 						.findViewById(R.id.security_indicator);
-				viewHolder.image = (ImageView) view
+					viewHolder.image = (ImageView) view
 						.findViewById(R.id.message_image);
-				viewHolder.messageBody = (TextView) view
+					viewHolder.messageBody = (TextView) view
 						.findViewById(R.id.message_body);
-				viewHolder.time = (TextView) view
+					viewHolder.time = (TextView) view
 						.findViewById(R.id.message_time);
-				viewHolder.indicatorReceived = (ImageView) view
+					viewHolder.indicatorReceived = (ImageView) view
 						.findViewById(R.id.indicator_received);
-				break;
-			case RECEIVED:
-				view = activity.getLayoutInflater().inflate(
-						R.layout.message_received, parent, false);
-				viewHolder.message_box = (LinearLayout) view
+					break;
+				case RECEIVED:
+					view = activity.getLayoutInflater().inflate(
+							R.layout.message_received, parent, false);
+					viewHolder.message_box = (LinearLayout) view
 						.findViewById(R.id.message_box);
-				viewHolder.contact_picture = (ImageView) view
+					viewHolder.contact_picture = (ImageView) view
 						.findViewById(R.id.message_photo);
-				viewHolder.download_button = (Button) view
+					viewHolder.download_button = (Button) view
 						.findViewById(R.id.download_button);
-				viewHolder.indicator = (ImageView) view
+					viewHolder.indicator = (ImageView) view
 						.findViewById(R.id.security_indicator);
-				viewHolder.image = (ImageView) view
+					viewHolder.image = (ImageView) view
 						.findViewById(R.id.message_image);
-				viewHolder.messageBody = (TextView) view
+					viewHolder.messageBody = (TextView) view
 						.findViewById(R.id.message_body);
-				viewHolder.time = (TextView) view
+					viewHolder.time = (TextView) view
 						.findViewById(R.id.message_time);
-				viewHolder.indicatorReceived = (ImageView) view
+					viewHolder.indicatorReceived = (ImageView) view
 						.findViewById(R.id.indicator_received);
-				break;
-			case STATUS:
-				view = activity.getLayoutInflater().inflate(
-						R.layout.message_status, parent, false);
-				viewHolder.contact_picture = (ImageView) view
+					break;
+				case STATUS:
+					view = activity.getLayoutInflater().inflate(
+							R.layout.message_status, parent, false);
+					viewHolder.contact_picture = (ImageView) view
 						.findViewById(R.id.message_photo);
-				break;
-			default:
-				viewHolder = null;
-				break;
+					break;
+				default:
+					viewHolder = null;
+					break;
 			}
 			view.setTag(viewHolder);
 		} else {
@@ -412,22 +438,22 @@ public class MessageAdapter extends ArrayAdapter<Message> {
 			if (conversation.getMode() == Conversation.MODE_SINGLE) {
 				viewHolder.contact_picture.setImageBitmap(activity
 						.avatarService().get(conversation.getContact(),
-								activity.getPixel(32)));
+							activity.getPixel(32)));
 				viewHolder.contact_picture.setAlpha(0.5f);
 				viewHolder.contact_picture
-						.setOnClickListener(new OnClickListener() {
-
-							@Override
-							public void onClick(View v) {
-								String name = conversation.getName();
-								String read = getContext()
-										.getString(
-												R.string.contact_has_read_up_to_this_point,
-												name);
-								Toast.makeText(getContext(), read,
-										Toast.LENGTH_SHORT).show();
-							}
-						});
+					.setOnClickListener(new OnClickListener() {
+
+						@Override
+						public void onClick(View v) {
+							String name = conversation.getName();
+							String read = getContext()
+								.getString(
+										R.string.contact_has_read_up_to_this_point,
+										name);
+							Toast.makeText(getContext(), read,
+									Toast.LENGTH_SHORT).show();
+						}
+					});
 
 			}
 			return view;
@@ -456,38 +482,38 @@ public class MessageAdapter extends ArrayAdapter<Message> {
 				viewHolder.contact_picture.setImageBitmap(activity.avatarService().get(contact, activity.getPixel(48)));
 			} else if (conversation.getMode() == Conversation.MODE_MULTI) {
 				viewHolder.contact_picture.setImageBitmap(activity.avatarService().get(getDisplayedMucCounterpart(message.getCounterpart()),
-                        activity.getPixel(48)));
+							activity.getPixel(48)));
 			}
 		} else if (type == SENT) {
 			viewHolder.contact_picture.setImageBitmap(activity.avatarService().get(account, activity.getPixel(48)));
 		}
 
 		viewHolder.contact_picture
-				.setOnClickListener(new OnClickListener() {
-
-					@Override
-					public void onClick(View v) {
-						if (MessageAdapter.this.mOnContactPictureClickedListener != null) {
-							MessageAdapter.this.mOnContactPictureClickedListener
-									.onContactPictureClicked(message);
-						}
+			.setOnClickListener(new OnClickListener() {
 
+				@Override
+				public void onClick(View v) {
+					if (MessageAdapter.this.mOnContactPictureClickedListener != null) {
+						MessageAdapter.this.mOnContactPictureClickedListener
+							.onContactPictureClicked(message);
 					}
-				});
+
+				}
+			});
 		viewHolder.contact_picture
-				.setOnLongClickListener(new OnLongClickListener() {
-
-					@Override
-					public boolean onLongClick(View v) {
-						if (MessageAdapter.this.mOnContactPictureLongClickedListener != null) {
-							MessageAdapter.this.mOnContactPictureLongClickedListener
-									.onContactPictureLongClicked(message);
-							return true;
-						} else {
-							return false;
-						}
+			.setOnLongClickListener(new OnLongClickListener() {
+
+				@Override
+				public boolean onLongClick(View v) {
+					if (MessageAdapter.this.mOnContactPictureLongClickedListener != null) {
+						MessageAdapter.this.mOnContactPictureLongClickedListener
+							.onContactPictureLongClicked(message);
+						return true;
+					} else {
+						return false;
 					}
-				});
+				}
+			});
 
 		if (message.getDownloadable() != null && message.getDownloadable().getStatus() != Downloadable.STATUS_UPLOADING) {
 			Downloadable d = message.getDownloadable();
@@ -536,13 +562,13 @@ public class MessageAdapter extends ArrayAdapter<Message> {
 						activity.getString(R.string.install_openkeychain));
 				if (viewHolder != null) {
 					viewHolder.message_box
-							.setOnClickListener(new OnClickListener() {
+						.setOnClickListener(new OnClickListener() {
 
-								@Override
-								public void onClick(View v) {
-									activity.showInstallPgpDialog();
-								}
-							});
+							@Override
+							public void onClick(View v) {
+								activity.showInstallPgpDialog();
+							}
+						});
 				}
 			}
 		} else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) {