浏览代码

add openGraph link previews

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
Marcel Hibbe 2 年之前
父节点
当前提交
9bc42334d4
共有 19 个文件被更改,包括 1119 次插入1 次删除
  1. 207 0
      app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLinkPreviewMessageViewHolder.kt
  2. 122 0
      app/src/main/java/com/nextcloud/talk/adapters/messages/LinkPreview.kt
  3. 168 0
      app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLinkPreviewMessageViewHolder.kt
  4. 6 0
      app/src/main/java/com/nextcloud/talk/api/NcApi.java
  5. 15 0
      app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt
  6. 3 1
      app/src/main/java/com/nextcloud/talk/models/json/capabilities/Capabilities.kt
  7. 43 0
      app/src/main/java/com/nextcloud/talk/models/json/capabilities/CoreCapability.kt
  8. 34 0
      app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.kt
  9. 38 0
      app/src/main/java/com/nextcloud/talk/models/json/opengraph/OpenGraphOCS.kt
  10. 44 0
      app/src/main/java/com/nextcloud/talk/models/json/opengraph/OpenGraphObject.kt
  11. 35 0
      app/src/main/java/com/nextcloud/talk/models/json/opengraph/OpenGraphOverall.kt
  12. 35 0
      app/src/main/java/com/nextcloud/talk/models/json/opengraph/OpenGraphResponse.kt
  13. 42 0
      app/src/main/java/com/nextcloud/talk/models/json/opengraph/Reference.kt
  14. 44 0
      app/src/main/java/com/nextcloud/talk/models/json/opengraph/RichObject.kt
  15. 3 0
      app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java
  16. 10 0
      app/src/main/java/com/nextcloud/talk/utils/database/user/CapabilitiesUtilNew.kt
  17. 107 0
      app/src/main/res/layout/item_custom_incoming_link_preview_message.xml
  18. 96 0
      app/src/main/res/layout/item_custom_outcoming_link_preview_message.xml
  19. 67 0
      app/src/main/res/layout/reference_inside_message.xml

+ 207 - 0
app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLinkPreviewMessageViewHolder.kt

@@ -0,0 +1,207 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Mario Danic
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ * Copyright (C) 2017-2019 Mario Danic <mario@lovelyhq.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.adapters.messages
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.LayerDrawable
+import android.os.Build
+import android.text.TextUtils
+import android.view.View
+import androidx.core.content.ContextCompat
+import androidx.core.content.res.ResourcesCompat
+import autodagger.AutoInjector
+import coil.load
+import com.amulyakhare.textdrawable.TextDrawable
+import com.nextcloud.talk.R
+import com.nextcloud.talk.api.NcApi
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
+import com.nextcloud.talk.databinding.ItemCustomIncomingLinkPreviewMessageBinding
+import com.nextcloud.talk.models.json.chat.ChatMessage
+import com.nextcloud.talk.ui.theme.ViewThemeUtils
+import com.nextcloud.talk.utils.ApiUtils
+import com.nextcloud.talk.utils.DisplayUtils
+import com.nextcloud.talk.utils.preferences.AppPreferences
+import com.stfalcon.chatkit.messages.MessageHolders
+import javax.inject.Inject
+
+@AutoInjector(NextcloudTalkApplication::class)
+class IncomingLinkPreviewMessageViewHolder(incomingView: View, payload: Any) : MessageHolders
+.IncomingTextMessageViewHolder<ChatMessage>(incomingView, payload) {
+
+    private val binding: ItemCustomIncomingLinkPreviewMessageBinding =
+        ItemCustomIncomingLinkPreviewMessageBinding.bind(itemView)
+
+    @Inject
+    lateinit var context: Context
+
+    @Inject
+    lateinit var appPreferences: AppPreferences
+
+    @Inject
+    lateinit var viewThemeUtils: ViewThemeUtils
+
+    @Inject
+    lateinit var ncApi: NcApi
+
+    lateinit var message: ChatMessage
+
+    lateinit var reactionsInterface: ReactionsInterface
+
+    @SuppressLint("SetTextI18n")
+    override fun onBind(message: ChatMessage) {
+        super.onBind(message)
+        this.message = message
+        sharedApplication!!.componentApplication.inject(this)
+
+        setAvatarAndAuthorOnMessageItem(message)
+
+        colorizeMessageBubble(message)
+
+        itemView.isSelected = false
+
+        // parent message handling
+        setParentMessageDataOnMessageItem(message)
+
+        LinkPreview().showLink(
+            message,
+            ncApi,
+            binding.referenceInclude,
+            context
+        )
+
+        Reaction().showReactions(
+            message,
+            binding.reactions,
+            binding.messageTime.context,
+            false,
+            viewThemeUtils
+        )
+        binding.reactions.reactionsEmojiWrapper.setOnClickListener {
+            reactionsInterface.onClickReactions(message)
+        }
+        binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
+            reactionsInterface.onLongClickReactions(message)
+            true
+        }
+    }
+
+    private fun setAvatarAndAuthorOnMessageItem(message: ChatMessage) {
+        val author: String = message.actorDisplayName!!
+        if (!TextUtils.isEmpty(author)) {
+            binding.messageAuthor.text = author
+            binding.messageUserAvatar.setOnClickListener {
+                (payload as? MessagePayload)?.profileBottomSheet?.showFor(message.actorId!!, itemView.context)
+            }
+        } else {
+            binding.messageAuthor.setText(R.string.nc_nick_guest)
+        }
+
+        if (!message.isGrouped && !message.isOneToOneConversation) {
+            setAvatarOnMessage(message)
+        } else {
+            if (message.isOneToOneConversation) {
+                binding.messageUserAvatar.visibility = View.GONE
+            } else {
+                binding.messageUserAvatar.visibility = View.INVISIBLE
+            }
+            binding.messageAuthor.visibility = View.GONE
+        }
+    }
+
+    private fun setAvatarOnMessage(message: ChatMessage) {
+        binding.messageUserAvatar.visibility = View.VISIBLE
+        if (message.actorType == "guests") {
+            // do nothing, avatar is set
+        } else if (message.actorType == "bots" && message.actorId == "changelog") {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+                val layers = arrayOfNulls<Drawable>(2)
+                layers[0] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_background)
+                layers[1] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_foreground)
+                val layerDrawable = LayerDrawable(layers)
+                binding.messageUserAvatar.setImageDrawable(DisplayUtils.getRoundedDrawable(layerDrawable))
+            } else {
+                binding.messageUserAvatar.setImageResource(R.mipmap.ic_launcher)
+            }
+        } else if (message.actorType == "bots") {
+            val drawable = TextDrawable.builder()
+                .beginConfig()
+                .bold()
+                .endConfig()
+                .buildRound(
+                    ">",
+                    ResourcesCompat.getColor(context.resources, R.color.black, null)
+                )
+            binding.messageUserAvatar.visibility = View.VISIBLE
+            binding.messageUserAvatar.setImageDrawable(drawable)
+        }
+    }
+
+    private fun colorizeMessageBubble(message: ChatMessage) {
+        viewThemeUtils.talk.themeIncomingMessageBubble(bubble, message.isGrouped, message.isDeleted)
+    }
+
+    private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
+        if (!message.isDeleted && message.parentMessage != null) {
+            val parentChatMessage = message.parentMessage
+            parentChatMessage!!.activeUser = message.activeUser
+            parentChatMessage.imageUrl?.let {
+                binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
+                binding.messageQuote.quotedMessageImage.load(it) {
+                    addHeader(
+                        "Authorization",
+                        ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)
+                    )
+                }
+            } ?: run {
+                binding.messageQuote.quotedMessageImage.visibility = View.GONE
+            }
+            binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
+                ?: context.getText(R.string.nc_nick_guest)
+            binding.messageQuote.quotedMessage.text = parentChatMessage.text
+
+            binding.messageQuote.quotedMessageAuthor
+                .setTextColor(ContextCompat.getColor(context, R.color.textColorMaxContrast))
+
+            if (parentChatMessage.actorId?.equals(message.activeUser!!.userId) == true) {
+                viewThemeUtils.platform.colorPrimaryView(binding.messageQuote.quoteColoredView)
+            } else {
+                binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.textColorMaxContrast)
+            }
+
+            binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
+        } else {
+            binding.messageQuote.quotedChatMessageView.visibility = View.GONE
+        }
+    }
+
+    fun assignReactionInterface(reactionsInterface: ReactionsInterface) {
+        this.reactionsInterface = reactionsInterface
+    }
+
+    companion object {
+        private val TAG = IncomingLinkPreviewMessageViewHolder::class.java.simpleName
+    }
+}

+ 122 - 0
app/src/main/java/com/nextcloud/talk/adapters/messages/LinkPreview.kt

@@ -0,0 +1,122 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe (dev@mhibbe.de)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Parts related to account import were either copied from or inspired by the great work done by David Luhmer at:
+ * https://github.com/nextcloud/ownCloud-Account-Importer
+ */
+
+package com.nextcloud.talk.adapters.messages
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.util.Log
+import android.view.View
+import com.facebook.drawee.backends.pipeline.Fresco
+import com.facebook.drawee.interfaces.DraweeController
+import com.nextcloud.talk.api.NcApi
+import com.nextcloud.talk.databinding.ReferenceInsideMessageBinding
+import com.nextcloud.talk.models.json.chat.ChatMessage
+import com.nextcloud.talk.models.json.opengraph.OpenGraphOverall
+import com.nextcloud.talk.utils.ApiUtils
+import com.nextcloud.talk.utils.DisplayUtils
+import io.reactivex.Observer
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.disposables.Disposable
+import io.reactivex.schedulers.Schedulers
+
+class LinkPreview {
+
+    fun showLink(
+        message: ChatMessage,
+        ncApi: NcApi,
+        binding: ReferenceInsideMessageBinding,
+        context: Context
+    ) {
+        if (!message.extractedUrlToPreview.isNullOrEmpty()) {
+            val credentials: String = ApiUtils.getCredentials(message.activeUser?.username, message.activeUser?.token)
+            val openGraphLink = ApiUtils.getUrlForOpenGraph(message.activeUser?.baseUrl)
+            ncApi.getOpenGraph(
+                credentials,
+                openGraphLink,
+                message.extractedUrlToPreview
+            )
+                .subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe(object : Observer<OpenGraphOverall> {
+                    override fun onSubscribe(d: Disposable) {
+                        // unused atm
+                    }
+
+                    override fun onNext(openGraphOverall: OpenGraphOverall) {
+                        val reference = openGraphOverall.ocs?.data?.references?.entries?.iterator()?.next()?.value
+
+                        if (reference != null) {
+                            val referenceName = reference.openGraphObject?.name
+                            if (!referenceName.isNullOrEmpty()) {
+                                binding.referenceName.visibility = View.VISIBLE
+                                binding.referenceName.text = referenceName
+                            } else {
+                                binding.referenceName.visibility = View.GONE
+                            }
+
+                            val referenceLink = reference.openGraphObject?.link
+                            if (!referenceLink.isNullOrEmpty()) {
+                                binding.referenceLink.visibility = View.VISIBLE
+                                binding.referenceLink.text = referenceLink
+                            } else {
+                                binding.referenceLink.visibility = View.GONE
+                            }
+
+                            val referenceThumbUrl = reference.openGraphObject?.thumb
+                            if (!referenceThumbUrl.isNullOrEmpty()) {
+                                binding.referenceThumbImage.visibility = View.VISIBLE
+                                val draweeController: DraweeController = Fresco.newDraweeControllerBuilder()
+                                    .setAutoPlayAnimations(true)
+                                    .setImageRequest(DisplayUtils.getImageRequestForUrl(referenceThumbUrl))
+                                    .build()
+                                binding.referenceThumbImage.controller =
+                                    draweeController
+                            } else {
+                                binding.referenceThumbImage.visibility = View.GONE
+                            }
+
+                            binding.referenceWrapper.setOnClickListener {
+                                val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(referenceLink))
+                                browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                                context.startActivity(browserIntent)
+                            }
+                        }
+                    }
+
+                    override fun onError(e: Throwable) {
+                        Log.e(TAG, "failed to get openGraph data", e)
+                    }
+
+                    override fun onComplete() {
+                        // unused atm
+                    }
+                })
+        }
+    }
+
+    companion object {
+        private val TAG = LinkPreview::class.java.simpleName
+    }
+}

+ 168 - 0
app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLinkPreviewMessageViewHolder.kt

@@ -0,0 +1,168 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Mario Danic
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ * Copyright (C) 2017-2019 Mario Danic <mario@lovelyhq.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.adapters.messages
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.PorterDuff
+import android.view.View
+import androidx.appcompat.content.res.AppCompatResources
+import autodagger.AutoInjector
+import coil.load
+import com.nextcloud.talk.R
+import com.nextcloud.talk.api.NcApi
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
+import com.nextcloud.talk.databinding.ItemCustomOutcomingLinkPreviewMessageBinding
+import com.nextcloud.talk.models.json.chat.ChatMessage
+import com.nextcloud.talk.models.json.chat.ReadStatus
+import com.nextcloud.talk.ui.theme.ViewThemeUtils
+import com.nextcloud.talk.utils.ApiUtils
+import com.nextcloud.talk.utils.preferences.AppPreferences
+import com.stfalcon.chatkit.messages.MessageHolders
+import javax.inject.Inject
+
+@AutoInjector(NextcloudTalkApplication::class)
+class OutcomingLinkPreviewMessageViewHolder(outcomingView: View, payload: Any) : MessageHolders
+.OutcomingTextMessageViewHolder<ChatMessage>(outcomingView, payload) {
+
+    private val binding: ItemCustomOutcomingLinkPreviewMessageBinding =
+        ItemCustomOutcomingLinkPreviewMessageBinding.bind(itemView)
+
+    @Inject
+    lateinit var context: Context
+
+    @Inject
+    lateinit var viewThemeUtils: ViewThemeUtils
+
+    @Inject
+    lateinit var appPreferences: AppPreferences
+
+    @Inject
+    lateinit var ncApi: NcApi
+
+    lateinit var message: ChatMessage
+
+    lateinit var reactionsInterface: ReactionsInterface
+
+    @SuppressLint("SetTextI18n")
+    override fun onBind(message: ChatMessage) {
+        super.onBind(message)
+        this.message = message
+        sharedApplication!!.componentApplication.inject(this)
+        val textColor = viewThemeUtils.getScheme(binding.messageTime.context).onSurfaceVariant
+        binding.messageTime.setTextColor(textColor)
+
+        colorizeMessageBubble(message)
+
+        itemView.isSelected = false
+
+        // parent message handling
+        setParentMessageDataOnMessageItem(message)
+
+        val readStatusDrawableInt = when (message.readStatus) {
+            ReadStatus.READ -> R.drawable.ic_check_all
+            ReadStatus.SENT -> R.drawable.ic_check
+            else -> null
+        }
+
+        val readStatusContentDescriptionString = when (message.readStatus) {
+            ReadStatus.READ -> context?.resources?.getString(R.string.nc_message_read)
+            ReadStatus.SENT -> context?.resources?.getString(R.string.nc_message_sent)
+            else -> null
+        }
+
+        readStatusDrawableInt?.let { drawableInt ->
+            AppCompatResources.getDrawable(context, drawableInt)?.let {
+                binding.checkMark.setImageDrawable(it)
+                binding.checkMark.setColorFilter(
+                    viewThemeUtils.getScheme(binding.checkMark.context).onSurfaceVariant, PorterDuff.Mode.SRC_ATOP
+                )
+            }
+        }
+
+        binding.checkMark.contentDescription = readStatusContentDescriptionString
+
+        LinkPreview().showLink(
+            message,
+            ncApi,
+            binding.referenceInclude,
+            context
+        )
+
+        Reaction().showReactions(
+            message,
+            binding.reactions,
+            binding.messageTime.context,
+            true,
+            viewThemeUtils
+        )
+        binding.reactions.reactionsEmojiWrapper.setOnClickListener {
+            reactionsInterface.onClickReactions(message)
+        }
+        binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
+            reactionsInterface.onLongClickReactions(message)
+            true
+        }
+    }
+
+    private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
+        if (!message.isDeleted && message.parentMessage != null) {
+            val parentChatMessage = message.parentMessage
+            parentChatMessage!!.activeUser = message.activeUser
+            parentChatMessage.imageUrl?.let {
+                binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
+                binding.messageQuote.quotedMessageImage.load(it) {
+                    addHeader(
+                        "Authorization",
+                        ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)
+                    )
+                }
+            } ?: run {
+                binding.messageQuote.quotedMessageImage.visibility = View.GONE
+            }
+            binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
+                ?: context.getText(R.string.nc_nick_guest)
+            binding.messageQuote.quotedMessage.text = parentChatMessage.text
+            viewThemeUtils.talk.colorOutgoingQuoteText(binding.messageQuote.quotedMessage)
+            viewThemeUtils.talk.colorOutgoingQuoteAuthorText(binding.messageQuote.quotedMessageAuthor)
+            viewThemeUtils.talk.colorOutgoingQuoteBackground(binding.messageQuote.quoteColoredView)
+
+            binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
+        } else {
+            binding.messageQuote.quotedChatMessageView.visibility = View.GONE
+        }
+    }
+
+    private fun colorizeMessageBubble(message: ChatMessage) {
+        viewThemeUtils.talk.themeOutgoingMessageBubble(bubble, message.isGrouped, message.isDeleted)
+    }
+
+    fun assignReactionInterface(reactionsInterface: ReactionsInterface) {
+        this.reactionsInterface = reactionsInterface
+    }
+
+    companion object {
+        private val TAG = OutcomingLinkPreviewMessageViewHolder::class.java.simpleName
+    }
+}

+ 6 - 0
app/src/main/java/com/nextcloud/talk/api/NcApi.java

@@ -35,6 +35,7 @@ import com.nextcloud.talk.models.json.generic.Status;
 import com.nextcloud.talk.models.json.hovercard.HoverCardOverall;
 import com.nextcloud.talk.models.json.mention.MentionOverall;
 import com.nextcloud.talk.models.json.notifications.NotificationOverall;
+import com.nextcloud.talk.models.json.opengraph.OpenGraphOverall;
 import com.nextcloud.talk.models.json.participants.AddParticipantOverall;
 import com.nextcloud.talk.models.json.participants.ParticipantsOverall;
 import com.nextcloud.talk.models.json.push.PushRegistrationOverall;
@@ -570,4 +571,9 @@ public interface NcApi {
     Observable<GenericOverall> setMessageExpiration(@Header("Authorization") String authorization,
                                      @Url String url,
                                      @Field("seconds") Integer seconds);
+
+    @GET
+    Observable<OpenGraphOverall> getOpenGraph(@Header("Authorization") String authorization,
+                                              @Url String url,
+                                              @Query("reference") String urlToFindPreviewFor);
 }

+ 15 - 0
app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt

@@ -106,6 +106,7 @@ import com.nextcloud.talk.R
 import com.nextcloud.talk.activities.CallActivity
 import com.nextcloud.talk.activities.MainActivity
 import com.nextcloud.talk.activities.TakePhotoActivity
+import com.nextcloud.talk.adapters.messages.IncomingLinkPreviewMessageViewHolder
 import com.nextcloud.talk.adapters.messages.IncomingLocationMessageViewHolder
 import com.nextcloud.talk.adapters.messages.IncomingPollMessageViewHolder
 import com.nextcloud.talk.adapters.messages.IncomingPreviewMessageViewHolder
@@ -115,6 +116,7 @@ import com.nextcloud.talk.adapters.messages.MagicOutcomingTextMessageViewHolder
 import com.nextcloud.talk.adapters.messages.MagicSystemMessageViewHolder
 import com.nextcloud.talk.adapters.messages.MagicUnreadNoticeMessageViewHolder
 import com.nextcloud.talk.adapters.messages.MessagePayload
+import com.nextcloud.talk.adapters.messages.OutcomingLinkPreviewMessageViewHolder
 import com.nextcloud.talk.adapters.messages.OutcomingLocationMessageViewHolder
 import com.nextcloud.talk.adapters.messages.OutcomingPollMessageViewHolder
 import com.nextcloud.talk.adapters.messages.OutcomingPreviewMessageViewHolder
@@ -587,6 +589,17 @@ class ChatController(args: Bundle) :
                 this
             )
 
+            messageHolders.registerContentType(
+                CONTENT_TYPE_LINK_PREVIEW,
+                IncomingLinkPreviewMessageViewHolder::class.java,
+                payload,
+                R.layout.item_custom_incoming_link_preview_message,
+                OutcomingLinkPreviewMessageViewHolder::class.java,
+                payload,
+                R.layout.item_custom_outcoming_link_preview_message,
+                this
+            )
+
             val senderId = if (!conversationUser.userId.equals("?")) {
                 "users/" + conversationUser.userId
             } else {
@@ -3162,6 +3175,7 @@ class ChatController(args: Bundle) :
             CONTENT_TYPE_LOCATION -> message.hasGeoLocation()
             CONTENT_TYPE_VOICE_MESSAGE -> message.isVoiceMessage
             CONTENT_TYPE_POLL -> message.isPoll()
+            CONTENT_TYPE_LINK_PREVIEW -> message.isLinkPreview()
             CONTENT_TYPE_SYSTEM_MESSAGE -> !TextUtils.isEmpty(message.systemMessage)
             CONTENT_TYPE_UNREAD_NOTICE_MESSAGE -> message.id == "-1"
             else -> false
@@ -3322,6 +3336,7 @@ class ChatController(args: Bundle) :
         private const val CONTENT_TYPE_LOCATION: Byte = 3
         private const val CONTENT_TYPE_VOICE_MESSAGE: Byte = 4
         private const val CONTENT_TYPE_POLL: Byte = 5
+        private const val CONTENT_TYPE_LINK_PREVIEW: Byte = 6
         private const val NEW_MESSAGES_POPUP_BUBBLE_DELAY: Long = 200
         private const val POP_CURRENT_CONTROLLER_DELAY: Long = 100
         private const val LOBBY_TIMER_DELAY: Long = 5000

+ 3 - 1
app/src/main/java/com/nextcloud/talk/models/json/capabilities/Capabilities.kt

@@ -29,6 +29,8 @@ import kotlinx.android.parcel.Parcelize
 @Parcelize
 @JsonObject
 data class Capabilities(
+    @JsonField(name = ["core"])
+    var coreCapability: CoreCapability?,
     @JsonField(name = ["spreed"])
     var spreedCapability: SpreedCapability?,
     @JsonField(name = ["notifications"])
@@ -43,5 +45,5 @@ data class Capabilities(
     var userStatusCapability: UserStatusCapability?
 ) : Parcelable {
     // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
-    constructor() : this(null, null, null, null, null, null)
+    constructor() : this(null, null, null, null, null, null, null)
 }

+ 43 - 0
app/src/main/java/com/nextcloud/talk/models/json/capabilities/CoreCapability.kt

@@ -0,0 +1,43 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.models.json.capabilities
+
+import android.os.Parcelable
+import com.bluelinelabs.logansquare.annotation.JsonField
+import com.bluelinelabs.logansquare.annotation.JsonObject
+import kotlinx.android.parcel.Parcelize
+import kotlinx.serialization.Serializable
+
+@Parcelize
+@JsonObject
+@Serializable
+data class CoreCapability(
+    @JsonField(name = ["pollinterval"])
+    var pollInterval: Int?,
+    @JsonField(name = ["webdav-root"])
+    var webdavRoot: String?,
+    @JsonField(name = ["reference-api"])
+    var referenceApi: String?,
+    @JsonField(name = ["reference-regex"])
+    var referenceRegex: String?
+) : Parcelable {
+    // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
+    constructor() : this(null, null, null, null)
+}

+ 34 - 0
app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.kt

@@ -37,6 +37,7 @@ import com.nextcloud.talk.data.user.model.User
 import com.nextcloud.talk.models.json.chat.ChatUtils.Companion.getParsedMessage
 import com.nextcloud.talk.models.json.converters.EnumSystemMessageTypeConverter
 import com.nextcloud.talk.utils.ApiUtils
+import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew
 import com.stfalcon.chatkit.commons.models.IUser
 import com.stfalcon.chatkit.commons.models.MessageContentType
 import kotlinx.android.parcel.Parcelize
@@ -126,8 +127,11 @@ data class ChatMessage(
     var voiceMessagePlayedSeconds: Int = 0,
 
     var voiceMessageDownloadProgress: Int = 0,
+
 ) : Parcelable, MessageContentType, MessageContentType.Image {
 
+    var extractedUrlToPreview: String? = null
+
     // messageTypesToIgnore is weird. must be deleted by refactoring!!!
     @JsonIgnore
     var messageTypesToIgnore = Arrays.asList(
@@ -174,6 +178,33 @@ data class ChatMessage(
         return false
     }
 
+    @Suppress("ReturnCount")
+    fun isLinkPreview(): Boolean {
+        if (CapabilitiesUtilNew.isLinkPreviewAvailable(activeUser!!)) {
+            val regexStringFromServer = activeUser?.capabilities?.coreCapability?.referenceRegex
+
+            val regexFromServer = regexStringFromServer?.toRegex(setOf(RegexOption.MULTILINE, RegexOption.IGNORE_CASE))
+            val regexDefault = REGEX_STRING_DEFAULT.toRegex(setOf(RegexOption.MULTILINE, RegexOption.IGNORE_CASE))
+
+            val messageCharSequence: CharSequence = StringBuffer(message!!)
+
+            if (regexFromServer != null) {
+                val foundLinkInServerRegex = regexFromServer.containsMatchIn(messageCharSequence)
+                if (foundLinkInServerRegex) {
+                    extractedUrlToPreview = regexFromServer.find(messageCharSequence)?.groups?.get(0)?.value?.trim()
+                    return true
+                }
+            }
+
+            val foundLinkInDefaultRegex = regexDefault.containsMatchIn(messageCharSequence)
+            if (foundLinkInDefaultRegex) {
+                extractedUrlToPreview = regexDefault.find(messageCharSequence)?.groups?.get(0)?.value?.trim()
+                return true
+            }
+        }
+        return false
+    }
+
     @Suppress("Detekt.NestedBlockDepth")
     override fun getImageUrl(): String? {
         if (messageParameters != null && messageParameters!!.size > 0) {
@@ -492,5 +523,8 @@ data class ChatMessage(
     companion object {
         private const val TAG = "ChatMessage"
         private const val MILLIES: Long = 1000L
+
+        private const val REGEX_STRING_DEFAULT =
+            """(\s|\n|^)(https?:\/\/)((?:[-A-Z0-9+_]+\.)+[-A-Z]+(?:\/[-A-Z0-9+&@#%?=~_|!:,.;()]*)*)(\s|\n|$)"""
     }
 }

+ 38 - 0
app/src/main/java/com/nextcloud/talk/models/json/opengraph/OpenGraphOCS.kt

@@ -0,0 +1,38 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.models.json.opengraph
+
+import android.os.Parcelable
+import com.bluelinelabs.logansquare.annotation.JsonField
+import com.bluelinelabs.logansquare.annotation.JsonObject
+import com.nextcloud.talk.models.json.generic.GenericMeta
+import kotlinx.android.parcel.Parcelize
+
+@Parcelize
+@JsonObject
+data class OpenGraphOCS(
+    @JsonField(name = ["meta"])
+    var meta: GenericMeta?,
+    @JsonField(name = ["data"])
+    var data: OpenGraphResponse?
+) : Parcelable {
+    // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
+    constructor() : this(null, null)
+}

+ 44 - 0
app/src/main/java/com/nextcloud/talk/models/json/opengraph/OpenGraphObject.kt

@@ -0,0 +1,44 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.models.json.opengraph
+
+import android.os.Parcelable
+import com.bluelinelabs.logansquare.annotation.JsonField
+import com.bluelinelabs.logansquare.annotation.JsonObject
+import kotlinx.android.parcel.Parcelize
+
+@Parcelize
+@JsonObject
+data class OpenGraphObject(
+    @JsonField(name = ["id"])
+    var id: String,
+    @JsonField(name = ["name"])
+    var name: String,
+    @JsonField(name = ["description"])
+    var description: String? = null,
+    @JsonField(name = ["thumb"])
+    var thumb: String? = null,
+    @JsonField(name = ["link"])
+    var link: String? = null,
+
+) : Parcelable {
+    // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
+    constructor() : this("", "", null, null)
+}

+ 35 - 0
app/src/main/java/com/nextcloud/talk/models/json/opengraph/OpenGraphOverall.kt

@@ -0,0 +1,35 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.models.json.opengraph
+
+import android.os.Parcelable
+import com.bluelinelabs.logansquare.annotation.JsonField
+import com.bluelinelabs.logansquare.annotation.JsonObject
+import kotlinx.android.parcel.Parcelize
+
+@Parcelize
+@JsonObject
+data class OpenGraphOverall(
+    @JsonField(name = ["ocs"])
+    var ocs: OpenGraphOCS? = null
+) : Parcelable {
+    // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
+    constructor() : this(null)
+}

+ 35 - 0
app/src/main/java/com/nextcloud/talk/models/json/opengraph/OpenGraphResponse.kt

@@ -0,0 +1,35 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.models.json.opengraph
+
+import android.os.Parcelable
+import com.bluelinelabs.logansquare.annotation.JsonField
+import com.bluelinelabs.logansquare.annotation.JsonObject
+import kotlinx.android.parcel.Parcelize
+
+@Parcelize
+@JsonObject
+data class OpenGraphResponse(
+    @JsonField(name = ["references"])
+    var references: HashMap<String, Reference>?
+) : Parcelable {
+    // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
+    constructor() : this(null)
+}

+ 42 - 0
app/src/main/java/com/nextcloud/talk/models/json/opengraph/Reference.kt

@@ -0,0 +1,42 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.models.json.opengraph
+
+import android.os.Parcelable
+import com.bluelinelabs.logansquare.annotation.JsonField
+import com.bluelinelabs.logansquare.annotation.JsonObject
+import kotlinx.android.parcel.Parcelize
+
+@Parcelize
+@JsonObject
+data class Reference(
+    @JsonField(name = ["richObjectType"])
+    var richObjectType: String? = null,
+    @JsonField(name = ["richObject"])
+    var richObject: RichObject? = null,
+    @JsonField(name = ["openGraphObject"])
+    var openGraphObject: OpenGraphObject? = null,
+    @JsonField(name = ["accessible"])
+    var accessible: Boolean,
+
+) : Parcelable {
+    // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
+    constructor() : this(null, null, null, false)
+}

+ 44 - 0
app/src/main/java/com/nextcloud/talk/models/json/opengraph/RichObject.kt

@@ -0,0 +1,44 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.models.json.opengraph
+
+import android.os.Parcelable
+import com.bluelinelabs.logansquare.annotation.JsonField
+import com.bluelinelabs.logansquare.annotation.JsonObject
+import kotlinx.android.parcel.Parcelize
+
+@Parcelize
+@JsonObject
+data class RichObject(
+    @JsonField(name = ["id"])
+    var id: String,
+    @JsonField(name = ["name"])
+    var name: String,
+    @JsonField(name = ["description"])
+    var description: String? = null,
+    @JsonField(name = ["thumb"])
+    var thumb: String? = null,
+    @JsonField(name = ["link"])
+    var link: String? = null,
+
+) : Parcelable {
+    // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
+    constructor() : this("", "", null, null)
+}

+ 3 - 0
app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java

@@ -491,4 +491,7 @@ public class ApiUtils {
         return getUrlForRoom(version, baseUrl, token) + "/message-expiration";
     }
 
+    public static String getUrlForOpenGraph(String baseUrl) {
+        return baseUrl + ocsApiVersion + "/references/resolve";
+    }
 }

+ 10 - 0
app/src/main/java/com/nextcloud/talk/utils/database/user/CapabilitiesUtilNew.kt

@@ -154,5 +154,15 @@ object CapabilitiesUtilNew {
         return hasSpreedFeatureCapability(user, "unified-search")
     }
 
+    @JvmStatic
+    fun isLinkPreviewAvailable(user: User): Boolean {
+        if (user.capabilities?.coreCapability?.referenceApi != null &&
+            user.capabilities?.coreCapability?.referenceApi == "true"
+        ) {
+            return true
+        }
+        return false
+    }
+
     const val DEFAULT_CHAT_SIZE = 1000
 }

+ 107 - 0
app/src/main/res/layout/item_custom_incoming_link_preview_message.xml

@@ -0,0 +1,107 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Nextcloud Talk application
+  ~
+  ~ @author Mario Danic
+  ~ @author Andy Scherzinger
+  ~ @author Marcel Hibbe
+  ~ Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+  ~ Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
+  ~ Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation, either version 3 of the License, or
+  ~ at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+  -->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_marginLeft="16dp"
+    android:layout_marginTop="2dp"
+    android:layout_marginRight="16dp"
+    android:layout_marginBottom="2dp">
+
+    <com.facebook.drawee.view.SimpleDraweeView
+        android:id="@id/messageUserAvatar"
+        android:layout_width="24dp"
+        android:layout_height="24dp"
+        android:layout_alignParentTop="true"
+        android:layout_marginEnd="8dp"
+        app:roundAsCircle="true" />
+
+    <com.google.android.flexbox.FlexboxLayout
+        android:id="@id/bubble"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="@dimen/message_incoming_bubble_margin_right"
+        android:layout_toEndOf="@id/messageUserAvatar"
+        android:orientation="vertical"
+        app:alignContent="stretch"
+        app:alignItems="stretch"
+        app:flexWrap="wrap">
+
+        <include
+            android:id="@+id/message_quote"
+            layout="@layout/item_message_quote"
+            android:visibility="gone" />
+
+        <androidx.emoji.widget.EmojiTextView
+            android:id="@+id/messageAuthor"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="4dp"
+            android:alpha="0.6"
+            android:textAlignment="viewStart"
+            android:textColor="@color/no_emphasis_text"
+            android:textIsSelectable="false"
+            android:textSize="12sp"
+            tools:text="Jane Doe" />
+
+        <androidx.emoji.widget.EmojiTextView
+            android:id="@id/messageText"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:lineSpacingMultiplier="1.2"
+            android:textAlignment="viewStart"
+            android:textIsSelectable="false"
+            app:layout_alignSelf="flex_start"
+            app:layout_flexGrow="1"
+            app:layout_wrapBefore="true"
+            tools:text="Talk to you later!" />
+
+        <include
+            android:id="@+id/referenceInclude"
+            layout="@layout/reference_inside_message" />
+
+        <TextView
+            android:id="@id/messageTime"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_below="@id/messageText"
+            android:layout_marginStart="8dp"
+            android:alpha="0.6"
+            android:gravity="end"
+            android:textColor="@color/no_emphasis_text"
+            android:textIsSelectable="false"
+            app:layout_alignSelf="center"
+            app:layout_flexGrow="1"
+            app:layout_wrapBefore="false"
+            tools:text="12:38" />
+
+        <include
+            android:id="@+id/reactions"
+            layout="@layout/reactions_inside_message" />
+
+    </com.google.android.flexbox.FlexboxLayout>
+</RelativeLayout>

+ 96 - 0
app/src/main/res/layout/item_custom_outcoming_link_preview_message.xml

@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Nextcloud Talk application
+  ~
+  ~ @author Mario Danic
+  ~ @author Andy Scherzinger
+  ~ @author Marcel Hibbe
+  ~ Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+  ~ Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
+  ~ Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation, either version 3 of the License, or
+  ~ at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+  -->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_marginLeft="16dp"
+    android:layout_marginTop="2dp"
+    android:layout_marginRight="16dp"
+    android:layout_marginBottom="2dp">
+
+    <com.google.android.flexbox.FlexboxLayout
+        android:id="@id/bubble"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentEnd="true"
+        android:layout_marginStart="@dimen/message_outcoming_bubble_margin_left"
+        app:alignContent="stretch"
+        app:alignItems="stretch"
+        app:flexWrap="wrap"
+        app:justifyContent="flex_end">
+
+        <include
+            android:id="@+id/message_quote"
+            layout="@layout/item_message_quote"
+            android:visibility="gone" />
+
+        <androidx.emoji.widget.EmojiTextView
+            android:id="@id/messageText"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_alignWithParentIfMissing="true"
+            android:lineSpacingMultiplier="1.2"
+            android:textAlignment="viewStart"
+            android:textColorHighlight="@color/nc_grey"
+            android:textIsSelectable="false"
+            tools:text="Talk to you later!" />
+
+        <include
+            android:id="@+id/referenceInclude"
+            layout="@layout/reference_inside_message" />
+
+        <TextView
+            android:id="@id/messageTime"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_below="@id/messageText"
+            android:layout_marginStart="8dp"
+            android:alpha="0.6"
+            android:gravity="end"
+            android:textColor="@color/no_emphasis_text"
+            android:textIsSelectable="false"
+            app:layout_alignSelf="center"
+            app:layout_flexGrow="1"
+            app:layout_wrapBefore="false"
+            tools:text="10:35" />
+
+        <ImageView
+            android:id="@+id/checkMark"
+            android:layout_width="wrap_content"
+            android:layout_height="@dimen/message_bubble_checkmark_height"
+            android:layout_below="@id/messageTime"
+            android:layout_marginStart="8dp"
+            android:contentDescription="@null"
+            app:layout_alignSelf="center"
+            app:tint="@color/high_emphasis_text" />
+
+        <include
+            android:id="@+id/reactions"
+            layout="@layout/reactions_inside_message" />
+
+    </com.google.android.flexbox.FlexboxLayout>
+</RelativeLayout>

+ 67 - 0
app/src/main/res/layout/reference_inside_message.xml

@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+
+  Nextcloud Talk application
+
+  Copyright (C) 2022 Marcel Hibbe
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU Affero General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  GNU Affero General Public License for more details.
+
+  You should have received a copy of the GNU Affero General Public License
+  along with this program. If not, see <https://www.gnu.org/licenses/>.
+-->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/referenceWrapper"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_marginTop="5dp">
+
+    <View
+        android:id="@+id/referenceColoredView"
+        android:layout_width="2dp"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="8dp"
+        android:background="@color/high_emphasis_text"
+        tools:layout_height="100dp"/>
+
+    <androidx.emoji.widget.EmojiTextView
+        android:id="@+id/referenceName"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:lineSpacingMultiplier="1.2"
+        android:textAlignment="viewStart"
+        android:textIsSelectable="false"
+        android:layout_marginStart="10dp"
+        tools:text="Name of Website" />
+
+    <androidx.emoji.widget.EmojiTextView
+        android:id="@+id/referenceLink"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_below="@id/referenceName"
+        android:lineSpacingMultiplier="1.2"
+        android:textAlignment="viewStart"
+        android:textIsSelectable="false"
+        android:layout_marginStart="10dp"
+        tools:text="http://nextcloud.com" />
+
+    <com.facebook.drawee.view.SimpleDraweeView
+        android:id="@+id/referenceThumbImage"
+        android:layout_width="match_parent"
+        android:layout_height="120dp"
+        android:scaleType="fitEnd"
+        android:layout_below="@id/referenceLink"
+        android:layout_marginTop="5dp"
+        android:layout_marginStart="10dp"
+        app:roundedCornerRadius="6dp" />
+</RelativeLayout>