Initial display for link previews

Stephen Paul Weber created

Change summary

src/cheogram/java/com/cheogram/android/Util.java                    | 20 
src/cheogram/res/drawable/background_link_description.xml           |  5 
src/cheogram/res/layout/link_description.xml                        | 38 
src/main/java/eu/siacs/conversations/entities/Message.java          | 13 
src/main/java/eu/siacs/conversations/parser/MessageParser.java      |  3 
src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java | 31 
src/main/res/layout/item_message_content.xml                        |  8 
7 files changed, 115 insertions(+), 3 deletions(-)

Detailed changes

src/cheogram/java/com/cheogram/android/Util.java 🔗

@@ -7,7 +7,11 @@ import android.widget.ListView;
 import android.widget.ListAdapter;
 
 public class Util {
-	public static void justifyListViewHeightBasedOnChildren (ListView listView) {
+	public static void justifyListViewHeightBasedOnChildren(ListView listView) {
+		justifyListViewHeightBasedOnChildren(listView, 0, false);
+	}
+
+	public static void justifyListViewHeightBasedOnChildren(ListView listView, int offset, boolean andWidth) {
 		ListAdapter adapter = listView.getAdapter();
 
 		if (adapter == null) {
@@ -15,16 +19,26 @@ public class Util {
 		}
 		ViewGroup vg = listView;
 		int totalHeight = 0;
-		final int width = listView.getWidth() > 0 ? listView.getWidth() : listView.getContext().getResources().getDisplayMetrics().widthPixels;
+		int maxWidth = 0;
+		final var displayWidth = listView.getContext().getResources().getDisplayMetrics().widthPixels;
+		final int width = !andWidth && listView.getWidth() > 0 ? listView.getWidth() : (displayWidth - offset);
 		final int widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST);
 		for (int i = 0; i < adapter.getCount(); i++) {
 			View listItem = adapter.getView(i, null, vg);
 			listItem.measure(widthSpec, 0);
 			totalHeight += listItem.getMeasuredHeight();
+			maxWidth = Math.max(maxWidth, listItem.getMeasuredWidth());
 		}
 
 		ViewGroup.LayoutParams par = listView.getLayoutParams();
-		par.height = totalHeight + (listView.getDividerHeight() * (adapter.getCount() - 1));
+		par.height = totalHeight + (listView.getDividerHeight() * Math.max(0, adapter.getCount() - 1));
+		if (andWidth) {
+			if (maxWidth <= (displayWidth - offset) && maxWidth > offset*2) {
+				par.width = maxWidth;
+			} else {
+				par.width = ViewGroup.LayoutParams.MATCH_PARENT;
+			}
+		}
 		listView.setLayoutParams(par);
 		listView.requestLayout();
 	}
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <solid android:color="?colorSurfaceContainerHighest"/>
+    <corners android:radius="@dimen/bubble_radius"/>
+</shape>
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:paddingHorizontal="10dp"
+        android:paddingVertical="4dp"
+        android:background="@drawable/background_message_bubble"
+        android:orientation="vertical">
+
+        <TextView
+            android:id="@+id/title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:maxLines="1"
+            android:textColor="?colorPrimary"
+            android:textAppearance="?textAppearanceTitleSmall" />
+
+        <TextView
+            android:id="@+id/url"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:maxLines="1"
+            android:textColor="?colorOnSurface"
+            android:textAppearance="?textAppearanceLabelSmall" />
+
+        <TextView
+            android:id="@+id/description"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textColor="?colorOnSurface"
+            android:textAppearance="?textAppearanceBodyMedium" />
+
+    </LinearLayout>
+
+</layout>

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

@@ -1323,6 +1323,19 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
         return null;
     }
 
+    public List<Element> getLinkDescriptions() {
+        final ArrayList<Element> result = new ArrayList<>();
+        if (this.payloads == null) return result;
+
+        for (Element el : this.payloads) {
+            if (el.getName().equals("Description") && el.getNamespace().equals("http://www.w3.org/1999/02/22-rdf-syntax-ns#")) {
+                result.add(el);
+            }
+        }
+
+        return result;
+    }
+
     public String getMimeType() {
         String extension;
         if (relativeFilePath != null) {

src/main/java/eu/siacs/conversations/parser/MessageParser.java 🔗

@@ -777,6 +777,9 @@ public class MessageParser extends AbstractParser implements Consumer<im.convers
                 if (el.getName().equals("attention") && el.getNamespace() != null && el.getNamespace().equals("urn:xmpp:attention:0")) {
                     message.addPayload(el);
                 }
+                if (el.getName().equals("Description") && el.getNamespace() != null && el.getNamespace().equals("http://www.w3.org/1999/02/22-rdf-syntax-ns#")) {
+                    message.addPayload(el);
+                }
             }
             if (conversationMultiMode) {
                 message.setMucUser(conversation.getMucOptions().findUserByFullJid(counterpart));

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

@@ -28,6 +28,7 @@ import android.util.DisplayMetrics;
 import android.util.LruCache;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.Gravity;
+import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
@@ -50,6 +51,7 @@ import androidx.core.app.ActivityCompat;
 import androidx.core.content.ContextCompat;
 import androidx.core.content.res.ResourcesCompat;
 import androidx.core.widget.ImageViewCompat;
+import androidx.databinding.DataBindingUtil;
 
 import com.google.android.material.imageview.ShapeableImageView;
 import com.google.android.material.shape.CornerFamily;
@@ -58,6 +60,7 @@ import com.google.android.material.shape.ShapeAppearanceModel;
 import com.cheogram.android.BobTransfer;
 import com.cheogram.android.MessageTextActionModeCallback;
 import com.cheogram.android.SwipeDetector;
+import com.cheogram.android.Util;
 import com.cheogram.android.WebxdcPage;
 import com.cheogram.android.WebxdcUpdate;
 
@@ -90,6 +93,7 @@ import eu.siacs.conversations.AppSettings;
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
+import eu.siacs.conversations.databinding.LinkDescriptionBinding;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Contact;
 import eu.siacs.conversations.entities.Conversation;
@@ -110,6 +114,7 @@ import eu.siacs.conversations.ui.ConversationsActivity;
 import eu.siacs.conversations.ui.XmppActivity;
 import eu.siacs.conversations.ui.service.AudioPlayer;
 import eu.siacs.conversations.ui.text.DividerSpan;
+import eu.siacs.conversations.ui.text.FixedURLSpan;
 import eu.siacs.conversations.ui.text.QuoteSpan;
 import eu.siacs.conversations.ui.util.Attachment;
 import eu.siacs.conversations.ui.util.AvatarWorkerTask;
@@ -1117,6 +1122,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
                     viewHolder.inReplyToQuote = view.findViewById(R.id.in_reply_to_quote);
                     viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
                     viewHolder.audioPlayer = view.findViewById(R.id.audio_player);
+                    viewHolder.link_descriptions = view.findViewById(R.id.link_descriptions);
                     viewHolder.thread_identicon = view.findViewById(R.id.thread_identicon);
                     break;
                 case RECEIVED:
@@ -1139,6 +1145,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
                     viewHolder.encryption = view.findViewById(R.id.message_encryption);
                     viewHolder.audioPlayer = view.findViewById(R.id.audio_player);
                     viewHolder.commands_list = view.findViewById(R.id.commands_list);
+                    viewHolder.link_descriptions = view.findViewById(R.id.link_descriptions);
                     viewHolder.thread_identicon = view.findViewById(R.id.thread_identicon);
                     break;
                 case STATUS:
@@ -1152,6 +1159,16 @@ public class MessageAdapter extends ArrayAdapter<Message> {
                 default:
                     throw new AssertionError("Unknown view type");
             }
+            if (viewHolder.link_descriptions != null) {
+                viewHolder.link_descriptions.setOnItemClickListener((adapter, v, pos, id) -> {
+                    final var desc = (Element) adapter.getItemAtPosition(pos);
+                    var url = desc.findChildContent("url", "https://ogp.me/ns#");
+                    // should we prefer about? Maybe, it's the real original link, but it's not what we show the user
+                    if (url == null || url.length() < 1) url = desc.getAttribute("{http://www.w3.org/1999/02/22-rdf-syntax-ns#}about");
+                    if (url == null || url.length() < 1) return;
+                    new FixedURLSpan(url).onClick(v);
+                });
+            }
             view.setTag(viewHolder);
         } else {
             viewHolder = (ViewHolder) view.getTag();
@@ -1523,6 +1540,19 @@ public class MessageAdapter extends ArrayAdapter<Message> {
                 viewHolder.inReplyToQuote.setOnClickListener((v) -> mConversationFragment.jumpTo(message.getInReplyTo()));
                 setTextColor(viewHolder.inReplyTo, bubbleColor);
             }
+
+            final var descriptions = message.getLinkDescriptions();
+            viewHolder.link_descriptions.setAdapter(new ArrayAdapter<>(activity, 0, descriptions) {
+                @Override
+                public View getView(int position, View view, @NonNull ViewGroup parent) {
+                    final LinkDescriptionBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.link_description, parent, false);
+                    binding.title.setText(getItem(position).findChildContent("title", "https://ogp.me/ns#"));
+                    binding.description.setText(getItem(position).findChildContent("description", "https://ogp.me/ns#"));
+                    binding.url.setText(getItem(position).findChildContent("url", "https://ogp.me/ns#"));
+                    return binding.getRoot();
+                }
+            });
+            Util.justifyListViewHeightBasedOnChildren(viewHolder.link_descriptions, (int)(metrics.density * 100), true);
         }
 
         displayStatus(viewHolder, message, type, bubbleColor);
@@ -1750,6 +1780,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
         protected TextView status_message;
         protected TextView encryption;
         protected ListView commands_list;
+        protected ListView link_descriptions;
         protected GithubIdenticonView thread_identicon;
     }
 

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

@@ -89,6 +89,14 @@
             android:divider="@android:color/transparent"
             android:dividerHeight="0dp"></ListView>
 
+        <ListView
+            android:id="@+id/link_descriptions"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginHorizontal="10dp"
+            android:divider="@android:color/transparent"
+            android:dividerHeight="4dp"></ListView>
+
         <RelativeLayout
             android:id="@+id/audio_player"
             android:layout_width="@dimen/audio_player_width"