浏览代码

Merge pull request #2460 from nextcloud/feature/2419/linkPreviews

add openGraph link previews
Tim Krüger 2 年之前
父节点
当前提交
009cd5a425
共有 30 个文件被更改,包括 1190 次插入59 次删除
  1. 2 2
      app/src/main/java/com/nextcloud/talk/adapters/messages/CommonMessageInterface.kt
  2. 211 0
      app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLinkPreviewMessageViewHolder.kt
  3. 5 5
      app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt
  4. 5 5
      app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPollMessageViewHolder.kt
  5. 5 5
      app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt
  6. 122 0
      app/src/main/java/com/nextcloud/talk/adapters/messages/LinkPreview.kt
  7. 5 5
      app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt
  8. 5 5
      app/src/main/java/com/nextcloud/talk/adapters/messages/MagicOutcomingTextMessageViewHolder.kt
  9. 5 5
      app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.java
  10. 172 0
      app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLinkPreviewMessageViewHolder.kt
  11. 5 5
      app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLocationMessageViewHolder.kt
  12. 5 5
      app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPollMessageViewHolder.kt
  13. 5 5
      app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingVoiceMessageViewHolder.kt
  14. 12 7
      app/src/main/java/com/nextcloud/talk/adapters/messages/TalkMessagesListAdapter.java
  15. 6 0
      app/src/main/java/com/nextcloud/talk/api/NcApi.java
  16. 19 4
      app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt
  17. 3 1
      app/src/main/java/com/nextcloud/talk/models/json/capabilities/Capabilities.kt
  18. 43 0
      app/src/main/java/com/nextcloud/talk/models/json/capabilities/CoreCapability.kt
  19. 34 0
      app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.kt
  20. 38 0
      app/src/main/java/com/nextcloud/talk/models/json/opengraph/OpenGraphOCS.kt
  21. 44 0
      app/src/main/java/com/nextcloud/talk/models/json/opengraph/OpenGraphObject.kt
  22. 35 0
      app/src/main/java/com/nextcloud/talk/models/json/opengraph/OpenGraphOverall.kt
  23. 35 0
      app/src/main/java/com/nextcloud/talk/models/json/opengraph/OpenGraphResponse.kt
  24. 42 0
      app/src/main/java/com/nextcloud/talk/models/json/opengraph/Reference.kt
  25. 44 0
      app/src/main/java/com/nextcloud/talk/models/json/opengraph/RichObject.kt
  26. 3 0
      app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java
  27. 10 0
      app/src/main/java/com/nextcloud/talk/utils/database/user/CapabilitiesUtilNew.kt
  28. 107 0
      app/src/main/res/layout/item_custom_incoming_link_preview_message.xml
  29. 96 0
      app/src/main/res/layout/item_custom_outcoming_link_preview_message.xml
  30. 67 0
      app/src/main/res/layout/reference_inside_message.xml

+ 2 - 2
app/src/main/java/com/nextcloud/talk/adapters/messages/ReactionsInterface.kt → app/src/main/java/com/nextcloud/talk/adapters/messages/CommonMessageInterface.kt

@@ -2,7 +2,7 @@ package com.nextcloud.talk.adapters.messages
 
 
 import com.nextcloud.talk.models.json.chat.ChatMessage
 import com.nextcloud.talk.models.json.chat.ChatMessage
 
 
-interface ReactionsInterface {
+interface CommonMessageInterface {
     fun onClickReactions(chatMessage: ChatMessage)
     fun onClickReactions(chatMessage: ChatMessage)
-    fun onLongClickReactions(chatMessage: ChatMessage)
+    fun onOpenMessageActionsDialog(chatMessage: ChatMessage)
 }
 }

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

@@ -0,0 +1,211 @@
+/*
+ * 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 commonMessageInterface: CommonMessageInterface
+
+    @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
+        )
+        binding.referenceInclude.referenceWrapper.setOnLongClickListener { l: View? ->
+            commonMessageInterface.onOpenMessageActionsDialog(message)
+            true
+        }
+
+        Reaction().showReactions(
+            message,
+            binding.reactions,
+            binding.messageTime.context,
+            false,
+            viewThemeUtils
+        )
+        binding.reactions.reactionsEmojiWrapper.setOnClickListener {
+            commonMessageInterface.onClickReactions(message)
+        }
+        binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
+            commonMessageInterface.onOpenMessageActionsDialog(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 assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) {
+        this.commonMessageInterface = commonMessageInterface
+    }
+
+    companion object {
+        private val TAG = IncomingLinkPreviewMessageViewHolder::class.java.simpleName
+    }
+}

+ 5 - 5
app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt

@@ -78,7 +78,7 @@ class IncomingLocationMessageViewHolder(incomingView: View, payload: Any) : Mess
     @Inject
     @Inject
     lateinit var viewThemeUtils: ViewThemeUtils
     lateinit var viewThemeUtils: ViewThemeUtils
 
 
-    lateinit var reactionsInterface: ReactionsInterface
+    lateinit var commonMessageInterface: CommonMessageInterface
 
 
     @SuppressLint("SetTextI18n")
     @SuppressLint("SetTextI18n")
     override fun onBind(message: ChatMessage) {
     override fun onBind(message: ChatMessage) {
@@ -109,10 +109,10 @@ class IncomingLocationMessageViewHolder(incomingView: View, payload: Any) : Mess
             viewThemeUtils
             viewThemeUtils
         )
         )
         binding.reactions.reactionsEmojiWrapper.setOnClickListener {
         binding.reactions.reactionsEmojiWrapper.setOnClickListener {
-            reactionsInterface.onClickReactions(message)
+            commonMessageInterface.onClickReactions(message)
         }
         }
         binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
         binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
-            reactionsInterface.onLongClickReactions(message)
+            commonMessageInterface.onOpenMessageActionsDialog(message)
             true
             true
         }
         }
     }
     }
@@ -267,8 +267,8 @@ class IncomingLocationMessageViewHolder(incomingView: View, payload: Any) : Mess
         return locationGeoLink.replace("geo:", "geo:0,0?q=")
         return locationGeoLink.replace("geo:", "geo:0,0?q=")
     }
     }
 
 
-    fun assignReactionInterface(reactionsInterface: ReactionsInterface) {
-        this.reactionsInterface = reactionsInterface
+    fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) {
+        this.commonMessageInterface = commonMessageInterface
     }
     }
 
 
     companion object {
     companion object {

+ 5 - 5
app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPollMessageViewHolder.kt

@@ -68,7 +68,7 @@ class IncomingPollMessageViewHolder(incomingView: View, payload: Any) : MessageH
 
 
     lateinit var message: ChatMessage
     lateinit var message: ChatMessage
 
 
-    lateinit var reactionsInterface: ReactionsInterface
+    lateinit var commonMessageInterface: CommonMessageInterface
 
 
     @SuppressLint("SetTextI18n")
     @SuppressLint("SetTextI18n")
     override fun onBind(message: ChatMessage) {
     override fun onBind(message: ChatMessage) {
@@ -95,10 +95,10 @@ class IncomingPollMessageViewHolder(incomingView: View, payload: Any) : MessageH
             viewThemeUtils
             viewThemeUtils
         )
         )
         binding.reactions.reactionsEmojiWrapper.setOnClickListener {
         binding.reactions.reactionsEmojiWrapper.setOnClickListener {
-            reactionsInterface.onClickReactions(message)
+            commonMessageInterface.onClickReactions(message)
         }
         }
         binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
         binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
-            reactionsInterface.onLongClickReactions(message)
+            commonMessageInterface.onOpenMessageActionsDialog(message)
             true
             true
         }
         }
     }
     }
@@ -228,8 +228,8 @@ class IncomingPollMessageViewHolder(incomingView: View, payload: Any) : MessageH
         }
         }
     }
     }
 
 
-    fun assignReactionInterface(reactionsInterface: ReactionsInterface) {
-        this.reactionsInterface = reactionsInterface
+    fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) {
+        this.commonMessageInterface = commonMessageInterface
     }
     }
 
 
     companion object {
     companion object {

+ 5 - 5
app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt

@@ -76,7 +76,7 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) : Message
     lateinit var message: ChatMessage
     lateinit var message: ChatMessage
 
 
     lateinit var voiceMessageInterface: VoiceMessageInterface
     lateinit var voiceMessageInterface: VoiceMessageInterface
-    lateinit var reactionsInterface: ReactionsInterface
+    lateinit var commonMessageInterface: CommonMessageInterface
 
 
     @SuppressLint("SetTextI18n")
     @SuppressLint("SetTextI18n")
     override fun onBind(message: ChatMessage) {
     override fun onBind(message: ChatMessage) {
@@ -153,10 +153,10 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) : Message
             viewThemeUtils
             viewThemeUtils
         )
         )
         binding.reactions.reactionsEmojiWrapper.setOnClickListener {
         binding.reactions.reactionsEmojiWrapper.setOnClickListener {
-            reactionsInterface.onClickReactions(message)
+            commonMessageInterface.onClickReactions(message)
         }
         }
         binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
         binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
-            reactionsInterface.onLongClickReactions(message)
+            commonMessageInterface.onOpenMessageActionsDialog(message)
             true
             true
         }
         }
     }
     }
@@ -307,8 +307,8 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) : Message
         this.voiceMessageInterface = voiceMessageInterface
         this.voiceMessageInterface = voiceMessageInterface
     }
     }
 
 
-    fun assignReactionInterface(reactionsInterface: ReactionsInterface) {
-        this.reactionsInterface = reactionsInterface
+    fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) {
+        this.commonMessageInterface = commonMessageInterface
     }
     }
 
 
     companion object {
     companion object {

+ 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
+    }
+}

+ 5 - 5
app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt

@@ -69,7 +69,7 @@ class MagicIncomingTextMessageViewHolder(itemView: View, payload: Any) : Message
     @Inject
     @Inject
     lateinit var appPreferences: AppPreferences
     lateinit var appPreferences: AppPreferences
 
 
-    lateinit var reactionsInterface: ReactionsInterface
+    lateinit var commonMessageInterface: CommonMessageInterface
 
 
     override fun onBind(message: ChatMessage) {
     override fun onBind(message: ChatMessage) {
         super.onBind(message)
         super.onBind(message)
@@ -125,10 +125,10 @@ class MagicIncomingTextMessageViewHolder(itemView: View, payload: Any) : Message
             viewThemeUtils
             viewThemeUtils
         )
         )
         binding.reactions.reactionsEmojiWrapper.setOnClickListener {
         binding.reactions.reactionsEmojiWrapper.setOnClickListener {
-            reactionsInterface.onClickReactions(message)
+            commonMessageInterface.onClickReactions(message)
         }
         }
         binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
         binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
-            reactionsInterface.onLongClickReactions(message)
+            commonMessageInterface.onOpenMessageActionsDialog(message)
             true
             true
         }
         }
     }
     }
@@ -249,8 +249,8 @@ class MagicIncomingTextMessageViewHolder(itemView: View, payload: Any) : Message
         return messageStringInternal
         return messageStringInternal
     }
     }
 
 
-    fun assignReactionInterface(reactionsInterface: ReactionsInterface) {
-        this.reactionsInterface = reactionsInterface
+    fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) {
+        this.commonMessageInterface = commonMessageInterface
     }
     }
 
 
     companion object {
     companion object {

+ 5 - 5
app/src/main/java/com/nextcloud/talk/adapters/messages/MagicOutcomingTextMessageViewHolder.kt

@@ -60,7 +60,7 @@ class MagicOutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessage
     @Inject
     @Inject
     lateinit var viewThemeUtils: ViewThemeUtils
     lateinit var viewThemeUtils: ViewThemeUtils
 
 
-    lateinit var reactionsInterface: ReactionsInterface
+    lateinit var commonMessageInterface: CommonMessageInterface
 
 
     override fun onBind(message: ChatMessage) {
     override fun onBind(message: ChatMessage) {
         super.onBind(message)
         super.onBind(message)
@@ -123,10 +123,10 @@ class MagicOutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessage
 
 
         Reaction().showReactions(message, binding.reactions, context, true, viewThemeUtils)
         Reaction().showReactions(message, binding.reactions, context, true, viewThemeUtils)
         binding.reactions.reactionsEmojiWrapper.setOnClickListener {
         binding.reactions.reactionsEmojiWrapper.setOnClickListener {
-            reactionsInterface.onClickReactions(message)
+            commonMessageInterface.onClickReactions(message)
         }
         }
         binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
         binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
-            reactionsInterface.onLongClickReactions(message)
+            commonMessageInterface.onOpenMessageActionsDialog(message)
             true
             true
         }
         }
     }
     }
@@ -193,8 +193,8 @@ class MagicOutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessage
         return messageString1
         return messageString1
     }
     }
 
 
-    fun assignReactionInterface(reactionsInterface: ReactionsInterface) {
-        this.reactionsInterface = reactionsInterface
+    fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) {
+        this.commonMessageInterface = commonMessageInterface
     }
     }
 
 
     companion object {
     companion object {

+ 5 - 5
app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.java

@@ -108,7 +108,7 @@ public abstract class MagicPreviewMessageViewHolder extends MessageHolders.Incom
 
 
     View clickView;
     View clickView;
 
 
-    ReactionsInterface reactionsInterface;
+    CommonMessageInterface commonMessageInterface;
     PreviewMessageInterface previewMessageInterface;
     PreviewMessageInterface previewMessageInterface;
 
 
     public MagicPreviewMessageViewHolder(View itemView, Object payload) {
     public MagicPreviewMessageViewHolder(View itemView, Object payload) {
@@ -251,10 +251,10 @@ public abstract class MagicPreviewMessageViewHolder extends MessageHolders.Incom
                                      true,
                                      true,
                                      viewThemeUtils);
                                      viewThemeUtils);
         reactionsBinding.reactionsEmojiWrapper.setOnClickListener(l -> {
         reactionsBinding.reactionsEmojiWrapper.setOnClickListener(l -> {
-            reactionsInterface.onClickReactions(message);
+            commonMessageInterface.onClickReactions(message);
         });
         });
         reactionsBinding.reactionsEmojiWrapper.setOnLongClickListener(l -> {
         reactionsBinding.reactionsEmojiWrapper.setOnLongClickListener(l -> {
-            reactionsInterface.onLongClickReactions(message);
+            commonMessageInterface.onOpenMessageActionsDialog(message);
             return true;
             return true;
         });
         });
     }
     }
@@ -347,8 +347,8 @@ public abstract class MagicPreviewMessageViewHolder extends MessageHolders.Incom
             });
             });
     }
     }
 
 
-    public void assignReactionInterface(ReactionsInterface reactionsInterface) {
-        this.reactionsInterface = reactionsInterface;
+    public void assignCommonMessageInterface(CommonMessageInterface commonMessageInterface) {
+        this.commonMessageInterface = commonMessageInterface;
     }
     }
 
 
     public void assignPreviewMessageInterface(PreviewMessageInterface previewMessageInterface) {
     public void assignPreviewMessageInterface(PreviewMessageInterface previewMessageInterface) {

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

@@ -0,0 +1,172 @@
+/*
+ * 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 commonMessageInterface: CommonMessageInterface
+
+    @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
+        )
+        binding.referenceInclude.referenceWrapper.setOnLongClickListener { l: View? ->
+            commonMessageInterface.onOpenMessageActionsDialog(message)
+            true
+        }
+
+        Reaction().showReactions(
+            message,
+            binding.reactions,
+            binding.messageTime.context,
+            true,
+            viewThemeUtils
+        )
+        binding.reactions.reactionsEmojiWrapper.setOnClickListener {
+            commonMessageInterface.onClickReactions(message)
+        }
+        binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
+            commonMessageInterface.onOpenMessageActionsDialog(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 assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) {
+        this.commonMessageInterface = commonMessageInterface
+    }
+
+    companion object {
+        private val TAG = OutcomingLinkPreviewMessageViewHolder::class.java.simpleName
+    }
+}

+ 5 - 5
app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLocationMessageViewHolder.kt

@@ -70,7 +70,7 @@ class OutcomingLocationMessageViewHolder(incomingView: View) : MessageHolders
     @Inject
     @Inject
     lateinit var viewThemeUtils: ViewThemeUtils
     lateinit var viewThemeUtils: ViewThemeUtils
 
 
-    lateinit var reactionsInterface: ReactionsInterface
+    lateinit var commonMessageInterface: CommonMessageInterface
 
 
     @SuppressLint("SetTextI18n")
     @SuppressLint("SetTextI18n")
     override fun onBind(message: ChatMessage) {
     override fun onBind(message: ChatMessage) {
@@ -128,10 +128,10 @@ class OutcomingLocationMessageViewHolder(incomingView: View) : MessageHolders
             viewThemeUtils
             viewThemeUtils
         )
         )
         binding.reactions.reactionsEmojiWrapper.setOnClickListener {
         binding.reactions.reactionsEmojiWrapper.setOnClickListener {
-            reactionsInterface.onClickReactions(message)
+            commonMessageInterface.onClickReactions(message)
         }
         }
         binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
         binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
-            reactionsInterface.onLongClickReactions(message)
+            commonMessageInterface.onOpenMessageActionsDialog(message)
             true
             true
         }
         }
     }
     }
@@ -242,8 +242,8 @@ class OutcomingLocationMessageViewHolder(incomingView: View) : MessageHolders
         return locationGeoLink.replace("geo:", "geo:0,0?q=")
         return locationGeoLink.replace("geo:", "geo:0,0?q=")
     }
     }
 
 
-    fun assignReactionInterface(reactionsInterface: ReactionsInterface) {
-        this.reactionsInterface = reactionsInterface
+    fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) {
+        this.commonMessageInterface = commonMessageInterface
     }
     }
 
 
     companion object {
     companion object {

+ 5 - 5
app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPollMessageViewHolder.kt

@@ -64,7 +64,7 @@ class OutcomingPollMessageViewHolder(outcomingView: View, payload: Any) : Messag
 
 
     lateinit var message: ChatMessage
     lateinit var message: ChatMessage
 
 
-    lateinit var reactionsInterface: ReactionsInterface
+    lateinit var commonMessageInterface: CommonMessageInterface
 
 
     @SuppressLint("SetTextI18n")
     @SuppressLint("SetTextI18n")
     override fun onBind(message: ChatMessage) {
     override fun onBind(message: ChatMessage) {
@@ -114,10 +114,10 @@ class OutcomingPollMessageViewHolder(outcomingView: View, payload: Any) : Messag
             viewThemeUtils
             viewThemeUtils
         )
         )
         binding.reactions.reactionsEmojiWrapper.setOnClickListener {
         binding.reactions.reactionsEmojiWrapper.setOnClickListener {
-            reactionsInterface.onClickReactions(message)
+            commonMessageInterface.onClickReactions(message)
         }
         }
         binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
         binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
-            reactionsInterface.onLongClickReactions(message)
+            commonMessageInterface.onOpenMessageActionsDialog(message)
             true
             true
         }
         }
     }
     }
@@ -190,8 +190,8 @@ class OutcomingPollMessageViewHolder(outcomingView: View, payload: Any) : Messag
         viewThemeUtils.talk.themeOutgoingMessageBubble(bubble, message.isGrouped, message.isDeleted)
         viewThemeUtils.talk.themeOutgoingMessageBubble(bubble, message.isGrouped, message.isDeleted)
     }
     }
 
 
-    fun assignReactionInterface(reactionsInterface: ReactionsInterface) {
-        this.reactionsInterface = reactionsInterface
+    fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) {
+        this.commonMessageInterface = commonMessageInterface
     }
     }
 
 
     companion object {
     companion object {

+ 5 - 5
app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingVoiceMessageViewHolder.kt

@@ -71,7 +71,7 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) : MessageHolders
     lateinit var handler: Handler
     lateinit var handler: Handler
 
 
     lateinit var voiceMessageInterface: VoiceMessageInterface
     lateinit var voiceMessageInterface: VoiceMessageInterface
-    lateinit var reactionsInterface: ReactionsInterface
+    lateinit var commonMessageInterface: CommonMessageInterface
 
 
     @SuppressLint("SetTextI18n")
     @SuppressLint("SetTextI18n")
     override fun onBind(message: ChatMessage) {
     override fun onBind(message: ChatMessage) {
@@ -146,10 +146,10 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) : MessageHolders
             viewThemeUtils
             viewThemeUtils
         )
         )
         binding.reactions.reactionsEmojiWrapper.setOnClickListener {
         binding.reactions.reactionsEmojiWrapper.setOnClickListener {
-            reactionsInterface.onClickReactions(message)
+            commonMessageInterface.onClickReactions(message)
         }
         }
         binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
         binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
-            reactionsInterface.onLongClickReactions(message)
+            commonMessageInterface.onOpenMessageActionsDialog(message)
             true
             true
         }
         }
     }
     }
@@ -281,8 +281,8 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) : MessageHolders
         this.voiceMessageInterface = voiceMessageInterface
         this.voiceMessageInterface = voiceMessageInterface
     }
     }
 
 
-    fun assignReactionInterface(reactionsInterface: ReactionsInterface) {
-        this.reactionsInterface = reactionsInterface
+    fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) {
+        this.commonMessageInterface = commonMessageInterface
     }
     }
 
 
     companion object {
     companion object {

+ 12 - 7
app/src/main/java/com/nextcloud/talk/adapters/messages/TalkMessagesListAdapter.java

@@ -50,25 +50,30 @@ public class TalkMessagesListAdapter<M extends IMessage> extends MessagesListAda
         super.onBindViewHolder(holder, position);
         super.onBindViewHolder(holder, position);
 
 
         if (holder instanceof MagicIncomingTextMessageViewHolder) {
         if (holder instanceof MagicIncomingTextMessageViewHolder) {
-            ((MagicIncomingTextMessageViewHolder) holder).assignReactionInterface(chatController);
+            ((MagicIncomingTextMessageViewHolder) holder).assignCommonMessageInterface(chatController);
         } else if (holder instanceof MagicOutcomingTextMessageViewHolder) {
         } else if (holder instanceof MagicOutcomingTextMessageViewHolder) {
-            ((MagicOutcomingTextMessageViewHolder) holder).assignReactionInterface(chatController);
+            ((MagicOutcomingTextMessageViewHolder) holder).assignCommonMessageInterface(chatController);
 
 
         } else if (holder instanceof IncomingLocationMessageViewHolder) {
         } else if (holder instanceof IncomingLocationMessageViewHolder) {
-            ((IncomingLocationMessageViewHolder) holder).assignReactionInterface(chatController);
+            ((IncomingLocationMessageViewHolder) holder).assignCommonMessageInterface(chatController);
         } else if (holder instanceof OutcomingLocationMessageViewHolder) {
         } else if (holder instanceof OutcomingLocationMessageViewHolder) {
-            ((OutcomingLocationMessageViewHolder) holder).assignReactionInterface(chatController);
+            ((OutcomingLocationMessageViewHolder) holder).assignCommonMessageInterface(chatController);
+
+        } else if (holder instanceof IncomingLinkPreviewMessageViewHolder) {
+            ((IncomingLinkPreviewMessageViewHolder) holder).assignCommonMessageInterface(chatController);
+        } else if (holder instanceof OutcomingLinkPreviewMessageViewHolder) {
+            ((OutcomingLinkPreviewMessageViewHolder) holder).assignCommonMessageInterface(chatController);
 
 
         } else if (holder instanceof IncomingVoiceMessageViewHolder) {
         } else if (holder instanceof IncomingVoiceMessageViewHolder) {
             ((IncomingVoiceMessageViewHolder) holder).assignVoiceMessageInterface(chatController);
             ((IncomingVoiceMessageViewHolder) holder).assignVoiceMessageInterface(chatController);
-            ((IncomingVoiceMessageViewHolder) holder).assignReactionInterface(chatController);
+            ((IncomingVoiceMessageViewHolder) holder).assignCommonMessageInterface(chatController);
         } else if (holder instanceof OutcomingVoiceMessageViewHolder) {
         } else if (holder instanceof OutcomingVoiceMessageViewHolder) {
             ((OutcomingVoiceMessageViewHolder) holder).assignVoiceMessageInterface(chatController);
             ((OutcomingVoiceMessageViewHolder) holder).assignVoiceMessageInterface(chatController);
-            ((OutcomingVoiceMessageViewHolder) holder).assignReactionInterface(chatController);
+            ((OutcomingVoiceMessageViewHolder) holder).assignCommonMessageInterface(chatController);
 
 
         } else if (holder instanceof MagicPreviewMessageViewHolder) {
         } else if (holder instanceof MagicPreviewMessageViewHolder) {
             ((MagicPreviewMessageViewHolder) holder).assignPreviewMessageInterface(chatController);
             ((MagicPreviewMessageViewHolder) holder).assignPreviewMessageInterface(chatController);
-            ((MagicPreviewMessageViewHolder) holder).assignReactionInterface(chatController);
+            ((MagicPreviewMessageViewHolder) holder).assignCommonMessageInterface(chatController);
         }
         }
     }
     }
 }
 }

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

+ 19 - 4
app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt

@@ -106,6 +106,8 @@ import com.nextcloud.talk.R
 import com.nextcloud.talk.activities.CallActivity
 import com.nextcloud.talk.activities.CallActivity
 import com.nextcloud.talk.activities.MainActivity
 import com.nextcloud.talk.activities.MainActivity
 import com.nextcloud.talk.activities.TakePhotoActivity
 import com.nextcloud.talk.activities.TakePhotoActivity
+import com.nextcloud.talk.adapters.messages.CommonMessageInterface
+import com.nextcloud.talk.adapters.messages.IncomingLinkPreviewMessageViewHolder
 import com.nextcloud.talk.adapters.messages.IncomingLocationMessageViewHolder
 import com.nextcloud.talk.adapters.messages.IncomingLocationMessageViewHolder
 import com.nextcloud.talk.adapters.messages.IncomingPollMessageViewHolder
 import com.nextcloud.talk.adapters.messages.IncomingPollMessageViewHolder
 import com.nextcloud.talk.adapters.messages.IncomingPreviewMessageViewHolder
 import com.nextcloud.talk.adapters.messages.IncomingPreviewMessageViewHolder
@@ -115,12 +117,12 @@ import com.nextcloud.talk.adapters.messages.MagicOutcomingTextMessageViewHolder
 import com.nextcloud.talk.adapters.messages.MagicSystemMessageViewHolder
 import com.nextcloud.talk.adapters.messages.MagicSystemMessageViewHolder
 import com.nextcloud.talk.adapters.messages.MagicUnreadNoticeMessageViewHolder
 import com.nextcloud.talk.adapters.messages.MagicUnreadNoticeMessageViewHolder
 import com.nextcloud.talk.adapters.messages.MessagePayload
 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.OutcomingLocationMessageViewHolder
 import com.nextcloud.talk.adapters.messages.OutcomingPollMessageViewHolder
 import com.nextcloud.talk.adapters.messages.OutcomingPollMessageViewHolder
 import com.nextcloud.talk.adapters.messages.OutcomingPreviewMessageViewHolder
 import com.nextcloud.talk.adapters.messages.OutcomingPreviewMessageViewHolder
 import com.nextcloud.talk.adapters.messages.OutcomingVoiceMessageViewHolder
 import com.nextcloud.talk.adapters.messages.OutcomingVoiceMessageViewHolder
 import com.nextcloud.talk.adapters.messages.PreviewMessageInterface
 import com.nextcloud.talk.adapters.messages.PreviewMessageInterface
-import com.nextcloud.talk.adapters.messages.ReactionsInterface
 import com.nextcloud.talk.adapters.messages.TalkMessagesListAdapter
 import com.nextcloud.talk.adapters.messages.TalkMessagesListAdapter
 import com.nextcloud.talk.adapters.messages.VoiceMessageInterface
 import com.nextcloud.talk.adapters.messages.VoiceMessageInterface
 import com.nextcloud.talk.api.NcApi
 import com.nextcloud.talk.api.NcApi
@@ -222,7 +224,7 @@ class ChatController(args: Bundle) :
     MessagesListAdapter.OnMessageViewLongClickListener<IMessage>,
     MessagesListAdapter.OnMessageViewLongClickListener<IMessage>,
     ContentChecker<ChatMessage>,
     ContentChecker<ChatMessage>,
     VoiceMessageInterface,
     VoiceMessageInterface,
-    ReactionsInterface,
+    CommonMessageInterface,
     PreviewMessageInterface {
     PreviewMessageInterface {
 
 
     private val binding: ControllerChatBinding by viewBinding(ControllerChatBinding::bind)
     private val binding: ControllerChatBinding by viewBinding(ControllerChatBinding::bind)
@@ -587,6 +589,17 @@ class ChatController(args: Bundle) :
                 this
                 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("?")) {
             val senderId = if (!conversationUser.userId.equals("?")) {
                 "users/" + conversationUser.userId
                 "users/" + conversationUser.userId
             } else {
             } else {
@@ -2788,7 +2801,7 @@ class ChatController(args: Bundle) :
         }
         }
     }
     }
 
 
-    override fun onLongClickReactions(chatMessage: ChatMessage) {
+    override fun onOpenMessageActionsDialog(chatMessage: ChatMessage) {
         openMessageActionsDialog(chatMessage)
         openMessageActionsDialog(chatMessage)
     }
     }
 
 
@@ -2797,7 +2810,7 @@ class ChatController(args: Bundle) :
     }
     }
 
 
     override fun onPreviewMessageLongClick(chatMessage: ChatMessage) {
     override fun onPreviewMessageLongClick(chatMessage: ChatMessage) {
-        openMessageActionsDialog(chatMessage)
+        onOpenMessageActionsDialog(chatMessage)
     }
     }
 
 
     private fun openMessageActionsDialog(iMessage: IMessage?) {
     private fun openMessageActionsDialog(iMessage: IMessage?) {
@@ -3162,6 +3175,7 @@ class ChatController(args: Bundle) :
             CONTENT_TYPE_LOCATION -> message.hasGeoLocation()
             CONTENT_TYPE_LOCATION -> message.hasGeoLocation()
             CONTENT_TYPE_VOICE_MESSAGE -> message.isVoiceMessage
             CONTENT_TYPE_VOICE_MESSAGE -> message.isVoiceMessage
             CONTENT_TYPE_POLL -> message.isPoll()
             CONTENT_TYPE_POLL -> message.isPoll()
+            CONTENT_TYPE_LINK_PREVIEW -> message.isLinkPreview()
             CONTENT_TYPE_SYSTEM_MESSAGE -> !TextUtils.isEmpty(message.systemMessage)
             CONTENT_TYPE_SYSTEM_MESSAGE -> !TextUtils.isEmpty(message.systemMessage)
             CONTENT_TYPE_UNREAD_NOTICE_MESSAGE -> message.id == "-1"
             CONTENT_TYPE_UNREAD_NOTICE_MESSAGE -> message.id == "-1"
             else -> false
             else -> false
@@ -3322,6 +3336,7 @@ class ChatController(args: Bundle) :
         private const val CONTENT_TYPE_LOCATION: Byte = 3
         private const val CONTENT_TYPE_LOCATION: Byte = 3
         private const val CONTENT_TYPE_VOICE_MESSAGE: Byte = 4
         private const val CONTENT_TYPE_VOICE_MESSAGE: Byte = 4
         private const val CONTENT_TYPE_POLL: Byte = 5
         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 NEW_MESSAGES_POPUP_BUBBLE_DELAY: Long = 200
         private const val POP_CURRENT_CONTROLLER_DELAY: Long = 100
         private const val POP_CURRENT_CONTROLLER_DELAY: Long = 100
         private const val LOBBY_TIMER_DELAY: Long = 5000
         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
 @Parcelize
 @JsonObject
 @JsonObject
 data class Capabilities(
 data class Capabilities(
+    @JsonField(name = ["core"])
+    var coreCapability: CoreCapability?,
     @JsonField(name = ["spreed"])
     @JsonField(name = ["spreed"])
     var spreedCapability: SpreedCapability?,
     var spreedCapability: SpreedCapability?,
     @JsonField(name = ["notifications"])
     @JsonField(name = ["notifications"])
@@ -43,5 +45,5 @@ data class Capabilities(
     var userStatusCapability: UserStatusCapability?
     var userStatusCapability: UserStatusCapability?
 ) : Parcelable {
 ) : Parcelable {
     // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
     // 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.chat.ChatUtils.Companion.getParsedMessage
 import com.nextcloud.talk.models.json.converters.EnumSystemMessageTypeConverter
 import com.nextcloud.talk.models.json.converters.EnumSystemMessageTypeConverter
 import com.nextcloud.talk.utils.ApiUtils
 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.IUser
 import com.stfalcon.chatkit.commons.models.MessageContentType
 import com.stfalcon.chatkit.commons.models.MessageContentType
 import kotlinx.android.parcel.Parcelize
 import kotlinx.android.parcel.Parcelize
@@ -126,8 +127,11 @@ data class ChatMessage(
     var voiceMessagePlayedSeconds: Int = 0,
     var voiceMessagePlayedSeconds: Int = 0,
 
 
     var voiceMessageDownloadProgress: Int = 0,
     var voiceMessageDownloadProgress: Int = 0,
+
 ) : Parcelable, MessageContentType, MessageContentType.Image {
 ) : Parcelable, MessageContentType, MessageContentType.Image {
 
 
+    var extractedUrlToPreview: String? = null
+
     // messageTypesToIgnore is weird. must be deleted by refactoring!!!
     // messageTypesToIgnore is weird. must be deleted by refactoring!!!
     @JsonIgnore
     @JsonIgnore
     var messageTypesToIgnore = Arrays.asList(
     var messageTypesToIgnore = Arrays.asList(
@@ -174,6 +178,33 @@ data class ChatMessage(
         return false
         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")
     @Suppress("Detekt.NestedBlockDepth")
     override fun getImageUrl(): String? {
     override fun getImageUrl(): String? {
         if (messageParameters != null && messageParameters!!.size > 0) {
         if (messageParameters != null && messageParameters!!.size > 0) {
@@ -492,5 +523,8 @@ data class ChatMessage(
     companion object {
     companion object {
         private const val TAG = "ChatMessage"
         private const val TAG = "ChatMessage"
         private const val MILLIES: Long = 1000L
         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";
         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")
         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
     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>