Allow setting subject on messages

Stephen Paul Weber created

Change summary

src/cheogram/res/drawable/subject.xml                                    | 10 
src/main/java/eu/siacs/conversations/entities/Message.java               |  8 
src/main/java/eu/siacs/conversations/generator/MessageGenerator.java     |  1 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java | 14 
src/main/java/eu/siacs/conversations/ui/ConversationFragment.java        | 41 
src/main/java/eu/siacs/conversations/ui/util/SendButtonTool.java         |  6 
src/main/res/layout/fragment_conversation.xml                            | 34 
src/main/res/menu/fragment_conversation.xml                              |  5 
8 files changed, 89 insertions(+), 30 deletions(-)

Detailed changes

src/cheogram/res/drawable/subject.xml 🔗

@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="960"
+    android:viewportHeight="960"
+    android:autoMirrored="true">
+  <path
+      android:fillColor="?icon_tint"
+      android:pathData="M160,760L160,680L560,680L560,760L160,760ZM160,600L160,520L800,520L800,600L160,600ZM160,440L160,360L800,360L800,440L160,440ZM160,280L160,200L800,200L800,280L160,280Z"/>
+</vector>

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

@@ -518,7 +518,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
     }
 
     private Pair<StringBuilder, Boolean> bodyMinusFallbacks(String... fallbackNames) {
-        StringBuilder body = new StringBuilder(this.body);
+        StringBuilder body = new StringBuilder(this.body == null ? "" : this.body);
 
         List<Element> fallbacks = getFallbacks(fallbackNames);
         List<Pair<Integer, Integer>> spans = new ArrayList<>();
@@ -541,6 +541,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
     }
 
     public String getBody() {
+        if (body == null) return "";
+
         Pair<StringBuilder, Boolean> result = bodyMinusFallbacks("http://jabber.org/protocol/address", Namespace.OOB);
         StringBuilder body = result.first;
 
@@ -568,8 +570,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
     }
 
     public synchronized void setBody(Spanned span) {
-        setBody(span.toString());
-        if (SpannedToXHTML.isPlainText(span)) {
+        setBody(span == null ? null : span.toString());
+        if (span == null || SpannedToXHTML.isPlainText(span)) {
             this.payloads.remove(getHtml(true));
         } else {
             final Element body = getOrMakeHtml();

src/main/java/eu/siacs/conversations/generator/MessageGenerator.java 🔗

@@ -64,6 +64,7 @@ public class MessageGenerator extends AbstractGenerator {
             packet.addChild("replace", "urn:xmpp:message-correct:0").setAttribute("id", message.getEditedIdWireFormat());
         }
         if (!legacyEncryption) {
+            if (message.getSubject() != null && message.getSubject().length() > 0) packet.addChild("subject").setContent(message.getSubject());
             // Legacy encryption can't handle advanced payloads
             for (Element el : message.getPayloads()) {
                 packet.addChild(el);

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java 🔗

@@ -658,12 +658,13 @@ public class XmppConnectionService extends Service {
         return this.mAvatarService;
     }
 
-    public void attachLocationToConversation(final Conversation conversation, final Uri uri, final UiCallback<Message> callback) {
+    public void attachLocationToConversation(final Conversation conversation, final Uri uri, final String subject, final UiCallback<Message> callback) {
         int encryption = conversation.getNextEncryption();
         if (encryption == Message.ENCRYPTION_PGP) {
             encryption = Message.ENCRYPTION_DECRYPTED;
         }
         Message message = new Message(conversation, uri.toString(), encryption);
+        if (subject != null && subject.length() > 0) message.setSubject(subject);
         message.setThread(conversation.getThread());
         Message.configurePrivateMessage(message);
         if (encryption == Message.ENCRYPTION_DECRYPTED) {
@@ -674,7 +675,7 @@ public class XmppConnectionService extends Service {
         }
     }
 
-    public void attachFileToConversation(final Conversation conversation, final Uri uri, final String type, final UiCallback<Message> callback) {
+    public void attachFileToConversation(final Conversation conversation, final Uri uri, final String type, final String subject, final UiCallback<Message> callback) {
         final Message message;
         if (conversation.getReplyTo() == null) {
             message = new Message(conversation, "", conversation.getNextEncryption());
@@ -685,6 +686,8 @@ public class XmppConnectionService extends Service {
         if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
             message.setEncryption(Message.ENCRYPTION_DECRYPTED);
         }
+        if (subject.length() > 0) message.setSubject(subject);
+        if (subject != null && subject.length() > 0) message.setSubject(subject);
         message.setThread(conversation.getThread());
         if (!Message.configurePrivateFileMessage(message)) {
             message.setCounterpart(conversation.getNextCounterpart());
@@ -700,7 +703,7 @@ public class XmppConnectionService extends Service {
         }
     }
 
-    public void attachImageToConversation(final Conversation conversation, final Uri uri,  final String type, final UiCallback<Message> callback) {
+    public void attachImageToConversation(final Conversation conversation, final Uri uri, final String type, final String subject, final UiCallback<Message> callback) {
         final String mimeType = MimeUtils.guessMimeTypeFromUriAndMime(this, uri, type);
         final String compressPictures = getCompressPicturesPreference();
 
@@ -709,7 +712,7 @@ public class XmppConnectionService extends Service {
                 || (mimeType != null && mimeType.endsWith("/gif"))
                 || getFileBackend().unusualBounds(uri)) {
             Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": not compressing picture. sending as file");
-            attachFileToConversation(conversation, uri, mimeType, callback);
+            attachFileToConversation(conversation, uri, mimeType, subject, callback);
             return;
         }
         final Message message;
@@ -723,6 +726,7 @@ public class XmppConnectionService extends Service {
         if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
             message.setEncryption(Message.ENCRYPTION_DECRYPTED);
         }
+        if (subject != null && subject.length() > 0) message.setSubject(subject);
         message.setThread(conversation.getThread());
         if (!Message.configurePrivateFileMessage(message)) {
             message.setCounterpart(conversation.getNextCounterpart());
@@ -734,7 +738,7 @@ public class XmppConnectionService extends Service {
                 getFileBackend().copyImageToPrivateStorage(message, uri);
             } catch (FileBackend.ImageCompressionException e) {
                 Log.d(Config.LOGTAG, "unable to compress image. fall back to file transfer", e);
-                attachFileToConversation(conversation, uri, mimeType, callback);
+                attachFileToConversation(conversation, uri, mimeType, subject, callback);
                 return;
             } catch (final FileBackend.FileCopyException e) {
                 callback.error(e.getResId(), message);

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

@@ -618,6 +618,8 @@ public class ConversationFragment extends XmppFragment
                                     } else {
                                         binding.textinput.setText("");
                                     }
+                                    binding.textinputSubject.setText("");
+                                    binding.textinputSubject.setVisibility(View.GONE);
                                     updateChatMsgHint();
                                     updateSendButton();
                                     updateEditablity();
@@ -817,13 +819,17 @@ public class ConversationFragment extends XmppFragment
         if (conversation == null) {
             return;
         }
+        final String subject = binding.textinputSubject.getText().toString();
         activity.xmppConnectionService.attachLocationToConversation(
                 conversation,
                 uri,
+                subject,
                 new UiCallback<Message>() {
 
                     @Override
-                    public void success(Message message) {}
+                    public void success(Message message) {
+                        messageSent();
+                    }
 
                     @Override
                     public void error(int errorCode, Message object) {
@@ -839,6 +845,7 @@ public class ConversationFragment extends XmppFragment
         if (conversation == null) {
             return;
         }
+        final String subject = binding.textinputSubject.getText().toString();
         if (type == "application/xdc+zip") newSubThread();
         final Toast prepareFileToast =
                 Toast.makeText(getActivity(), getText(R.string.preparing_file), Toast.LENGTH_LONG);
@@ -848,6 +855,7 @@ public class ConversationFragment extends XmppFragment
                 conversation,
                 uri,
                 type,
+                subject,
                 new UiInformableCallback<Message>() {
                     @Override
                     public void inform(final String text) {
@@ -859,7 +867,7 @@ public class ConversationFragment extends XmppFragment
                     public void success(Message message) {
                         runOnUiThread(() -> {
                             activity.hideToast();
-                            setupReply(null);
+                            messageSent();
                         });
                         hidePrepareFileToast(prepareFileToast);
                     }
@@ -887,6 +895,7 @@ public class ConversationFragment extends XmppFragment
         if (conversation == null) {
             return;
         }
+        final String subject = binding.textinputSubject.getText().toString();
         final Toast prepareFileToast =
                 Toast.makeText(getActivity(), getText(R.string.preparing_image), Toast.LENGTH_LONG);
         prepareFileToast.show();
@@ -895,6 +904,7 @@ public class ConversationFragment extends XmppFragment
                 conversation,
                 uri,
                 type,
+                subject,
                 new UiCallback<Message>() {
 
                     @Override
@@ -905,7 +915,7 @@ public class ConversationFragment extends XmppFragment
                     @Override
                     public void success(Message message) {
                         hidePrepareFileToast(prepareFileToast);
-                        runOnUiThread(() -> setupReply(null));
+                        runOnUiThread(() -> messageSent());
                     }
 
                     @Override
@@ -934,7 +944,8 @@ public class ConversationFragment extends XmppFragment
         Editable body = this.binding.textinput.getText();
         if (body == null) body = new SpannableStringBuilder("");
         final Conversation conversation = this.conversation;
-        if (body.length() == 0 || conversation == null) {
+        final boolean hasSubject = binding.textinputSubject.getText().length() > 0;
+        if (conversation == null || body.length() == 0) { // (conversation.getThread() == null || !hasSubject))) https://issues.prosody.im/1838
             binding.textSendButton.showContextMenu(0, 0);
             return;
         }
@@ -959,7 +970,7 @@ public class ConversationFragment extends XmppFragment
                 message.setEncryption(conversation.getNextEncryption());
             } else {
                 message = new Message(conversation, body.toString(), conversation.getNextEncryption());
-                message.setBody(body);
+                message.setBody(hasSubject && body.length() == 0 ? null : body);
                 if (message.bodyIsOnlyEmojis()) {
                     SpannableStringBuilder spannable = message.getSpannableBody(null, null);
                     ImageSpan[] imageSpans = spannable.getSpans(0, spannable.length(), ImageSpan.class);
@@ -981,6 +992,7 @@ public class ConversationFragment extends XmppFragment
                     }
                 }
             }
+            if (hasSubject) message.setSubject(binding.textinputSubject.getText().toString());
             message.setThread(conversation.getThread());
             if (attention) {
                 message.addPayload(new Element("attention", "urn:xmpp:attention:0"));
@@ -988,7 +1000,8 @@ public class ConversationFragment extends XmppFragment
             Message.configurePrivateMessage(message);
         } else {
             message = conversation.getCorrectingMessage();
-            message.setBody(body);
+            message.setBody(hasSubject && body.length() == 0 ? null : body);
+            if (hasSubject) message.setSubject(binding.textinputSubject.getText().toString());
             message.setThread(conversation.getThread());
             message.putEdited(message.getUuid(), message.getServerMsgId());
             message.setServerMsgId(null);
@@ -1809,6 +1822,7 @@ public class ConversationFragment extends XmppFragment
                             }
                         }
                         message.setBody(" ");
+                        message.setSubject(null);
                         message.putEdited(message.getUuid(), message.getServerMsgId());
                         message.setServerMsgId(null);
                         message.setUuid(UUID.randomUUID().toString());
@@ -1923,6 +1937,9 @@ public class ConversationFragment extends XmppFragment
             case R.id.attach_location:
                 handleAttachmentSelection(item);
                 break;
+            case R.id.attach_subject:
+                binding.textinputSubject.setVisibility(binding.textinputSubject.getVisibility() == View.GONE ? View.VISIBLE : View.GONE);
+                break;
             case R.id.action_search:
                 startSearch();
                 break;
@@ -2856,6 +2873,10 @@ public class ConversationFragment extends XmppFragment
         this.conversation.setDraftMessage(editable.toString());
         this.binding.textinput.setText("");
         this.binding.textinput.append(message.getBody());
+        if (message.getSubject() != null && message.getSubject().length() > 0) {
+            this.binding.textinputSubject.setText(message.getSubject());
+            this.binding.textinputSubject.setVisibility(View.VISIBLE);
+        }
     }
 
     private void highlightInConference(String nick) {
@@ -3072,6 +3093,7 @@ public class ConversationFragment extends XmppFragment
         this.binding.textSendButton.setContentDescription(
                 activity.getString(R.string.send_message_to_x, conversation.getName()));
         this.binding.textinput.setKeyboardListener(null);
+        this.binding.textinputSubject.setKeyboardListener(null);
         final boolean participating =
                 conversation.getMode() == Conversational.MODE_SINGLE
                         || conversation.getMucOptions().participating();
@@ -3082,6 +3104,7 @@ public class ConversationFragment extends XmppFragment
             this.binding.textinput.setText(MessageUtils.EMPTY_STRING);
         }
         this.binding.textinput.setKeyboardListener(this);
+        this.binding.textinputSubject.setKeyboardListener(this);
         messageListAdapter.updatePreferences();
         refresh(false);
         activity.invalidateOptionsMenu();
@@ -3556,6 +3579,8 @@ public class ConversationFragment extends XmppFragment
     }
 
     protected void messageSent() {
+        binding.textinputSubject.setText("");
+        binding.textinputSubject.setVisibility(View.GONE);
         setThread(null);
         conversation.setUserSelectedThread(false);
         mSendingPgpMessage.set(false);
@@ -3636,7 +3661,7 @@ public class ConversationFragment extends XmppFragment
         if (hasAttachments) {
             action = SendButtonAction.TEXT;
         } else {
-            action = SendButtonTool.getAction(getActivity(), c, text);
+            action = SendButtonTool.getAction(getActivity(), c, text, binding.textinputSubject.getText().toString());
         }
         if (c.getAccount().getStatus() == Account.State.ONLINE) {
             if (activity != null
@@ -3658,7 +3683,7 @@ public class ConversationFragment extends XmppFragment
         final Activity activity = getActivity();
         if (activity != null) {
             this.binding.textSendButton.setImageDrawable(
-                    SendButtonTool.getSendButtonImageResource(activity, action, status, text.length() > 0 || hasAttachments));
+                    SendButtonTool.getSendButtonImageResource(activity, action, status, text.length() > 0 || hasAttachments)); // || (c.getThread() != null && binding.textinputSubject.getText().length() > 0))); https://issues.prosody.im/1838
         }
 
         ViewGroup.LayoutParams params = binding.threadIdenticonLayout.getLayoutParams();

src/main/java/eu/siacs/conversations/ui/util/SendButtonTool.java 🔗

@@ -44,14 +44,14 @@ import eu.siacs.conversations.utils.UIHelper;
 
 public class SendButtonTool {
 
-	public static SendButtonAction getAction(final Activity activity, final Conversation c, final String text) {
+	public static SendButtonAction getAction(final Activity activity, final Conversation c, final String text, final String subject) {
 		if (activity == null) {
 			return SendButtonAction.TEXT;
 		}
 		final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
 		final boolean empty = text.length() == 0;
 		final boolean conference = c.getMode() == Conversation.MODE_MULTI;
-		if (c.getCorrectingMessage() != null && (empty || (text.equals(c.getCorrectingMessage().getBody()) && (c.getThread() == c.getCorrectingMessage().getThread() || (c.getThread() != null && c.getThread().equals(c.getCorrectingMessage().getThread())))))) {
+		if (c.getCorrectingMessage() != null && (empty || (text.equals(c.getCorrectingMessage().getBody()) && (subject.equals(c.getCorrectingMessage().getSubject())) && (c.getThread() == c.getCorrectingMessage().getThread() || (c.getThread() != null && c.getThread().equals(c.getCorrectingMessage().getThread())))))) {
 			return SendButtonAction.CANCEL;
 		} else if (conference && !c.getAccount().httpUploadAvailable()) {
 			if (empty && c.getNextCounterpart() != null) {
@@ -60,7 +60,7 @@ public class SendButtonTool {
 				return SendButtonAction.TEXT;
 			}
 		} else {
-			if (empty) {
+			if (empty && (c.getThread() == null || subject.length() == 0)) {
 				if (conference && c.getNextCounterpart() != null) {
 					return SendButtonAction.CANCEL;
 				} else {

src/main/res/layout/fragment_conversation.xml 🔗

@@ -105,17 +105,6 @@
                         android:layout_toEndOf="@+id/thread_identicon_layout"
                         android:layout_toRightOf="@+id/thread_identicon_layout"
                         android:orientation="vertical">
-    
-                        <TextView
-                            android:id="@+id/text_input_hint"
-                            android:layout_width="wrap_content"
-                            android:layout_height="wrap_content"
-                            android:layout_marginTop="8dp"
-                            android:maxLines="1"
-                            android:paddingLeft="8dp"
-                            android:paddingRight="8dp"
-                            android:textAppearance="@style/TextAppearance.Conversations.Caption.Highlight"
-                            android:visibility="gone" />
 
                         <androidx.recyclerview.widget.RecyclerView
                             android:id="@+id/media_preview"
@@ -130,6 +119,29 @@
 
                         </androidx.recyclerview.widget.RecyclerView>
 
+                        <eu.siacs.conversations.ui.widget.EditMessage
+                            android:id="@+id/textinput_subject"
+                            style="@style/Widget.Conversations.EditText"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:hint="Subject"
+                            android:maxLines="1"
+                            android:padding="8dp"
+                            android:imeOptions="flagNoExtractUi"
+                            android:inputType="textShortMessage|textMultiLine|textCapSentences"
+                            android:visibility="gone" />
+
+                        <TextView
+                            android:id="@+id/text_input_hint"
+                            android:layout_width="wrap_content"
+                            android:layout_height="wrap_content"
+                            android:layout_marginTop="8dp"
+                            android:maxLines="1"
+                            android:paddingLeft="8dp"
+                            android:paddingRight="8dp"
+                            android:textAppearance="@style/TextAppearance.Conversations.Caption.Highlight"
+                            android:visibility="gone" />
+
                         <eu.siacs.conversations.ui.widget.EditMessage
                             android:id="@+id/textinput"
                             style="@style/Widget.Conversations.EditText"

src/main/res/menu/fragment_conversation.xml 🔗

@@ -61,6 +61,11 @@
                 android:id="@+id/attach_location"
                 android:icon="?attr/ic_attach_location"
                 android:title="@string/send_location" />
+
+            <item
+                android:id="@+id/attach_subject"
+                android:icon="@drawable/subject"
+                android:title="Add Subject" />
         </menu>
     </item>
     <item