From 3fca452b0e7905191b98400c87c9d4fd1764e461 Mon Sep 17 00:00:00 2001
From: Janne Koschinski <janne@kuschku.de>
Date: Thu, 14 Feb 2019 16:07:07 +0100
Subject: [PATCH] Implement attachments

---
 app/build.gradle.kts                          |   1 +
 app/src/main/AndroidManifest.xml              |  15 +-
 .../quasseldroid/dagger/ActivityModule.kt     |   6 +
 .../ui/chat/messages/MessageAdapter.kt        |  47 ++++
 .../ui/chat/messages/MessageListFragment.kt   |  38 +++-
 .../chat/messages/QuasselMessageRenderer.kt   |  73 ++++--
 .../info/message/MessageAttachmentAdapter.kt  | 103 +++++++++
 .../ui/info/message/MessageInfoActivity.kt    |  41 ++++
 .../ui/info/message/MessageInfoFragment.kt    | 124 ++++++++++
 .../message/MessageInfoFragmentProvider.kt    |  34 +++
 .../util/attachments/AttachmentApi.kt         |  30 +++
 .../util/ui/view/MessageAttachmentView.kt     | 211 ++++++++++++++++++
 app/src/main/res/layout/info_message.xml      |  23 ++
 .../res/layout/widget_chatmessage_action.xml  |  63 +++---
 .../res/layout/widget_chatmessage_info.xml    |  62 +++--
 .../res/layout/widget_chatmessage_notice.xml  |  60 +++--
 .../res/layout/widget_chatmessage_plain.xml   |   6 +-
 .../res/layout/widget_message_attachment.xml  | 173 ++++++++++++++
 .../layout/widget_message_attachment_item.xml |  23 ++
 app/src/main/res/menu/context_messages.xml    |   5 +
 app/src/main/res/values/strings.xml           |   1 +
 gradle.properties                             |   4 +-
 .../de/kuschku/libquassel/protocol/Message.kt |   3 +-
 .../primitive/serializer/MessageSerializer.kt |   4 +
 .../libquassel/quassel/ExtendedFeature.kt     |   4 +-
 .../persistence/dao/MessageDao.kt             |   5 +-
 .../persistence/db/QuasselDatabase.kt         |   7 +-
 .../persistence/models/MessageData.kt         |   3 +
 .../persistence/util/QuasselBacklogStorage.kt |   1 +
 viewmodel/build.gradle.kts                    |   1 +
 .../util/attachment/AttachmentData.kt         |  55 +++++
 .../util/attachment/AttachmentDataField.kt    |  26 +++
 .../viewmodel/data/FormattedMessage.kt        |   4 +
 33 files changed, 1150 insertions(+), 106 deletions(-)
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid/ui/info/message/MessageAttachmentAdapter.kt
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid/ui/info/message/MessageInfoActivity.kt
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid/ui/info/message/MessageInfoFragment.kt
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid/ui/info/message/MessageInfoFragmentProvider.kt
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid/util/attachments/AttachmentApi.kt
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid/util/ui/view/MessageAttachmentView.kt
 create mode 100644 app/src/main/res/layout/info_message.xml
 create mode 100644 app/src/main/res/layout/widget_message_attachment.xml
 create mode 100644 app/src/main/res/layout/widget_message_attachment_item.xml
 create mode 100644 viewmodel/src/main/java/de/kuschku/quasseldroid/util/attachment/AttachmentData.kt
 create mode 100644 viewmodel/src/main/java/de/kuschku/quasseldroid/util/attachment/AttachmentDataField.kt

diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 907d1f2e6..ca7dc1b02 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -137,6 +137,7 @@ dependencies {
   implementation("commons-codec", "commons-codec", "1.11")
   implementation("com.squareup.retrofit2", "retrofit", "2.5.0")
   implementation("com.squareup.retrofit2", "converter-gson", "2.5.0")
+  implementation("com.squareup.retrofit2", "adapter-rxjava2", "2.5.0")
   withVersion("10.0.0") {
     implementation("com.jakewharton", "butterknife", version)
     kapt("com.jakewharton", "butterknife-compiler", version)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 6caf75e7f..63784789b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -39,7 +39,8 @@
     android:icon="@mipmap/ic_launcher"
     android:label="@string/app_name"
     android:supportsRtl="true"
-    android:theme="@style/Theme.SplashTheme">
+    android:theme="@style/Theme.SplashTheme"
+    android:usesCleartextTraffic="true">
 
     <meta-data
       android:name="WindowManagerPreference:FreeformWindowSize"
@@ -112,6 +113,18 @@
       android:exported="false"
       android:label="@string/label_info_certificate"
       android:windowSoftInputMode="adjustResize" />
+    <activity
+      android:name="de.kuschku.quasseldroid.ui.info.message.MessageInfoActivity"
+      android:exported="true"
+      android:label="@string/label_info_message"
+      android:windowSoftInputMode="adjustResize">
+      <intent-filter>
+        <action android:name="android.intent.action.MAIN" />
+        <action android:name="android.intent.action.VIEW" />
+
+        <category android:name="android.intent.category.LAUNCHER" />
+      </intent-filter>
+    </activity>
 
     <!-- Core Settings -->
     <activity
diff --git a/app/src/main/java/de/kuschku/quasseldroid/dagger/ActivityModule.kt b/app/src/main/java/de/kuschku/quasseldroid/dagger/ActivityModule.kt
index 3feea1e69..7427678b6 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/dagger/ActivityModule.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/dagger/ActivityModule.kt
@@ -77,6 +77,8 @@ import de.kuschku.quasseldroid.ui.info.channellist.ChannelListActivity
 import de.kuschku.quasseldroid.ui.info.channellist.ChannelListFragmentProvider
 import de.kuschku.quasseldroid.ui.info.core.CoreInfoActivity
 import de.kuschku.quasseldroid.ui.info.core.CoreInfoFragmentProvider
+import de.kuschku.quasseldroid.ui.info.message.MessageInfoActivity
+import de.kuschku.quasseldroid.ui.info.message.MessageInfoFragmentProvider
 import de.kuschku.quasseldroid.ui.info.topic.TopicActivity
 import de.kuschku.quasseldroid.ui.info.user.UserInfoActivity
 import de.kuschku.quasseldroid.ui.info.user.UserInfoFragmentProvider
@@ -123,6 +125,10 @@ abstract class ActivityModule {
   @ContributesAndroidInjector(modules = [CertificateInfoFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class])
   abstract fun bindCertificateInfoActivity(): CertificateInfoActivity
 
+  @ActivityScope
+  @ContributesAndroidInjector(modules = [MessageInfoFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class])
+  abstract fun bindMessageInfoActivity(): MessageInfoActivity
+
   // Client Settings
 
   @ActivityScope
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageAdapter.kt
index bbee9923a..52a07dd04 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageAdapter.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageAdapter.kt
@@ -31,9 +31,11 @@ import androidx.recyclerview.widget.DiffUtil
 import androidx.recyclerview.widget.RecyclerView
 import butterknife.BindView
 import butterknife.ButterKnife
+import com.bumptech.glide.load.engine.DiskCacheStrategy
 import de.kuschku.libquassel.protocol.Message_Flag
 import de.kuschku.libquassel.protocol.Message_Type
 import de.kuschku.libquassel.util.flag.hasFlag
+import de.kuschku.quasseldroid.GlideApp
 import de.kuschku.quasseldroid.R
 import de.kuschku.quasseldroid.persistence.models.MessageData
 import de.kuschku.quasseldroid.settings.MessageSettings
@@ -42,6 +44,7 @@ import de.kuschku.quasseldroid.util.helper.loadAvatars
 import de.kuschku.quasseldroid.util.helper.visibleIf
 import de.kuschku.quasseldroid.util.ui.BetterLinkMovementMethod
 import de.kuschku.quasseldroid.util.ui.DoubleClickHelper
+import de.kuschku.quasseldroid.util.ui.view.MessageAttachmentView
 import de.kuschku.quasseldroid.viewmodel.data.FormattedMessage
 import javax.inject.Inject
 
@@ -225,6 +228,10 @@ class MessageAdapter @Inject constructor(
     @JvmField
     var combined: TextView? = null
 
+    @BindView(R.id.attachment)
+    @JvmField
+    var attachment: MessageAttachmentView? = null
+
     private var message: FormattedMessage? = null
     private var original: MessageData? = null
 
@@ -278,6 +285,46 @@ class MessageAdapter @Inject constructor(
       content?.text = message.content
       combined?.text = message.combined
 
+      attachment?.visibleIf(message.attachment != null)
+      attachment?.post {
+        attachment?.apply {
+          val attachment = message.attachment
+          reinitViews()
+
+          if (attachment != null) {
+            setLink(attachment.fromUrl)
+            setColor(attachment.color)
+            GlideApp.with(itemView)
+              .load(attachment.authorIcon)
+              .diskCacheStrategy(DiskCacheStrategy.ALL)
+              .into(authorIconTarget)
+            setAuthor(attachment.authorName)
+            setTitle(attachment.title)
+            setDescription(attachment.text)
+            if (false) {
+              GlideApp.with(itemView)
+                .clear(thumbnailTarget)
+              GlideApp.with(itemView)
+                .load(attachment.imageUrl)
+                .diskCacheStrategy(DiskCacheStrategy.ALL)
+                .into(previewTarget)
+            } else {
+              GlideApp.with(itemView)
+                .clear(previewTarget)
+              GlideApp.with(itemView)
+                .load(attachment.imageUrl)
+                .diskCacheStrategy(DiskCacheStrategy.ALL)
+                .into(thumbnailTarget)
+            }
+            GlideApp.with(itemView)
+              .load(attachment.serviceIcon)
+              .diskCacheStrategy(DiskCacheStrategy.ALL)
+              .into(serviceIconTarget)
+            setService(attachment.serviceName)
+          }
+        }
+      }
+
       this.messageContainer?.isSelected = message.isSelected
 
       if (hasDayChange) daychange?.text = message.dayChange
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageListFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageListFragment.kt
index ed05bd91a..6d303ae9b 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageListFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageListFragment.kt
@@ -65,6 +65,7 @@ import de.kuschku.quasseldroid.settings.AutoCompleteSettings
 import de.kuschku.quasseldroid.settings.BacklogSettings
 import de.kuschku.quasseldroid.settings.MessageSettings
 import de.kuschku.quasseldroid.ui.chat.ChatActivity
+import de.kuschku.quasseldroid.ui.info.message.MessageInfoActivity
 import de.kuschku.quasseldroid.ui.info.user.UserInfoActivity
 import de.kuschku.quasseldroid.util.Patterns
 import de.kuschku.quasseldroid.util.avatars.AvatarHelper
@@ -122,7 +123,16 @@ class MessageListFragment : ServiceBoundFragment() {
 
   private val actionModeCallback = object : ActionMode.Callback {
     override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?) = when (item?.itemId) {
-      R.id.action_user_info -> {
+      R.id.action_message_info -> {
+        viewModel.selectedMessages.value.values.firstOrNull()?.let { msg ->
+          MessageInfoActivity.launch(
+            requireContext(),
+            messageId = msg.original.messageId
+          )
+          true
+        } ?: false
+      }
+      R.id.action_user_info    -> {
         viewModel.selectedMessages.value.values.firstOrNull()?.let { msg ->
           viewModel.session.value?.orNull()?.bufferSyncer?.let { bufferSyncer ->
             viewModel.bufferData.value?.info?.let(BufferInfo::networkId)?.let { networkId ->
@@ -143,7 +153,7 @@ class MessageListFragment : ServiceBoundFragment() {
           true
         } ?: false
       }
-      R.id.action_copy      -> {
+      R.id.action_copy         -> {
         val builder = SpannableStringBuilder()
         viewModel.selectedMessages.value.values.asSequence().sortedBy {
           it.original.messageId
@@ -172,7 +182,7 @@ class MessageListFragment : ServiceBoundFragment() {
         actionMode?.finish()
         true
       }
-      R.id.action_share     -> {
+      R.id.action_share        -> {
         val builder = SpannableStringBuilder()
         viewModel.selectedMessages.value.values.asSequence().sortedBy {
           it.original.messageId
@@ -207,7 +217,7 @@ class MessageListFragment : ServiceBoundFragment() {
         actionMode?.finish()
         true
       }
-      else                  -> false
+      else                     -> false
     }
 
     override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
@@ -250,8 +260,14 @@ class MessageListFragment : ServiceBoundFragment() {
       if (actionMode != null) {
         when (viewModel.selectedMessagesToggle(msg.original.messageId, msg)) {
           0    -> actionMode?.finish()
-          1    -> actionMode?.menu?.findItem(R.id.action_user_info)?.isVisible = true
-          else -> actionMode?.menu?.findItem(R.id.action_user_info)?.isVisible = false
+          1    -> {
+            actionMode?.menu?.findItem(R.id.action_user_info)?.isVisible = true
+            //actionMode?.menu?.findItem(R.id.action_message_info)?.isVisible = true
+          }
+          else -> {
+            actionMode?.menu?.findItem(R.id.action_user_info)?.isVisible = false
+            //actionMode?.menu?.findItem(R.id.action_message_info)?.isVisible = false
+          }
         }
       } else if (msg.hasSpoilers) {
         val value = viewModel.expandedMessages.value
@@ -267,8 +283,14 @@ class MessageListFragment : ServiceBoundFragment() {
       }
       when (viewModel.selectedMessagesToggle(msg.original.messageId, msg)) {
         0    -> actionMode?.finish()
-        1    -> actionMode?.menu?.findItem(R.id.action_user_info)?.isVisible = true
-        else -> actionMode?.menu?.findItem(R.id.action_user_info)?.isVisible = false
+        1    -> {
+          actionMode?.menu?.findItem(R.id.action_user_info)?.isVisible = true
+          actionMode?.menu?.findItem(R.id.action_message_info)?.isVisible = true
+        }
+        else -> {
+          actionMode?.menu?.findItem(R.id.action_user_info)?.isVisible = false
+          actionMode?.menu?.findItem(R.id.action_message_info)?.isVisible = false
+        }
       }
     }
     if (autoCompleteSettings.senderDoubleClick)
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/QuasselMessageRenderer.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/QuasselMessageRenderer.kt
index e1dfd0eb8..1a9edb321 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/QuasselMessageRenderer.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/QuasselMessageRenderer.kt
@@ -30,6 +30,8 @@ import android.view.Gravity
 import android.view.View
 import android.widget.FrameLayout
 import android.widget.LinearLayout
+import com.google.gson.GsonBuilder
+import com.google.gson.JsonSyntaxException
 import de.kuschku.libquassel.protocol.Message.MessageType.*
 import de.kuschku.libquassel.protocol.Message_Flag
 import de.kuschku.libquassel.protocol.Message_Type
@@ -40,7 +42,9 @@ import de.kuschku.quasseldroid.R
 import de.kuschku.quasseldroid.persistence.models.MessageData
 import de.kuschku.quasseldroid.settings.MessageSettings
 import de.kuschku.quasseldroid.util.ColorContext
+import de.kuschku.quasseldroid.util.attachment.AttachmentData
 import de.kuschku.quasseldroid.util.avatars.AvatarHelper
+import de.kuschku.quasseldroid.util.helper.fromJson
 import de.kuschku.quasseldroid.util.helper.styledAttributes
 import de.kuschku.quasseldroid.util.helper.visibleIf
 import de.kuschku.quasseldroid.util.irc.format.ContentFormatter
@@ -103,6 +107,8 @@ class QuasselMessageRenderer @Inject constructor(
 
   private val zoneId = ZoneId.systemDefault()
 
+  private val gson = GsonBuilder().setLenient().create()
+
   override fun layout(type: Message_Type?,
                       hasHighlight: Boolean,
                       isFollowUp: Boolean,
@@ -244,6 +250,13 @@ class QuasselMessageRenderer @Inject constructor(
     val self = message.content.flag.hasFlag(Message_Flag.Self)
     val highlight = message.content.flag.hasFlag(Message_Flag.Highlight)
     val monochromeForeground = highlight && monochromeHighlights
+
+    val parsedAttachment: AttachmentData? = try {
+      gson.fromJson<AttachmentData>(message.content.attachments)
+    } catch (ignored: JsonSyntaxException) {
+      null
+    }
+
     return when (message.content.type.enabledValues().firstOrNull()) {
       Message_Type.Plain        -> {
         val realName = ircFormatDeserializer.formatString(message.content.realName,
@@ -292,7 +305,8 @@ class QuasselMessageRenderer @Inject constructor(
           isMarkerLine = message.isMarkerLine,
           isExpanded = message.isExpanded,
           isSelected = message.isSelected,
-          hasSpoilers = hasSpoilers
+          hasSpoilers = hasSpoilers,
+          attachment = parsedAttachment
         )
       }
       Message_Type.Action       -> {
@@ -330,7 +344,8 @@ class QuasselMessageRenderer @Inject constructor(
           isMarkerLine = message.isMarkerLine,
           isExpanded = message.isExpanded,
           isSelected = message.isSelected,
-          hasSpoilers = hasSpoilers
+          hasSpoilers = hasSpoilers,
+          attachment = parsedAttachment
         )
       }
       Message_Type.Notice       -> {
@@ -352,7 +367,8 @@ class QuasselMessageRenderer @Inject constructor(
           isMarkerLine = message.isMarkerLine,
           isExpanded = message.isExpanded,
           isSelected = message.isSelected,
-          hasSpoilers = hasSpoilers
+          hasSpoilers = hasSpoilers,
+          attachment = parsedAttachment
         )
       }
       Message_Type.Nick         -> {
@@ -395,7 +411,8 @@ class QuasselMessageRenderer @Inject constructor(
           isMarkerLine = message.isMarkerLine,
           isExpanded = message.isExpanded,
           isSelected = message.isSelected,
-          hasSpoilers = false
+          hasSpoilers = false,
+          attachment = parsedAttachment
         )
       }
       Message_Type.Mode         -> FormattedMessage(
@@ -412,7 +429,8 @@ class QuasselMessageRenderer @Inject constructor(
         isMarkerLine = message.isMarkerLine,
         isExpanded = message.isExpanded,
         isSelected = message.isSelected,
-        hasSpoilers = false
+        hasSpoilers = false,
+        attachment = parsedAttachment
       )
       Message_Type.Join         -> FormattedMessage(
         original = message.content,
@@ -433,7 +451,8 @@ class QuasselMessageRenderer @Inject constructor(
         isMarkerLine = message.isMarkerLine,
         isExpanded = message.isExpanded,
         isSelected = message.isSelected,
-        hasSpoilers = false
+        hasSpoilers = false,
+        attachment = parsedAttachment
       )
       Message_Type.Part         -> {
         val (content, hasSpoilers) = if (message.content.content.isBlank()) {
@@ -480,7 +499,8 @@ class QuasselMessageRenderer @Inject constructor(
           isMarkerLine = message.isMarkerLine,
           isExpanded = message.isExpanded,
           isSelected = message.isSelected,
-          hasSpoilers = hasSpoilers
+          hasSpoilers = hasSpoilers,
+          attachment = parsedAttachment
         )
       }
       Message_Type.Quit         -> {
@@ -528,7 +548,8 @@ class QuasselMessageRenderer @Inject constructor(
           isMarkerLine = message.isMarkerLine,
           isExpanded = message.isExpanded,
           isSelected = message.isSelected,
-          hasSpoilers = hasSpoilers
+          hasSpoilers = hasSpoilers,
+          attachment = parsedAttachment
         )
       }
       Message_Type.Kick         -> {
@@ -575,7 +596,8 @@ class QuasselMessageRenderer @Inject constructor(
           isMarkerLine = message.isMarkerLine,
           isExpanded = message.isExpanded,
           isSelected = message.isSelected,
-          hasSpoilers = hasSpoilers
+          hasSpoilers = hasSpoilers,
+          attachment = parsedAttachment
         )
       }
       Message_Type.Kill         -> {
@@ -622,7 +644,8 @@ class QuasselMessageRenderer @Inject constructor(
           isMarkerLine = message.isMarkerLine,
           isExpanded = message.isExpanded,
           isSelected = message.isSelected,
-          hasSpoilers = hasSpoilers
+          hasSpoilers = hasSpoilers,
+          attachment = parsedAttachment
         )
       }
       Message_Type.NetsplitJoin -> {
@@ -649,7 +672,8 @@ class QuasselMessageRenderer @Inject constructor(
           isMarkerLine = message.isMarkerLine,
           isExpanded = message.isExpanded,
           isSelected = message.isSelected,
-          hasSpoilers = false
+          hasSpoilers = false,
+          attachment = parsedAttachment
         )
       }
       Message_Type.NetsplitQuit -> {
@@ -676,7 +700,8 @@ class QuasselMessageRenderer @Inject constructor(
           isMarkerLine = message.isMarkerLine,
           isExpanded = message.isExpanded,
           isSelected = message.isSelected,
-          hasSpoilers = false
+          hasSpoilers = false,
+          attachment = parsedAttachment
         )
       }
       Message_Type.Server,
@@ -695,7 +720,8 @@ class QuasselMessageRenderer @Inject constructor(
           isMarkerLine = message.isMarkerLine,
           isExpanded = message.isExpanded,
           isSelected = message.isSelected,
-          hasSpoilers = hasSpoilers
+          hasSpoilers = hasSpoilers,
+          attachment = parsedAttachment
         )
       }
       Message_Type.Topic        -> {
@@ -712,7 +738,8 @@ class QuasselMessageRenderer @Inject constructor(
           isMarkerLine = message.isMarkerLine,
           isExpanded = message.isExpanded,
           isSelected = message.isSelected,
-          hasSpoilers = hasSpoilers
+          hasSpoilers = hasSpoilers,
+          attachment = parsedAttachment
         )
       }
       Message_Type.DayChange    -> FormattedMessage(
@@ -724,7 +751,8 @@ class QuasselMessageRenderer @Inject constructor(
         isMarkerLine = false,
         isExpanded = false,
         isSelected = false,
-        hasSpoilers = false
+        hasSpoilers = false,
+        attachment = parsedAttachment
       )
       Message_Type.Invite       -> {
         val (content, hasSpoilers) = contentFormatter.formatContent(message.content.content,
@@ -740,7 +768,8 @@ class QuasselMessageRenderer @Inject constructor(
           isMarkerLine = message.isMarkerLine,
           isExpanded = message.isExpanded,
           isSelected = message.isSelected,
-          hasSpoilers = hasSpoilers
+          hasSpoilers = hasSpoilers,
+          attachment = parsedAttachment
         )
       }
       else                      -> FormattedMessage(
@@ -763,13 +792,15 @@ class QuasselMessageRenderer @Inject constructor(
         isMarkerLine = message.isMarkerLine,
         isExpanded = message.isExpanded,
         isSelected = message.isSelected,
-        hasSpoilers = false
+        hasSpoilers = false,
+        attachment = parsedAttachment
       )
     }
   }
 
-  private fun formatDayChange(
-    message: DisplayMessage) =
-    if (message.hasDayChange) dateFormatter.format(message.content.time.atZone(zoneId).truncatedTo(
-      ChronoUnit.DAYS)) else null
+  private fun formatDayChange(message: DisplayMessage) =
+    if (message.hasDayChange)
+      dateFormatter.format(message.content.time.atZone(zoneId).truncatedTo(ChronoUnit.DAYS))
+    else
+      null
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/info/message/MessageAttachmentAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/info/message/MessageAttachmentAdapter.kt
new file mode 100644
index 000000000..8551ee4e6
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/info/message/MessageAttachmentAdapter.kt
@@ -0,0 +1,103 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2019 Janne Koschinski
+ * Copyright (c) 2019 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * 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 de.kuschku.quasseldroid.ui.info.message
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import butterknife.BindView
+import butterknife.ButterKnife
+import com.bumptech.glide.load.engine.DiskCacheStrategy
+import de.kuschku.quasseldroid.GlideApp
+import de.kuschku.quasseldroid.R
+import de.kuschku.quasseldroid.util.attachment.AttachmentData
+import de.kuschku.quasseldroid.util.ui.view.MessageAttachmentView
+
+class MessageAttachmentAdapter(private val showLarge: Boolean) :
+  ListAdapter<AttachmentData, MessageAttachmentAdapter.MessageAttachmentViewHolder>(
+    object : DiffUtil.ItemCallback<AttachmentData>() {
+      override fun areItemsTheSame(oldItem: AttachmentData, newItem: AttachmentData) =
+        oldItem.fromUrl == newItem.fromUrl
+
+      override fun areContentsTheSame(oldItem: AttachmentData, newItem: AttachmentData) =
+        oldItem == newItem
+    }
+  ) {
+  override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageAttachmentViewHolder {
+    return MessageAttachmentViewHolder(
+      LayoutInflater.from(parent.context)
+        .inflate(R.layout.widget_message_attachment_item, parent, false),
+      showLarge
+    )
+  }
+
+  override fun onBindViewHolder(holder: MessageAttachmentViewHolder, position: Int) {
+    holder.bind(getItem(position))
+  }
+
+
+  class MessageAttachmentViewHolder(itemView: View, private val showLarge: Boolean) :
+    RecyclerView.ViewHolder(itemView) {
+    @BindView(R.id.view)
+    lateinit var attachmentView: MessageAttachmentView
+
+    init {
+      ButterKnife.bind(this, itemView)
+    }
+
+    fun bind(attachment: AttachmentData) {
+      attachmentView.reinitViews()
+
+      attachmentView.setLink(attachment.fromUrl)
+      attachmentView.setColor(attachment.color)
+      GlideApp.with(itemView)
+        .load(attachment.authorIcon)
+        .diskCacheStrategy(DiskCacheStrategy.ALL)
+        .into(attachmentView.authorIconTarget)
+      attachmentView.setAuthor(attachment.authorName)
+      //attachmentView.setAuthorLink(attachment.author_link)
+      attachmentView.setTitle(attachment.title)
+      attachmentView.setDescription(attachment.text)
+      if (showLarge) {
+        GlideApp.with(itemView)
+          .clear(attachmentView.thumbnailTarget)
+        GlideApp.with(itemView)
+          .load(attachment.imageUrl)
+          .diskCacheStrategy(DiskCacheStrategy.ALL)
+          .into(attachmentView.previewTarget)
+      } else {
+        GlideApp.with(itemView)
+          .clear(attachmentView.previewTarget)
+        GlideApp.with(itemView)
+          .load(attachment.imageUrl)
+          .diskCacheStrategy(DiskCacheStrategy.ALL)
+          .into(attachmentView.thumbnailTarget)
+      }
+      GlideApp.with(itemView)
+        .load(attachment.serviceIcon)
+        .diskCacheStrategy(DiskCacheStrategy.ALL)
+        .into(attachmentView.serviceIconTarget)
+      attachmentView.setService(attachment.serviceName)
+    }
+  }
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/info/message/MessageInfoActivity.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/info/message/MessageInfoActivity.kt
new file mode 100644
index 000000000..8e793f0f1
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/info/message/MessageInfoActivity.kt
@@ -0,0 +1,41 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2019 Janne Koschinski
+ * Copyright (c) 2019 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * 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 de.kuschku.quasseldroid.ui.info.message
+
+import android.content.Context
+import android.content.Intent
+import de.kuschku.libquassel.protocol.MsgId
+import de.kuschku.quasseldroid.util.ui.settings.ServiceBoundSettingsActivity
+
+class MessageInfoActivity : ServiceBoundSettingsActivity(MessageInfoFragment()) {
+  companion object {
+    fun launch(
+      context: Context,
+      messageId: MsgId
+    ) = context.startActivity(intent(context, messageId))
+
+    fun intent(
+      context: Context,
+      messageId: MsgId
+    ) = Intent(context, MessageInfoActivity::class.java).apply {
+      putExtra("messageId", messageId.id)
+    }
+  }
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/info/message/MessageInfoFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/info/message/MessageInfoFragment.kt
new file mode 100644
index 000000000..cf8df1fb7
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/info/message/MessageInfoFragment.kt
@@ -0,0 +1,124 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2019 Janne Koschinski
+ * Copyright (c) 2019 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * 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 de.kuschku.quasseldroid.ui.info.message
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.lifecycle.Observer
+import androidx.recyclerview.widget.DefaultItemAnimator
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import butterknife.BindView
+import butterknife.ButterKnife
+import com.google.gson.Gson
+import de.kuschku.libquassel.protocol.MsgId
+import de.kuschku.libquassel.util.Optional
+import de.kuschku.quasseldroid.R
+import de.kuschku.quasseldroid.persistence.dao.find
+import de.kuschku.quasseldroid.persistence.db.QuasselDatabase
+import de.kuschku.quasseldroid.util.attachment.AttachmentData
+import de.kuschku.quasseldroid.util.attachments.AttachmentApi
+import de.kuschku.quasseldroid.util.helper.combineLatest
+import de.kuschku.quasseldroid.util.helper.toLiveData
+import de.kuschku.quasseldroid.util.service.ServiceBoundFragment
+import de.kuschku.quasseldroid.util.ui.BetterLinkMovementMethod
+import de.kuschku.quasseldroid.util.ui.LinkLongClickMenuHelper
+import retrofit2.Retrofit
+import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
+import retrofit2.converter.gson.GsonConverterFactory
+import javax.inject.Inject
+
+class MessageInfoFragment : ServiceBoundFragment() {
+  @BindView(R.id.list)
+  lateinit var list: RecyclerView
+
+  @Inject
+  lateinit var database: QuasselDatabase
+
+  override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
+                            savedInstanceState: Bundle?): View? {
+    val view = inflater.inflate(R.layout.info_message, container, false)
+    ButterKnife.bind(this, view)
+
+    val adapter = MessageAttachmentAdapter(true)
+    list.adapter = adapter
+    list.layoutManager = LinearLayoutManager(list.context)
+    list.itemAnimator = DefaultItemAnimator()
+
+    viewModel.session.toLiveData().observe(this, Observer {
+      runInBackground {
+        val movementMethod = BetterLinkMovementMethod.newInstance()
+        movementMethod.setOnLinkLongClickListener(LinkLongClickMenuHelper())
+
+        val messageId = MsgId(arguments?.getLong("messageId") ?: -1)
+        val message = database.message().find(messageId)
+
+        val retrofit = Retrofit.Builder()
+          .baseUrl("http://192.168.178.29:8080/")
+          .addConverterFactory(GsonConverterFactory.create())
+          .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
+          .build()
+
+        val api = retrofit.create(AttachmentApi::class.java)
+
+        val gson = Gson()
+
+        val ircCloudEmbeds = listOf(
+          "{\"ts\":1448400805,\"title_link\":\"https://medium.com/slack-developer-blog/everything-you-ever-wanted-to-know-about-unfurling-but-were-afraid-to-ask-or-how-to-make-your-e64b4bb9254\",\"title\":\"Everything you ever wanted to know about unfurling but were afraid to ask /or/ How to make your…\",\"text\":\"Let’s start with the most obvious question first. This is what an “unfurl” is:\",\"service_name\":\"Medium\",\"service_icon\":\"https://cdn-images-1.medium.com/fit/c/304/304/1*a1O3xhOq8KWSibZF6Ze5xQ.png\",\"image_width\":170,\"image_url\":\"https://cdn-images-1.medium.com/max/1200/1*QOMaDLcO8rExD0ctBV3BWg.png\",\"image_height\":250,\"image_bytes\":695475,\"from_url\":\"https://medium.com/slack-developer-blog/everything-you-ever-wanted-to-know-about-unfurling-but-were-afraid-to-ask-or-how-to-make-your-e64b4bb9254\",\"fields\":[{\"value\":\"10 min read\",\"title\":\"Reading time\",\"short\":true}]}",
+          "{\"title_link\":\"https://www.youtube.com/watch?v=_Cd9FYO9Rh4\",\"title\":\"Scorpions   Berlin Philharmonic Orchestra   Rock You Like a Hurricane\",\"service_url\":\"https://www.youtube.com/\",\"service_name\":\"YouTube\",\"service_icon\":\"https://a.slack-edge.com/2089/img/unfurl_icons/youtube.png\",\"from_url\":\"https://www.youtube.com/watch?v=_Cd9FYO9Rh4\",\"author_name\":\"Nathan Allen\",\"author_link\":\"https://www.youtube.com/user/27caboose\"}",
+          "{\"title_link\":\"http://www.kn-online.de/Nachrichten/Panorama/Lehrerin-fuehrt-Netflix-Experiment-durch-das-Ergebnis-erschreckt\",\"title\":\"Lehrerin führt Netflix-Experiment durch – das Ergebnis erschreckt\",\"text\":\"Rebecca Schiller aus Potsdam unterrichtet am Marie-Curie-Gymnasium im Havelland. Als „Frau Lehrerin“ ist sie auf Twitter eine kleine Berühmtheit – vor allem, seit sie dort eine unkonventionelle Unterrichtsmethode veröffentlicht hat.\",\"service_name\":\"KN - Kieler Nachrichten\",\"service_icon\":\"http://www.kn-online.de/bundles/molasset/images/sites/desktop/kn/apple-touch-icon.png\",\"image_width\":500,\"image_url\":\"http://www.kn-online.de/var/storage/images/rnd/nachrichten/panorama/uebersicht/lehrerin-fuehrt-netflix-experiment-durch-das-ergebnis-erschreckt/712106427-8-ger-DE/Lehrerin-fuehrt-Netflix-Experiment-durch-das-Ergebnis-erschreckt_reference_2_1.jpg\",\"image_height\":250,\"image_bytes\":65450,\"from_url\":\"http://www.kn-online.de/Nachrichten/Panorama/Lehrerin-fuehrt-Netflix-Experiment-durch-das-Ergebnis-erschreckt\"}"
+        ).map {
+          gson.fromJson(it, AttachmentData::class.java)
+        }
+
+        val urls = listOf(
+          "https://quasseldroid.info/",
+          "https://www.youtube.com/watch?v=IfXMN3VhikA",
+          "https://twitter.com/dw_politik/status/1092872739445104640",
+          "https://soundcloud.com/kevin-manthei/sto-ds9-main-title",
+          "https://twitter.com/raketenlurch/status/1093991675209416704",
+          "https://arxius.io/i/25287151",
+          "https://i.imgur.com/W8DjyWk.jpg",
+          "https://imgur.com/W8DjyWk",
+          "https://rp-online.de/panorama/deutschland/buchenbach-transporter-hindert-krankenwagen-mit-kind-an-bord-minutenlang-am-ueberholen_aid-35758071",
+          "http://m.kn-online.de/Nachrichten/Panorama/Lehrerin-fuehrt-Netflix-Experiment-durch-das-Ergebnis-erschreckt",
+          "https://media.ccc.de/v/35c3-9904-the_social_credit_system",
+          "https://medium.com/slack-developer-blog/everything-you-ever-wanted-to-know-about-unfurling-but-were-afraid-to-ask-or-how-to-make-your-e64b4bb9254"
+        )
+
+        activity?.runOnUiThread {
+          combineLatest(urls.map {
+            api.retrieve(it)
+              .map { Optional.of(it) }
+              .onErrorReturnItem(Optional.empty())
+          }).map {
+            it.mapNotNull(Optional<AttachmentData>::orNull)
+          }.toLiveData().observe(this, Observer {
+            adapter.submitList(ircCloudEmbeds + it)
+          })
+        }
+      }
+    })
+
+    return view
+  }
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/info/message/MessageInfoFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/info/message/MessageInfoFragmentProvider.kt
new file mode 100644
index 000000000..f9ad726af
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/info/message/MessageInfoFragmentProvider.kt
@@ -0,0 +1,34 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2019 Janne Koschinski
+ * Copyright (c) 2019 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * 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 de.kuschku.quasseldroid.ui.info.message
+
+import androidx.fragment.app.FragmentActivity
+import dagger.Binds
+import dagger.Module
+import dagger.android.ContributesAndroidInjector
+
+@Module
+abstract class MessageInfoFragmentProvider {
+  @Binds
+  abstract fun bindFragmentActivity(activity: MessageInfoActivity): FragmentActivity
+
+  @ContributesAndroidInjector
+  abstract fun bindMessageInfoFragment(): MessageInfoFragment
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/attachments/AttachmentApi.kt b/app/src/main/java/de/kuschku/quasseldroid/util/attachments/AttachmentApi.kt
new file mode 100644
index 000000000..02f921401
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/attachments/AttachmentApi.kt
@@ -0,0 +1,30 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2019 Janne Koschinski
+ * Copyright (c) 2019 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * 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 de.kuschku.quasseldroid.util.attachments
+
+import de.kuschku.quasseldroid.util.attachment.AttachmentData
+import io.reactivex.Observable
+import retrofit2.http.GET
+import retrofit2.http.Query
+
+interface AttachmentApi {
+  @GET("/")
+  fun retrieve(@Query("url") url: String): Observable<AttachmentData>
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/ui/view/MessageAttachmentView.kt b/app/src/main/java/de/kuschku/quasseldroid/util/ui/view/MessageAttachmentView.kt
new file mode 100644
index 000000000..567b13244
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/ui/view/MessageAttachmentView.kt
@@ -0,0 +1,211 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2019 Janne Koschinski
+ * Copyright (c) 2019 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * 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 de.kuschku.quasseldroid.util.ui.view
+
+import android.content.Context
+import android.content.Intent
+import android.graphics.Color
+import android.graphics.drawable.Drawable
+import android.net.Uri
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.FrameLayout
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.annotation.ColorInt
+import butterknife.BindView
+import butterknife.ButterKnife
+import com.bumptech.glide.request.target.CustomViewTarget
+import com.bumptech.glide.request.transition.Transition
+import de.kuschku.quasseldroid.R
+import de.kuschku.quasseldroid.util.helper.styledAttributes
+import de.kuschku.quasseldroid.util.helper.visibleIf
+
+class MessageAttachmentView : FrameLayout {
+  @BindView(R.id.attachment_color_bar)
+  lateinit var colorBar: View
+
+  @BindView(R.id.attachment_author_icon)
+  lateinit var authorIcon: ImageView
+
+  @BindView(R.id.attachment_author)
+  lateinit var author: TextView
+
+  @BindView(R.id.attachment_title)
+  lateinit var title: TextView
+
+  @BindView(R.id.attachment_description)
+  lateinit var description: TextView
+
+  @BindView(R.id.attachment_thumbnail)
+  lateinit var thumbnail: ImageView
+
+  @BindView(R.id.attachment_preview)
+  lateinit var preview: ImageView
+
+  @BindView(R.id.attachment_service_icon)
+  lateinit var serviceIcon: ImageView
+
+  @BindView(R.id.attachment_service)
+  lateinit var service: TextView
+
+  val authorIconTarget: VisibilitySettingDrawableImageViewTarget
+  val thumbnailTarget: VisibilitySettingDrawableImageViewTarget
+  val previewTarget: VisibilitySettingDrawableImageViewTarget
+  val serviceIconTarget: VisibilitySettingDrawableImageViewTarget
+
+  private var url: String? = null
+  private var authorUrl: String? = null
+
+  constructor(context: Context) :
+    this(context, null)
+
+  constructor(context: Context, attrs: AttributeSet?) :
+    this(context, attrs, 0)
+
+  constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) :
+    super(context, attrs, defStyleAttr) {
+
+    LayoutInflater.from(context).inflate(R.layout.widget_message_attachment, this, true)
+    ButterKnife.bind(this)
+
+    authorIconTarget = VisibilitySettingDrawableImageViewTarget(authorIcon)
+    thumbnailTarget = VisibilitySettingDrawableImageViewTarget(thumbnail)
+    previewTarget = VisibilitySettingDrawableImageViewTarget(preview)
+    serviceIconTarget = VisibilitySettingDrawableImageViewTarget(serviceIcon)
+
+    reinitViews()
+  }
+
+  fun reinitViews() {
+    setColor(0)
+    setAuthorIcon(null)
+    authorIcon.visibility = View.VISIBLE
+    setAuthor("")
+    setTitle("")
+    setDescription("")
+    setThumbnail(null)
+    thumbnail.visibility = View.VISIBLE
+    setPreview(null)
+    preview.visibility = View.VISIBLE
+    setServiceIcon(null)
+    serviceIcon.visibility = View.VISIBLE
+    setService("")
+  }
+
+  class VisibilitySettingDrawableImageViewTarget(view: ImageView) :
+    CustomViewTarget<ImageView, Drawable>(view) {
+    override fun onLoadFailed(errorDrawable: Drawable?) {
+      view.setImageDrawable(errorDrawable)
+      view.visibility = View.GONE
+    }
+
+    override fun onResourceCleared(placeholder: Drawable?) {
+      view.setImageDrawable(placeholder)
+      view.visibility = View.GONE
+    }
+
+    override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
+      view.setImageDrawable(resource)
+      view.visibility = View.VISIBLE
+    }
+  }
+
+  private val colorForegroundSecondary = context.theme.styledAttributes(R.attr.colorForegroundSecondary) {
+    getColor(0, 0)
+  }
+
+  fun setColor(color: String?) {
+    setColor(try {
+      Color.parseColor(color)
+    } catch (ignored: Throwable) {
+      0
+    })
+  }
+
+  fun setColor(@ColorInt color: Int) {
+    if (color != 0) {
+      colorBar.setBackgroundColor(color)
+    } else {
+      colorBar.setBackgroundColor(colorForegroundSecondary)
+    }
+  }
+
+  fun setAuthorIcon(drawable: Drawable?) {
+    authorIcon.setImageDrawable(drawable)
+  }
+
+  fun setAuthor(text: String?) {
+    author.text = text
+    author.visibleIf(!text.isNullOrBlank())
+  }
+
+  fun setAuthorLink(url: String) {
+    if (url.isNotBlank()) {
+      author.setOnClickListener {
+        context?.startActivity(Intent(Intent.ACTION_VIEW).apply {
+          data = Uri.parse(url)
+        })
+      }
+    } else {
+      author.setOnClickListener(null)
+    }
+  }
+
+  fun setTitle(text: String?) {
+    title.text = text
+    title.visibleIf(!text.isNullOrBlank())
+  }
+
+  fun setLink(url: String?) {
+    if (url.isNullOrBlank()) {
+      this.setOnClickListener(null)
+    } else {
+      this.setOnClickListener {
+        context?.startActivity(Intent(Intent.ACTION_VIEW).apply {
+          data = Uri.parse(url)
+        })
+      }
+    }
+  }
+
+  fun setDescription(text: String?) {
+    description.text = text
+    description.visibleIf(!text.isNullOrBlank())
+  }
+
+  fun setThumbnail(drawable: Drawable?) {
+    thumbnail.setImageDrawable(drawable)
+  }
+
+  fun setPreview(drawable: Drawable?) {
+    preview.setImageDrawable(drawable)
+  }
+
+  fun setServiceIcon(drawable: Drawable?) {
+    serviceIcon.setImageDrawable(drawable)
+  }
+
+  fun setService(text: String?) {
+    service.text = text
+    service.visibleIf(!text.isNullOrBlank())
+  }
+}
diff --git a/app/src/main/res/layout/info_message.xml b/app/src/main/res/layout/info_message.xml
new file mode 100644
index 000000000..04882f08e
--- /dev/null
+++ b/app/src/main/res/layout/info_message.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Quasseldroid - Quassel client for Android
+
+  Copyright (c) 2019 Janne Koschinski
+  Copyright (c) 2019 The Quassel Project
+
+  This program is free software: you can redistribute it and/or modify it
+  under the terms of the GNU General Public License version 3 as published
+  by the Free Software Foundation.
+
+  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/>.
+  -->
+
+<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
+  android:id="@+id/list"
+  android:layout_width="match_parent"
+  android:layout_height="match_parent" />
diff --git a/app/src/main/res/layout/widget_chatmessage_action.xml b/app/src/main/res/layout/widget_chatmessage_action.xml
index 6247fb6d2..3706c5fee 100644
--- a/app/src/main/res/layout/widget_chatmessage_action.xml
+++ b/app/src/main/res/layout/widget_chatmessage_action.xml
@@ -73,40 +73,51 @@
     <LinearLayout
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
-      android:layout_gravity="center_vertical|fill_horizontal"
-      android:orientation="horizontal">
+      android:orientation="vertical">
 
       <LinearLayout
-        android:layout_width="0dip"
+        android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:layout_weight="1"
-        android:orientation="vertical">
+        android:layout_gravity="center_vertical|fill_horizontal"
+        android:orientation="horizontal">
 
-        <de.kuschku.quasseldroid.util.ui.view.RipplePassthroughTextView
-          android:id="@+id/combined"
-          style="@style/Widget.RtlConformTextView"
-          android:layout_width="match_parent"
+        <LinearLayout
+          android:layout_width="0dip"
           android:layout_height="wrap_content"
-          android:textColor="?attr/colorForegroundAction"
+          android:layout_weight="1"
+          android:orientation="vertical">
+
+          <de.kuschku.quasseldroid.util.ui.view.RipplePassthroughTextView
+            android:id="@+id/combined"
+            style="@style/Widget.RtlConformTextView"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:textColor="?attr/colorForegroundAction"
+            android:textStyle="italic"
+            tools:text="@sample/messages.json/data/message" />
+
+        </LinearLayout>
+
+        <TextView
+          android:id="@+id/time_right"
+          android:layout_width="wrap_content"
+          android:layout_height="wrap_content"
+          android:layout_gravity="top"
+          android:layout_marginStart="@dimen/message_horizontal"
+          android:layout_marginLeft="@dimen/message_horizontal"
+          android:textColor="?attr/colorForegroundSecondary"
           android:textStyle="italic"
-          tools:text="@sample/messages.json/data/message" />
-
+          android:visibility="gone"
+          tools:ignore="SmallSp"
+          tools:text="@sample/messages.json/data/time"
+          tools:textSize="11.9sp"
+          tools:visibility="visible" />
       </LinearLayout>
 
-      <TextView
-        android:id="@+id/time_right"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_gravity="top"
-        android:layout_marginStart="@dimen/message_horizontal"
-        android:layout_marginLeft="@dimen/message_horizontal"
-        android:textColor="?attr/colorForegroundSecondary"
-        android:textStyle="italic"
-        android:visibility="gone"
-        tools:ignore="SmallSp"
-        tools:text="@sample/messages.json/data/time"
-        tools:textSize="11.9sp"
-        tools:visibility="visible" />
+      <de.kuschku.quasseldroid.util.ui.view.MessageAttachmentView
+        android:id="@+id/attachment"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" />
     </LinearLayout>
   </LinearLayout>
 
diff --git a/app/src/main/res/layout/widget_chatmessage_info.xml b/app/src/main/res/layout/widget_chatmessage_info.xml
index 343ba2e65..53dabcb69 100644
--- a/app/src/main/res/layout/widget_chatmessage_info.xml
+++ b/app/src/main/res/layout/widget_chatmessage_info.xml
@@ -59,29 +59,47 @@
       android:layout_marginRight="@dimen/message_horizontal"
       android:visibility="gone" />
 
-    <de.kuschku.quasseldroid.util.ui.view.RipplePassthroughTextView
-      android:id="@+id/combined"
-      style="@style/Widget.RtlConformTextView"
-      android:layout_width="0dip"
+    <LinearLayout
+      android:layout_width="match_parent"
       android:layout_height="wrap_content"
-      android:layout_weight="1"
-      android:textColor="?attr/colorForegroundSecondary"
-      android:textStyle="italic"
-      tools:text="@sample/messages.json/data/message" />
+      android:orientation="vertical">
 
-    <TextView
-      android:id="@+id/time_right"
-      android:layout_width="wrap_content"
-      android:layout_height="wrap_content"
-      android:layout_gravity="top"
-      android:layout_marginStart="@dimen/message_horizontal"
-      android:layout_marginLeft="@dimen/message_horizontal"
-      android:textColor="?attr/colorForegroundSecondary"
-      android:textStyle="italic"
-      android:visibility="gone"
-      tools:ignore="SmallSp"
-      tools:text="@sample/messages.json/data/time"
-      tools:textSize="11.9sp"
-      tools:visibility="visible" />
+      <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+
+        <de.kuschku.quasseldroid.util.ui.view.RipplePassthroughTextView
+          android:id="@+id/combined"
+          style="@style/Widget.RtlConformTextView"
+          android:layout_width="0dip"
+          android:layout_height="wrap_content"
+          android:layout_weight="1"
+          android:textColor="?attr/colorForegroundSecondary"
+          android:textStyle="italic"
+          tools:text="@sample/messages.json/data/message" />
+
+        <TextView
+          android:id="@+id/time_right"
+          android:layout_width="wrap_content"
+          android:layout_height="wrap_content"
+          android:layout_gravity="top"
+          android:layout_marginStart="@dimen/message_horizontal"
+          android:layout_marginLeft="@dimen/message_horizontal"
+          android:textColor="?attr/colorForegroundSecondary"
+          android:textStyle="italic"
+          android:visibility="gone"
+          tools:ignore="SmallSp"
+          tools:text="@sample/messages.json/data/time"
+          tools:textSize="11.9sp"
+          tools:visibility="visible" />
+
+      </LinearLayout>
+
+      <de.kuschku.quasseldroid.util.ui.view.MessageAttachmentView
+        android:id="@+id/attachment"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" />
+    </LinearLayout>
   </LinearLayout>
 </LinearLayout>
diff --git a/app/src/main/res/layout/widget_chatmessage_notice.xml b/app/src/main/res/layout/widget_chatmessage_notice.xml
index 86b8dad5e..7b62f418d 100644
--- a/app/src/main/res/layout/widget_chatmessage_notice.xml
+++ b/app/src/main/res/layout/widget_chatmessage_notice.xml
@@ -59,28 +59,46 @@
       android:layout_marginRight="@dimen/message_horizontal"
       android:visibility="gone" />
 
-    <de.kuschku.quasseldroid.util.ui.view.RipplePassthroughTextView
-      android:id="@+id/combined"
-      style="@style/Widget.RtlConformTextView"
-      android:layout_width="0dip"
+    <LinearLayout
+      android:layout_width="match_parent"
       android:layout_height="wrap_content"
-      android:layout_weight="1"
-      android:textColor="?attr/colorForegroundNotice"
-      tools:text="@sample/messages.json/data/message" />
+      android:orientation="vertical">
 
-    <TextView
-      android:id="@+id/time_right"
-      android:layout_width="wrap_content"
-      android:layout_height="wrap_content"
-      android:layout_gravity="top"
-      android:layout_marginStart="@dimen/message_horizontal"
-      android:layout_marginLeft="@dimen/message_horizontal"
-      android:textColor="?attr/colorForegroundSecondary"
-      android:textStyle="italic"
-      android:visibility="gone"
-      tools:ignore="SmallSp"
-      tools:text="@sample/messages.json/data/time"
-      tools:textSize="11.9sp"
-      tools:visibility="visible" />
+      <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+
+        <de.kuschku.quasseldroid.util.ui.view.RipplePassthroughTextView
+          android:id="@+id/combined"
+          style="@style/Widget.RtlConformTextView"
+          android:layout_width="0dip"
+          android:layout_height="wrap_content"
+          android:layout_weight="1"
+          android:textColor="?attr/colorForegroundNotice"
+          tools:text="@sample/messages.json/data/message" />
+
+        <TextView
+          android:id="@+id/time_right"
+          android:layout_width="wrap_content"
+          android:layout_height="wrap_content"
+          android:layout_gravity="top"
+          android:layout_marginStart="@dimen/message_horizontal"
+          android:layout_marginLeft="@dimen/message_horizontal"
+          android:textColor="?attr/colorForegroundSecondary"
+          android:textStyle="italic"
+          android:visibility="gone"
+          tools:ignore="SmallSp"
+          tools:text="@sample/messages.json/data/time"
+          tools:textSize="11.9sp"
+          tools:visibility="visible" />
+
+      </LinearLayout>
+
+      <de.kuschku.quasseldroid.util.ui.view.MessageAttachmentView
+        android:id="@+id/attachment"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" />
+    </LinearLayout>
   </LinearLayout>
 </LinearLayout>
diff --git a/app/src/main/res/layout/widget_chatmessage_plain.xml b/app/src/main/res/layout/widget_chatmessage_plain.xml
index 3ead811a1..b6e1f9376 100644
--- a/app/src/main/res/layout/widget_chatmessage_plain.xml
+++ b/app/src/main/res/layout/widget_chatmessage_plain.xml
@@ -138,7 +138,6 @@
             android:textColor="?attr/colorForeground"
             tools:text="@sample/messages.json/data/message"
             tools:visibility="gone" />
-
         </LinearLayout>
 
         <TextView
@@ -156,6 +155,11 @@
           tools:textSize="11.9sp"
           tools:visibility="visible" />
       </LinearLayout>
+
+      <de.kuschku.quasseldroid.util.ui.view.MessageAttachmentView
+        android:id="@+id/attachment"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" />
     </LinearLayout>
   </LinearLayout>
 
diff --git a/app/src/main/res/layout/widget_message_attachment.xml b/app/src/main/res/layout/widget_message_attachment.xml
new file mode 100644
index 000000000..79418540f
--- /dev/null
+++ b/app/src/main/res/layout/widget_message_attachment.xml
@@ -0,0 +1,173 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Quasseldroid - Quassel client for Android
+
+  Copyright (c) 2019 Janne Koschinski
+  Copyright (c) 2019 The Quassel Project
+
+  This program is free software: you can redistribute it and/or modify it
+  under the terms of the GNU General Public License version 3 as published
+  by the Free Software Foundation.
+
+  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/>.
+  -->
+
+<com.google.android.material.card.MaterialCardView 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_margin="8dp"
+  app:cardBackgroundColor="?colorBackgroundCard">
+
+  <androidx.constraintlayout.widget.ConstraintLayout
+    android:id="@+id/attachment_content"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:background="?selectableItemBackground"
+    android:orientation="horizontal">
+
+    <View
+      android:id="@+id/attachment_color_bar"
+      android:layout_width="4dp"
+      android:layout_height="0dp"
+      app:layout_constraintBottom_toBottomOf="parent"
+      app:layout_constraintStart_toStartOf="parent"
+      app:layout_constraintTop_toTopOf="parent"
+      tools:background="@android:color/holo_red_dark" />
+
+    <ImageView
+      android:id="@+id/attachment_author_icon"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:layout_marginStart="8dp"
+      android:layout_marginLeft="8dp"
+      android:adjustViewBounds="true"
+      android:maxWidth="16dp"
+      android:maxHeight="16dp"
+      app:layout_constraintBottom_toBottomOf="@+id/attachment_author"
+      app:layout_constraintStart_toEndOf="@+id/attachment_color_bar"
+      app:layout_constraintTop_toTopOf="@+id/attachment_author"
+      tools:srcCompat="@tools:sample/avatars" />
+
+    <TextView
+      android:id="@+id/attachment_author"
+      android:layout_width="0dip"
+      android:layout_height="wrap_content"
+      android:layout_marginStart="8dp"
+      android:layout_marginLeft="8dp"
+      android:layout_marginTop="4dp"
+      android:layout_marginEnd="8dp"
+      android:layout_marginRight="8dp"
+      android:textColor="?colorTextSecondary"
+      app:layout_constraintEnd_toStartOf="@+id/attachment_thumbnail"
+      app:layout_constraintStart_toEndOf="@id/attachment_author_icon"
+      app:layout_constraintTop_toTopOf="parent"
+      tools:text="MrDomigruber" />
+
+    <TextView
+      android:id="@+id/attachment_title"
+      android:layout_width="0dip"
+      android:layout_height="wrap_content"
+      android:layout_marginStart="8dp"
+      android:layout_marginLeft="8dp"
+      android:layout_marginTop="4dp"
+      android:layout_marginEnd="8dp"
+      android:layout_marginRight="8dp"
+      android:textColor="?colorTextPrimary"
+      android:textStyle="bold"
+      app:layout_constraintEnd_toStartOf="@+id/attachment_thumbnail"
+      app:layout_constraintStart_toEndOf="@id/attachment_color_bar"
+      app:layout_constraintTop_toBottomOf="@+id/attachment_author"
+      tools:text="Ein sprechender Elch will meine Kreditkartennummer" />
+
+    <TextView
+      android:id="@+id/attachment_description"
+      android:layout_width="0dip"
+      android:layout_height="wrap_content"
+      android:layout_marginStart="8dp"
+      android:layout_marginLeft="8dp"
+      android:layout_marginTop="4dp"
+      android:layout_marginEnd="8dp"
+      android:layout_marginRight="8dp"
+      android:textColor="?colorTextPrimary"
+      app:layout_constraintEnd_toStartOf="@+id/attachment_thumbnail"
+      app:layout_constraintStart_toEndOf="@id/attachment_color_bar"
+      app:layout_constraintTop_toBottomOf="@id/attachment_title"
+      tools:text="Homer Simpson gibt einen Elch seine Kreditkarten nummer" />
+
+    <ImageView
+      android:id="@+id/attachment_thumbnail"
+      android:layout_width="wrap_content"
+      android:layout_height="0dp"
+      android:layout_marginTop="8dp"
+      android:layout_marginEnd="8dp"
+      android:layout_marginRight="8dp"
+      android:layout_marginBottom="8dp"
+      android:adjustViewBounds="true"
+      android:maxWidth="80dp"
+      android:scaleType="fitStart"
+      app:layout_constraintBottom_toTopOf="@+id/attachment_preview"
+      app:layout_constraintEnd_toEndOf="parent"
+      app:layout_constraintTop_toTopOf="parent"
+      app:layout_constraintVertical_bias="0.0"
+      tools:background="#ff0000"
+      tools:minHeight="60dp"
+      tools:srcCompat="@tools:sample/avatars" />
+
+    <ImageView
+      android:id="@+id/attachment_preview"
+      android:layout_width="0dip"
+      android:layout_height="wrap_content"
+      android:layout_marginStart="8dp"
+      android:layout_marginLeft="8dp"
+      android:layout_marginTop="8dp"
+      android:layout_marginEnd="8dp"
+      android:layout_marginRight="8dp"
+      android:adjustViewBounds="true"
+      app:layout_constraintEnd_toEndOf="parent"
+      app:layout_constraintStart_toEndOf="@id/attachment_color_bar"
+      app:layout_constraintTop_toBottomOf="@+id/attachment_description"
+      tools:background="#00ff00"
+      tools:minHeight="160dp" />
+
+    <ImageView
+      android:id="@+id/attachment_service_icon"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:layout_marginStart="8dp"
+      android:layout_marginLeft="8dp"
+      android:adjustViewBounds="true"
+      android:maxWidth="16dp"
+      android:maxHeight="16dp"
+      app:layout_constraintBottom_toBottomOf="@+id/attachment_service"
+      app:layout_constraintStart_toEndOf="@id/attachment_color_bar"
+      app:layout_constraintTop_toTopOf="@+id/attachment_service"
+      tools:srcCompat="@tools:sample/avatars" />
+
+    <TextView
+      android:id="@+id/attachment_service"
+      android:layout_width="0dip"
+      android:layout_height="wrap_content"
+      android:layout_marginStart="8dp"
+      android:layout_marginLeft="8dp"
+      android:layout_marginTop="4dp"
+      android:layout_marginEnd="8dp"
+      android:layout_marginRight="8dp"
+      android:layout_marginBottom="4dp"
+      android:textColor="?colorTextSecondary"
+      app:layout_constraintBottom_toBottomOf="parent"
+      app:layout_constraintEnd_toEndOf="parent"
+      app:layout_constraintStart_toEndOf="@+id/attachment_service_icon"
+      app:layout_constraintTop_toBottomOf="@+id/attachment_preview"
+      app:layout_constraintVertical_bias="0.0"
+      tools:text="YouTube" />
+
+  </androidx.constraintlayout.widget.ConstraintLayout>
+
+</com.google.android.material.card.MaterialCardView>
diff --git a/app/src/main/res/layout/widget_message_attachment_item.xml b/app/src/main/res/layout/widget_message_attachment_item.xml
new file mode 100644
index 000000000..eaf71b668
--- /dev/null
+++ b/app/src/main/res/layout/widget_message_attachment_item.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Quasseldroid - Quassel client for Android
+
+  Copyright (c) 2019 Janne Koschinski
+  Copyright (c) 2019 The Quassel Project
+
+  This program is free software: you can redistribute it and/or modify it
+  under the terms of the GNU General Public License version 3 as published
+  by the Free Software Foundation.
+
+  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/>.
+  -->
+
+<de.kuschku.quasseldroid.util.ui.view.MessageAttachmentView xmlns:android="http://schemas.android.com/apk/res/android"
+  android:id="@+id/view"
+  android:layout_width="match_parent"
+  android:layout_height="wrap_content" />
diff --git a/app/src/main/res/menu/context_messages.xml b/app/src/main/res/menu/context_messages.xml
index 0554f668e..aa27fa1e4 100644
--- a/app/src/main/res/menu/context_messages.xml
+++ b/app/src/main/res/menu/context_messages.xml
@@ -18,6 +18,11 @@
   -->
 
 <menu xmlns:android="http://schemas.android.com/apk/res/android">
+  <item
+    android:id="@+id/action_message_info"
+    android:icon="@drawable/ic_info"
+    android:title="@string/label_info_message"
+    android:visible="false" />
   <item
     android:id="@+id/action_user_info"
     android:icon="@drawable/ic_account"
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 714217941..e0dc9ae71 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -65,6 +65,7 @@
   <string name="label_info_channel">Channel Details</string>
   <string name="label_info_channellist">Channel List</string>
   <string name="label_info_core">Core Details</string>
+  <string name="label_info_message">Message Details</string>
   <string name="label_info_user">User Details</string>
   <string name="label_input_history">Input History</string>
   <string name="label_join">Join</string>
diff --git a/gradle.properties b/gradle.properties
index 9450fb27e..9b93d673b 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -33,9 +33,9 @@ android.enableD8=true
 # Enable new Android R8 Optimizer
 android.enableR8=true
 # Enable gradle build cache
-org.gradle.caching=true
+org.gradle.caching=false
 # Enable android build cache
-android.enableBuildCache=true
+android.enableBuildCache=false
 # Enable AndroidX
 android.useAndroidX=true
 android.enableJetifier=true
diff --git a/lib/src/main/java/de/kuschku/libquassel/protocol/Message.kt b/lib/src/main/java/de/kuschku/libquassel/protocol/Message.kt
index e7f2542f7..6fd06dc00 100644
--- a/lib/src/main/java/de/kuschku/libquassel/protocol/Message.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/protocol/Message.kt
@@ -34,6 +34,7 @@ data class Message(
   val senderPrefixes: String,
   val realName: String,
   val avatarUrl: String,
+  val attachments: String,
   val content: String
 ) {
   enum class MessageType(override val bit: UInt) : Flag<MessageType> {
@@ -84,6 +85,6 @@ data class Message(
 
 
   override fun toString(): String {
-    return "Message(messageId=$messageId, time=$time, type=$type, flag=$flag, bufferInfo=$bufferInfo, sender='$sender', senderPrefixes='$senderPrefixes', content='$content')"
+    return "Message(messageId=$messageId, time=$time, type=$type, flag=$flag, bufferInfo=$bufferInfo, sender='$sender', senderPrefixes='$senderPrefixes', attachments='$attachments', content='$content')"
   }
 }
diff --git a/lib/src/main/java/de/kuschku/libquassel/protocol/primitive/serializer/MessageSerializer.kt b/lib/src/main/java/de/kuschku/libquassel/protocol/primitive/serializer/MessageSerializer.kt
index e3d038887..4ae6d4097 100644
--- a/lib/src/main/java/de/kuschku/libquassel/protocol/primitive/serializer/MessageSerializer.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/protocol/primitive/serializer/MessageSerializer.kt
@@ -43,6 +43,8 @@ object MessageSerializer : Serializer<Message> {
       StringSerializer.UTF8.serialize(buffer, data.realName, features)
     if (features.hasFeature(ExtendedFeature.RichMessages))
       StringSerializer.UTF8.serialize(buffer, data.avatarUrl, features)
+    if (features.hasFeature(ExtendedFeature.MessageAttachments))
+      StringSerializer.UTF8.serialize(buffer, data.attachments, features)
     StringSerializer.UTF8.serialize(buffer, data.content, features)
   }
 
@@ -65,6 +67,8 @@ object MessageSerializer : Serializer<Message> {
         StringSerializer.UTF8.deserialize(buffer, features) ?: "" else "",
       avatarUrl = if (features.hasFeature(ExtendedFeature.RichMessages))
         StringSerializer.UTF8.deserialize(buffer, features) ?: "" else "",
+      attachments = if (features.hasFeature(ExtendedFeature.MessageAttachments))
+        StringSerializer.UTF8.deserialize(buffer, features) ?: "" else "",
       content = StringSerializer.UTF8.deserialize(buffer, features) ?: ""
     )
   }
diff --git a/lib/src/main/java/de/kuschku/libquassel/quassel/ExtendedFeature.kt b/lib/src/main/java/de/kuschku/libquassel/quassel/ExtendedFeature.kt
index 8718c9900..8001e5bed 100644
--- a/lib/src/main/java/de/kuschku/libquassel/quassel/ExtendedFeature.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/quassel/ExtendedFeature.kt
@@ -58,7 +58,9 @@ enum class ExtendedFeature {
   /** 64-bit IDs for messages */
   LongMessageId,
   /** CoreInfo dynamically updated using signals */
-  SyncedCoreInfo;
+  SyncedCoreInfo,
+  /** Rich message attachments */
+  MessageAttachments;
 
   companion object {
     private val map = values().associateBy(ExtendedFeature::name)
diff --git a/persistence/src/main/java/de/kuschku/quasseldroid/persistence/dao/MessageDao.kt b/persistence/src/main/java/de/kuschku/quasseldroid/persistence/dao/MessageDao.kt
index 9880dcf9d..48429ef0b 100644
--- a/persistence/src/main/java/de/kuschku/quasseldroid/persistence/dao/MessageDao.kt
+++ b/persistence/src/main/java/de/kuschku/quasseldroid/persistence/dao/MessageDao.kt
@@ -42,7 +42,7 @@ interface MessageDao {
   fun _buffers(): List<BufferId_Type>
 
   @Query("SELECT * FROM message WHERE messageId = :messageId")
-  fun find(messageId: MsgId_Type): MessageData?
+  fun _find(messageId: MsgId_Type): MessageData?
 
   @Query("SELECT * FROM message WHERE bufferId = :bufferId ORDER BY messageId ASC")
   fun _findByBufferId(bufferId: BufferId_Type): List<MessageData>
@@ -93,6 +93,9 @@ interface MessageDao {
 inline fun MessageDao.buffers() =
   _buffers().map { BufferId(it) }
 
+inline fun MessageDao.find(messageId: MsgId) =
+  _find(messageId.id)
+
 inline fun MessageDao.findByBufferId(bufferId: BufferId) =
   _findByBufferId(bufferId.id)
 
diff --git a/persistence/src/main/java/de/kuschku/quasseldroid/persistence/db/QuasselDatabase.kt b/persistence/src/main/java/de/kuschku/quasseldroid/persistence/db/QuasselDatabase.kt
index e4d64c0ae..2ea4bd2fe 100644
--- a/persistence/src/main/java/de/kuschku/quasseldroid/persistence/db/QuasselDatabase.kt
+++ b/persistence/src/main/java/de/kuschku/quasseldroid/persistence/db/QuasselDatabase.kt
@@ -30,7 +30,7 @@ import de.kuschku.quasseldroid.persistence.models.*
 import de.kuschku.quasseldroid.persistence.util.MessageTypeConverter
 
 @Database(entities = [MessageData::class, Filtered::class, SslValidityWhitelistEntry::class, SslHostnameWhitelistEntry::class, NotificationData::class],
-          version = 19)
+          version = 20)
 @TypeConverters(MessageTypeConverter::class)
 abstract class QuasselDatabase : RoomDatabase() {
   abstract fun message(): MessageDao
@@ -157,6 +157,11 @@ abstract class QuasselDatabase : RoomDatabase() {
                 override fun migrate(database: SupportSQLiteDatabase) {
                   database.execSQL("ALTER TABLE `notification` ADD `networkName` TEXT DEFAULT '' NOT NULL;")
                 }
+              },
+              object : Migration(19, 20) {
+                override fun migrate(database: SupportSQLiteDatabase) {
+                  database.execSQL("ALTER TABLE `message` ADD `attachments` TEXT DEFAULT '' NOT NULL;")
+                }
               }
             ).build()
           }
diff --git a/persistence/src/main/java/de/kuschku/quasseldroid/persistence/models/MessageData.kt b/persistence/src/main/java/de/kuschku/quasseldroid/persistence/models/MessageData.kt
index 3233e80b6..93fbe4b1e 100644
--- a/persistence/src/main/java/de/kuschku/quasseldroid/persistence/models/MessageData.kt
+++ b/persistence/src/main/java/de/kuschku/quasseldroid/persistence/models/MessageData.kt
@@ -43,6 +43,7 @@ data class MessageData(
   var senderPrefixes: String,
   var realName: String,
   var avatarUrl: String,
+  var attachments: String,
   var content: String,
   var ignored: Boolean
 ) {
@@ -65,6 +66,7 @@ data class MessageData(
       senderPrefixes: String,
       realName: String,
       avatarUrl: String,
+      attachments: String,
       content: String,
       ignored: Boolean
     ) = MessageData(
@@ -78,6 +80,7 @@ data class MessageData(
       senderPrefixes,
       realName,
       avatarUrl,
+      attachments,
       content,
       ignored
     )
diff --git a/persistence/src/main/java/de/kuschku/quasseldroid/persistence/util/QuasselBacklogStorage.kt b/persistence/src/main/java/de/kuschku/quasseldroid/persistence/util/QuasselBacklogStorage.kt
index 60b813a7e..07ebbce4a 100644
--- a/persistence/src/main/java/de/kuschku/quasseldroid/persistence/util/QuasselBacklogStorage.kt
+++ b/persistence/src/main/java/de/kuschku/quasseldroid/persistence/util/QuasselBacklogStorage.kt
@@ -55,6 +55,7 @@ class QuasselBacklogStorage(private val db: QuasselDatabase) : BacklogStorage {
         senderPrefixes = it.senderPrefixes,
         realName = it.realName,
         avatarUrl = it.avatarUrl,
+        attachments = it.attachments,
         content = it.content,
         ignored = isIgnored(
           session,
diff --git a/viewmodel/build.gradle.kts b/viewmodel/build.gradle.kts
index a4d86f44a..5e4e2a749 100644
--- a/viewmodel/build.gradle.kts
+++ b/viewmodel/build.gradle.kts
@@ -56,6 +56,7 @@ dependencies {
   implementation("io.reactivex.rxjava2", "rxjava", "2.1.9")
   implementation("org.threeten", "threetenbp", "1.3.8", classifier = "no-tzdb")
   implementation("org.jetbrains", "annotations", "16.0.3")
+  implementation("com.google.code.gson", "gson", "2.8.5")
 
   // Quassel
   implementation(project(":persistence"))
diff --git a/viewmodel/src/main/java/de/kuschku/quasseldroid/util/attachment/AttachmentData.kt b/viewmodel/src/main/java/de/kuschku/quasseldroid/util/attachment/AttachmentData.kt
new file mode 100644
index 000000000..bf0842c36
--- /dev/null
+++ b/viewmodel/src/main/java/de/kuschku/quasseldroid/util/attachment/AttachmentData.kt
@@ -0,0 +1,55 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2019 Janne Koschinski
+ * Copyright (c) 2019 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * 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 de.kuschku.quasseldroid.util.attachment
+
+import com.google.gson.annotations.SerializedName
+
+data class AttachmentData(
+  @SerializedName("from_url")
+  val fromUrl: String?,
+  @SerializedName("color")
+  val color: String?,
+  @SerializedName("author_name")
+  val authorName: String?,
+  @SerializedName("author_link")
+  val authorLink: String?,
+  @SerializedName("author_icon")
+  val authorIcon: String?,
+  @SerializedName("title")
+  val title: String?,
+  @SerializedName("title_link")
+  val titleLink: String?,
+  @SerializedName("text")
+  val text: String?,
+  @SerializedName("fields")
+  val fields: List<AttachmentDataField>?,
+  @SerializedName("image_url")
+  val imageUrl: String?,
+  @SerializedName("type")
+  val type: String?,
+  @SerializedName("player")
+  val player: String?,
+  @SerializedName("service_name")
+  val serviceName: String?,
+  @SerializedName("service_icon")
+  val serviceIcon: String?,
+  @SerializedName("ts")
+  val timestamp: Int?
+)
diff --git a/viewmodel/src/main/java/de/kuschku/quasseldroid/util/attachment/AttachmentDataField.kt b/viewmodel/src/main/java/de/kuschku/quasseldroid/util/attachment/AttachmentDataField.kt
new file mode 100644
index 000000000..e2338e27a
--- /dev/null
+++ b/viewmodel/src/main/java/de/kuschku/quasseldroid/util/attachment/AttachmentDataField.kt
@@ -0,0 +1,26 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2019 Janne Koschinski
+ * Copyright (c) 2019 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * 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 de.kuschku.quasseldroid.util.attachment
+
+data class AttachmentDataField(
+  val title: String,
+  val value: String,
+  val short: Boolean
+)
diff --git a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/data/FormattedMessage.kt b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/data/FormattedMessage.kt
index 106fd6e16..92ca0ed3a 100644
--- a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/data/FormattedMessage.kt
+++ b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/data/FormattedMessage.kt
@@ -21,6 +21,7 @@ package de.kuschku.quasseldroid.viewmodel.data
 
 import android.graphics.drawable.Drawable
 import de.kuschku.quasseldroid.persistence.models.MessageData
+import de.kuschku.quasseldroid.util.attachment.AttachmentData
 
 class FormattedMessage(
   val original: MessageData,
@@ -33,6 +34,7 @@ class FormattedMessage(
   val realName: CharSequence? = null,
   val avatarUrls: List<Avatar> = emptyList(),
   val urls: List<String> = emptyList(),
+  val attachment: AttachmentData? = null,
   val hasDayChange: Boolean,
   val isSelected: Boolean,
   val isExpanded: Boolean,
@@ -54,6 +56,7 @@ class FormattedMessage(
     if (realName != other.realName) return false
     if (avatarUrls != other.avatarUrls) return false
     if (urls != other.urls) return false
+    if (attachment != other.attachment) return false
     if (hasDayChange != other.hasDayChange) return false
     if (isSelected != other.isSelected) return false
     if (isExpanded != other.isExpanded) return false
@@ -72,6 +75,7 @@ class FormattedMessage(
     result = 31 * result + (realName?.hashCode() ?: 0)
     result = 31 * result + avatarUrls.hashCode()
     result = 31 * result + urls.hashCode()
+    result = 31 * result + attachment.hashCode()
     result = 31 * result + hasDayChange.hashCode()
     result = 31 * result + isSelected.hashCode()
     result = 31 * result + isExpanded.hashCode()
-- 
GitLab