Show thread marker, send thread in stanza, allow replying and starting new thread

Stephen Paul Weber created

Change summary

src/main/java/eu/siacs/conversations/entities/Conversation.java      |  9 
src/main/java/eu/siacs/conversations/entities/Message.java           | 13 
src/main/java/eu/siacs/conversations/generator/MessageGenerator.java |  3 
src/main/java/eu/siacs/conversations/ui/ConversationFragment.java    | 58 
src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java  |  1 
src/main/res/layout/fragment_conversation.xml                        | 15 
src/main/res/layout/message_sent.xml                                 | 52 
7 files changed, 113 insertions(+), 38 deletions(-)

Detailed changes

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

@@ -148,6 +148,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
     private String mFirstMamReference = null;
     protected int mCurrentTab = -1;
     protected ConversationPagerAdapter pagerAdapter = new ConversationPagerAdapter();
+    protected Element thread = null;
 
     public Conversation(final String name, final Account account, final Jid contactJid,
                         final int mode) {
@@ -628,6 +629,14 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
         this.draftMessage = draftMessage;
     }
 
+    public Element getThread() {
+        return this.thread;
+    }
+
+    public void setThread(Element thread) {
+        this.thread = thread;
+    }
+
     public boolean isRead() {
         synchronized (this.messages) {
             for(final Message message : Lists.reverse(this.messages)) {

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

@@ -430,6 +430,11 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
         return null;
     }
 
+    public void setThread(Element thread) {
+        payloads.removeIf(el -> el.getName().equals("thread") && el.getNamespace().equals("jabber:client"));
+        addPayload(thread);
+    }
+
     public void setMucUser(MucOptions.User user) {
         this.user = new WeakReference<>(user);
     }
@@ -919,9 +924,15 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
     }
 
     public void addPayload(Element el) {
+        if (el == null) return;
+
         this.payloads.add(el);
     }
 
+    public List<Element> getPayloads() {
+       return new ArrayList<>(this.payloads);
+    }
+
     public Element getHtml() {
         if (this.payloads == null) return null;
 
@@ -932,7 +943,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
         }
 
         return null;
-    }
+   }
 
     public List<Element> getCommands() {
         if (this.payloads == null) return null;

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

@@ -62,6 +62,9 @@ public class MessageGenerator extends AbstractGenerator {
         if (message.edited()) {
             packet.addChild("replace", "urn:xmpp:message-correct:0").setAttribute("id", message.getEditedIdWireFormat());
         }
+        for (Element el : message.getPayloads()) {
+            packet.addChild(el);
+        }
         return packet;
     }
 

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

@@ -870,6 +870,7 @@ public class ConversationFragment extends XmppFragment
         final Message message;
         if (conversation.getCorrectingMessage() == null) {
             message = new Message(conversation, body, conversation.getNextEncryption());
+            message.setThread(conversation.getThread());
             Message.configurePrivateMessage(message);
         } else {
             message = conversation.getCorrectingMessage();
@@ -953,6 +954,8 @@ public class ConversationFragment extends XmppFragment
             this.binding.textinput.setHint(UIHelper.getMessageHint(getActivity(), conversation));
             getActivity().invalidateOptionsMenu();
         }
+
+        binding.messagesView.post(this::updateThreadFromLastMessage);
     }
 
     public void setupIme() {
@@ -1248,6 +1251,8 @@ public class ConversationFragment extends XmppFragment
                     new EditMessageActionModeCallback(this.binding.textinput));
         }
 
+        binding.threadIdenticon.setOnClickListener(v -> newThread());
+
         return binding.getRoot();
     }
 
@@ -1275,9 +1280,23 @@ public class ConversationFragment extends XmppFragment
     }
 
     private void quoteMessage(Message message) {
+        setThread(message.getThread());
         quoteText(MessageUtils.prepareQuote(message));
     }
 
+    private void setThread(Element thread) {
+        this.conversation.setThread(thread);
+        binding.threadIdenticon.setAlpha(0f);
+        if (thread != null) {
+            final String threadId = thread.getContent();
+            if (threadId != null) {
+                binding.threadIdenticon.setAlpha(1f);
+                binding.threadIdenticon.setColor(UIHelper.getColorForName(threadId));
+                binding.threadIdenticon.setHash(UIHelper.identiconHash(threadId));
+            }
+        }
+    }
+
     @Override
     public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
         // This should cancel any remaining click events that would otherwise trigger links
@@ -2116,7 +2135,29 @@ public class ConversationFragment extends XmppFragment
         }
     }
 
+    private void newThread() {
+        Element thread = new Element("thread", "jabber:client");
+        thread.setContent(UUID.randomUUID().toString());
+        setThread(thread);
+    }
+
+    private void updateThreadFromLastMessage() {
+        if (activity != null && this.conversation != null && TextUtils.isEmpty(binding.textinput.getText())) {
+            Message message = getLastVisibleMessage();
+            if (message == null) {
+                newThread();
+            } else {
+                setThread(message.getThread());
+            }
+        }
+    }
+
     private String getLastVisibleMessageUuid() {
+        Message message =  getLastVisibleMessage();
+        return message == null ? null : message.getUuid();
+    }
+
+    private Message getLastVisibleMessage() {
         if (binding == null) {
             return null;
         }
@@ -2140,7 +2181,7 @@ public class ConversationFragment extends XmppFragment
                     while (message.next() != null && message.next().wasMergedIntoPrevious()) {
                         message = message.next();
                     }
-                    return message.getUuid();
+                    return message;
                 }
             }
         }
@@ -3684,13 +3725,7 @@ public class ConversationFragment extends XmppFragment
 
     @Override
     public void onContactPictureClicked(Message message) {
-        String fingerprint;
-        if (message.getEncryption() == Message.ENCRYPTION_PGP
-                || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
-            fingerprint = "pgp";
-        } else {
-            fingerprint = message.getFingerprint();
-        }
+        setThread(message.getThread());
         final boolean received = message.getStatus() <= Message.STATUS_RECEIVED;
         if (received) {
             if (message.getConversation() instanceof Conversation
@@ -3724,15 +3759,8 @@ public class ConversationFragment extends XmppFragment
                                 .show();
                     }
                 }
-                return;
-            } else {
-                if (!message.getContact().isSelf()) {
-                    activity.switchToContactDetails(message.getContact(), fingerprint);
-                    return;
-                }
             }
         }
-        activity.switchToAccount(message.getConversation().getAccount(), fingerprint);
     }
 
     private Activity requireActivity() {

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

@@ -719,6 +719,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
                     viewHolder.subject = view.findViewById(R.id.message_subject);
                     viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
                     viewHolder.audioPlayer = view.findViewById(R.id.audio_player);
+                    viewHolder.thread_identicon = view.findViewById(R.id.thread_identicon);
                     break;
                 case RECEIVED:
                     view = activity.getLayoutInflater().inflate(R.layout.message_received, parent, false);

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

@@ -60,10 +60,10 @@
                     <LinearLayout
                         android:layout_width="wrap_content"
                         android:layout_height="wrap_content"
-                        android:layout_alignParentStart="true"
-                        android:layout_alignParentLeft="true"
                         android:layout_toStartOf="@+id/textSendButton"
                         android:layout_toLeftOf="@+id/textSendButton"
+                        android:layout_toEndOf="@+id/thread_identicon"
+                        android:layout_toRightOf="@+id/thread_identicon"
                         android:orientation="vertical">
     
                         <TextView
@@ -109,6 +109,17 @@
 
                     </LinearLayout>
 
+                    <com.lelloman.identicon.view.GithubIdenticonView
+                        android:id="@+id/thread_identicon"
+                        android:alpha="0"
+                        android:layout_width="24dp"
+                        android:layout_height="24dp"
+                        android:layout_marginLeft="8dp"
+                        android:layout_alignParentStart="true"
+                        android:layout_alignParentLeft="true"
+                        android:layout_centerVertical="true"
+                        android:contentDescription="Thread Marker" />
+
                     <ImageButton
                         android:id="@+id/textSendButton"
                         android:layout_width="48dp"

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

@@ -10,29 +10,33 @@
     android:paddingRight="8dp"
     android:paddingBottom="3dp">
 
-
-    <LinearLayout
+    <RelativeLayout
         android:id="@+id/message_photo_box"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
+        android:layout_width="48dp"
+        android:layout_height="48dp"
         android:layout_alignParentEnd="true"
         android:layout_alignParentRight="true"
-        android:layout_alignParentBottom="true"
-        android:orientation="vertical">
-
-        <com.makeramen.roundedimageview.RoundedImageView
-            android:id="@+id/message_photo"
-            android:layout_width="48dp"
-            android:layout_height="48dp"
-            android:scaleType="fitXY"
-            app:riv_corner_radius="2dp" />
-
-        <View
-            android:id="@+id/placeholder"
-            android:layout_width="48dp"
-            android:layout_height="3dp" />
-    </LinearLayout>
+        android:layout_alignParentBottom="true">
 
+        <LinearLayout
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:orientation="vertical">
+
+            <com.makeramen.roundedimageview.RoundedImageView
+                android:id="@+id/message_photo"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:scaleType="fitXY"
+                app:riv_corner_radius="2dp" />
+
+            <View
+                android:id="@+id/placeholder"
+                android:layout_width="match_parent"
+                android:layout_height="3dp" />
+        </LinearLayout>
+
+    </RelativeLayout>
 
     <LinearLayout
         android:id="@+id/message_box"
@@ -44,7 +48,6 @@
         android:paddingTop="5dp"
         android:paddingBottom="5dp"
         android:layout_toLeftOf="@+id/message_photo_box"
-        android:translationY="-2dp"
         android:elevation="3dp"
         android:background="@drawable/message_bubble_sent"
         android:longClickable="true"
@@ -93,6 +96,15 @@
                     android:text="@string/sending"
                     android:textAppearance="@style/TextAppearance.Conversations.Caption" />
 
+                <com.lelloman.identicon.view.GithubIdenticonView
+                    android:id="@+id/thread_identicon"
+                    android:visibility="gone"
+                    android:layout_width="9dp"
+                    android:layout_height="9dp"
+                    android:layout_gravity="center_vertical"
+                    android:layout_marginRight="4sp"
+                    android:layout_marginBottom="-1dp" />
+
                 <ImageView
                     android:id="@+id/security_indicator"
                     android:layout_width="?attr/TextSizeCaption"