From f2c612be79f801fc2e87b8062db3e8b8a5deb1a9 Mon Sep 17 00:00:00 2001
From: Janne Mareike Koschinski <mail@justjanne.de>
Date: Wed, 27 Nov 2024 13:53:38 +0100
Subject: [PATCH] fix: navigation between chats

---
 .../de/kuschku/quasseldroid/Quasseldroid.kt   |   3 +-
 .../quasseldroid/dagger/ActivityModule.kt     |  80 ++--
 .../quasseldroid/dagger/AppComponent.kt       |   9 +-
 .../service/QuasselNotificationBackend.kt     |   4 +-
 .../service/QuasselServiceModule.kt           |   6 +-
 .../quasseldroid/ui/chat/ChatActivity.kt      | 352 +++++++++---------
 .../ui/chat/ChatActivityModule.kt             |   4 +-
 .../ui/chat/ChatFragmentProvider.kt           |  14 +-
 .../quasseldroid/ui/chat/ToolbarFragment.kt   |  28 +-
 .../chat/add/create/ChannelCreateFragment.kt  |  33 +-
 .../create/ChannelCreateFragmentProvider.kt   |   6 +-
 .../ui/chat/add/join/ChannelJoinFragment.kt   |  40 +-
 .../add/join/ChannelJoinFragmentProvider.kt   |   6 +-
 .../ui/chat/add/query/QueryCreateFragment.kt  |  49 ++-
 .../add/query/QueryCreateFragmentProvider.kt  |   6 +-
 .../ui/chat/archive/ArchiveFragment.kt        |  38 +-
 .../chat/archive/ArchiveFragmentProvider.kt   |   6 +-
 .../chat/buffers/BufferViewConfigFragment.kt  |  78 ++--
 .../ui/chat/input/AutoCompleteHelper.kt       |  48 +--
 .../ui/chat/input/ChatlineFragment.kt         |  26 +-
 .../ui/chat/messages/MessageListFragment.kt   | 118 +++---
 .../chat/messages/QuasselMessageRenderer.kt   |  76 +++-
 .../ui/chat/nicks/NickListFragment.kt         |  36 +-
 .../ui/chat/topic/TopicFragment.kt            |  33 +-
 .../ui/chat/topic/TopicFragmentProvider.kt    |   6 +-
 .../about/AboutFragmentProvider.kt            |   6 +-
 .../client/ClientSettingsFragmentProvider.kt  |   6 +-
 .../crash/CrashFragmentProvider.kt            |   6 +-
 .../license/LicenseFragmentProvider.kt        |   6 +-
 .../whitelist/WhitelistFragmentProvider.kt    |   6 +-
 .../ui/coresettings/CoreSettingsFragment.kt   |  26 +-
 .../CoreSettingsFragmentProvider.kt           |   6 +-
 .../aliasitem/AliasItemFragment.kt            |  28 +-
 .../aliasitem/AliasItemFragmentProvider.kt    |   6 +-
 .../aliaslist/AliasListAdapter.kt             |   8 +-
 .../aliaslist/AliasListFragment.kt            |   5 +-
 .../aliaslist/AliasListFragmentProvider.kt    |   6 +-
 .../chatlist/ChatListBaseFragment.kt          |  21 +-
 .../ChatlistCreateFragmentProvider.kt         |   6 +-
 .../chatlist/ChatlistEditFragmentProvider.kt  |   6 +-
 .../highlightlist/HighlightListFragment.kt    |  30 +-
 .../HighlightListFragmentProvider.kt          |   6 +-
 .../HighlightRuleFragmentProvider.kt          |   6 +-
 .../identity/IdentityBaseFragment.kt          |   9 +-
 .../IdentityCreateFragmentProvider.kt         |   6 +-
 .../identity/IdentityEditFragmentProvider.kt  |   6 +-
 .../ignoreitem/IgnoreItemFragmentProvider.kt  |   6 +-
 .../ignorelist/IgnoreListFragment.kt          |   5 +-
 .../ignorelist/IgnoreListFragmentProvider.kt  |   6 +-
 .../network/NetworkBaseFragment.kt            |  17 +-
 .../network/NetworkCreateFragmentProvider.kt  |   6 +-
 .../network/NetworkEditFragmentProvider.kt    |   6 +-
 .../networkconfig/NetworkConfigFragment.kt    |   5 +-
 .../NetworkConfigFragmentProvider.kt          |   6 +-
 .../NetworkServerFragmentProvider.kt          |   6 +-
 .../passwordchange/PasswordChangeFragment.kt  |   5 +-
 .../PasswordChangeFragmentProvider.kt         |   6 +-
 .../certificate/CertificateInfoFragment.kt    |   5 +-
 .../CertificateInfoFragmentProvider.kt        |   6 +-
 .../ui/info/channel/ChannelInfoFragment.kt    |  33 +-
 .../channel/ChannelInfoFragmentProvider.kt    |   6 +-
 .../ui/info/channellist/ChannelListAdapter.kt |  18 +-
 .../info/channellist/ChannelListFragment.kt   | 106 ++++--
 .../ChannelListFragmentProvider.kt            |   6 +-
 .../ui/info/core/CoreInfoFragment.kt          |  50 ++-
 .../ui/info/core/CoreInfoFragmentProvider.kt  |   6 +-
 .../ui/info/user/ChannelAdapter.kt            |  11 +-
 .../ui/info/user/UserInfoFragment.kt          |  57 ++-
 .../ui/info/user/UserInfoFragmentProvider.kt  |   6 +-
 .../edit/AccountEditFragmentProvider.kt       |   6 +-
 .../AccountSelectionFragmentProvider.kt       |   6 +-
 .../setup/AccountSetupFragmentProvider.kt     |  10 +-
 .../setup/core/CoreSetupFragmentProvider.kt   |  12 +-
 .../network/NetworkSetupFragmentProvider.kt   |   8 +-
 .../setup/network/NetworkSetupNetworkSlide.kt |   9 +-
 .../ui/setup/user/UserSetupActivity.kt        |   8 +-
 .../setup/user/UserSetupFragmentProvider.kt   |  10 +-
 .../ui/setup/user/UserSetupIdentitySlide.kt   |   7 +-
 .../util/irc/format/ContentFormatter.kt       |  15 +-
 .../util/irc/format/IrcFormatDeserializer.kt  |   4 +-
 .../irc/format/model/FormatDescription.kt     |   5 +-
 .../util/irc/format/model/FormatInfo.kt       |   5 +-
 .../util/irc/format/model/IrcFormat.kt        |  45 ++-
 .../util/irc/format/spans/ChannelLinkSpan.kt  |   8 +-
 .../util/irc/format/spans/QuasselURLSpan.kt   |  19 +-
 .../util/listener/AutocompleteTextHandler.kt  |  25 ++
 .../util/listener/AutocompleteTextListener.kt |   8 +
 .../util/listener/LinkClickHandler.kt         |  61 +++
 .../util/listener/LinkClickListener.kt        |  27 ++
 .../util/listener/ListenerModule.kt           |  13 +
 .../util/listener/QuasselLinkClickListener.kt |  27 ++
 .../util/ui/BetterLinkMovementMethod.java     |  12 +-
 .../util/ui/presenter/BufferPresenter.kt      |   7 +-
 .../irc/format/IrcFormatDeserializerTest.kt   |   5 +
 94 files changed, 1306 insertions(+), 756 deletions(-)
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid/util/listener/AutocompleteTextHandler.kt
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid/util/listener/AutocompleteTextListener.kt
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid/util/listener/LinkClickHandler.kt
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid/util/listener/LinkClickListener.kt
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid/util/listener/ListenerModule.kt
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid/util/listener/QuasselLinkClickListener.kt

diff --git a/app/src/main/java/de/kuschku/quasseldroid/Quasseldroid.kt b/app/src/main/java/de/kuschku/quasseldroid/Quasseldroid.kt
index 79b88d15d..f7e1a76f0 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/Quasseldroid.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/Quasseldroid.kt
@@ -20,6 +20,7 @@
 package de.kuschku.quasseldroid
 
 import android.content.Context
+import dagger.android.AndroidInjector
 import dagger.android.support.DaggerApplication
 import de.kuschku.quasseldroid.app.AppDelegate
 import de.kuschku.quasseldroid.app.QuasseldroidReleaseDelegate
@@ -27,7 +28,7 @@ import de.kuschku.quasseldroid.dagger.DaggerAppComponent
 import de.kuschku.quasseldroid.util.ui.LocaleHelper
 
 open class Quasseldroid : DaggerApplication() {
-  override fun applicationInjector() = DaggerAppComponent.builder().create(this)
+  override fun applicationInjector(): AndroidInjector<Quasseldroid> = DaggerAppComponent.factory().create(this)
   open val delegate: AppDelegate = QuasseldroidReleaseDelegate(this)
 
   override fun onCreate() {
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 841c00314..381e5bfac 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/dagger/ActivityModule.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/dagger/ActivityModule.kt
@@ -102,172 +102,172 @@ import de.kuschku.quasseldroid.ui.setup.user.UserSetupActivity
 import de.kuschku.quasseldroid.ui.setup.user.UserSetupFragmentProvider
 
 @Module
-abstract class ActivityModule {
+interface ActivityModule {
   @ActivityScope
   @ContributesAndroidInjector(modules = [ChatActivityModule::class, ChatFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class])
-  abstract fun bindChatActivity(): ChatActivity
+  fun bindChatActivity(): ChatActivity
 
   @ActivityScope
   @ContributesAndroidInjector(modules = [ArchiveFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class])
-  abstract fun bindArchiveActivity(): ArchiveActivity
+  fun bindArchiveActivity(): ArchiveActivity
 
   // Info
 
   @ActivityScope
   @ContributesAndroidInjector(modules = [UserInfoFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class])
-  abstract fun bindUserInfoActivity(): UserInfoActivity
+  fun bindUserInfoActivity(): UserInfoActivity
 
   @ActivityScope
   @ContributesAndroidInjector(modules = [ChannelInfoFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class])
-  abstract fun bindChannelInfoActivity(): ChannelInfoActivity
+  fun bindChannelInfoActivity(): ChannelInfoActivity
 
   @ActivityScope
   @ContributesAndroidInjector(modules = [CoreInfoFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class])
-  abstract fun bindCoreInfoActivity(): CoreInfoActivity
+  fun bindCoreInfoActivity(): CoreInfoActivity
 
   @ActivityScope
   @ContributesAndroidInjector(modules = [TopicFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class])
-  abstract fun bindTopicActivity(): TopicActivity
+  fun bindTopicActivity(): TopicActivity
 
   @ActivityScope
   @ContributesAndroidInjector(modules = [ChannelListFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class])
-  abstract fun bindChannelListActivity(): ChannelListActivity
+  fun bindChannelListActivity(): ChannelListActivity
 
   @ActivityScope
   @ContributesAndroidInjector(modules = [CertificateInfoFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class])
-  abstract fun bindCertificateInfoActivity(): CertificateInfoActivity
+  fun bindCertificateInfoActivity(): CertificateInfoActivity
 
   // Add
 
   @ActivityScope
   @ContributesAndroidInjector(modules = [ChannelCreateFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class])
-  abstract fun bindChannelCreateActivity(): ChannelCreateActivity
+  fun bindChannelCreateActivity(): ChannelCreateActivity
 
   @ActivityScope
   @ContributesAndroidInjector(modules = [ChannelJoinFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class])
-  abstract fun bindChannelJoinActivity(): ChannelJoinActivity
+  fun bindChannelJoinActivity(): ChannelJoinActivity
 
   @ActivityScope
   @ContributesAndroidInjector(modules = [QueryCreateFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class])
-  abstract fun bindQueryCreateActivity(): QueryCreateActivity
+  fun bindQueryCreateActivity(): QueryCreateActivity
 
   // Client Settings
 
   @ActivityScope
   @ContributesAndroidInjector(modules = [ClientSettingsFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class])
-  abstract fun bindClientSettingsActivity(): ClientSettingsActivity
+  fun bindClientSettingsActivity(): ClientSettingsActivity
 
   @ActivityScope
   @ContributesAndroidInjector(modules = [WhitelistFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class])
-  abstract fun bindWhitelistActivity(): WhitelistActivity
+  fun bindWhitelistActivity(): WhitelistActivity
 
   @ActivityScope
   @ContributesAndroidInjector(modules = [CrashFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class])
-  abstract fun bindCrashActivity(): CrashActivity
+  fun bindCrashActivity(): CrashActivity
 
   @ActivityScope
   @ContributesAndroidInjector(modules = [AboutFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class])
-  abstract fun bindAboutActivity(): AboutActivity
+  fun bindAboutActivity(): AboutActivity
 
   @ActivityScope
   @ContributesAndroidInjector(modules = [LicenseFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class])
-  abstract fun bindLicenseActivity(): LicenseActivity
+  fun bindLicenseActivity(): LicenseActivity
 
   // Core Settings
 
   @ActivityScope
   @ContributesAndroidInjector(modules = [CoreSettingsFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class])
-  abstract fun bindCoreSettingsActivity(): CoreSettingsActivity
+  fun bindCoreSettingsActivity(): CoreSettingsActivity
 
   @ActivityScope
   @ContributesAndroidInjector(modules = [NetworkCreateFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class])
-  abstract fun bindNetworkCreateActivity(): NetworkCreateActivity
+  fun bindNetworkCreateActivity(): NetworkCreateActivity
 
   @ActivityScope
   @ContributesAndroidInjector(modules = [NetworkEditFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class])
-  abstract fun bindNetworkEditActivity(): NetworkEditActivity
+  fun bindNetworkEditActivity(): NetworkEditActivity
 
   @ActivityScope
   @ContributesAndroidInjector(modules = [NetworkServerFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class])
-  abstract fun bindNetworkServerActivity(): NetworkServerActivity
+  fun bindNetworkServerActivity(): NetworkServerActivity
 
   @ActivityScope
   @ContributesAndroidInjector(modules = [IdentityCreateFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class])
-  abstract fun bindIdentityCreateActivity(): IdentityCreateActivity
+  fun bindIdentityCreateActivity(): IdentityCreateActivity
 
   @ActivityScope
   @ContributesAndroidInjector(modules = [IdentityEditFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class])
-  abstract fun bindIdentityEditActivity(): IdentityEditActivity
+  fun bindIdentityEditActivity(): IdentityEditActivity
 
   @ActivityScope
   @ContributesAndroidInjector(modules = [ChatlistCreateFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class])
-  abstract fun bindChatListCreateActivity(): ChatlistCreateActivity
+  fun bindChatListCreateActivity(): ChatlistCreateActivity
 
   @ActivityScope
   @ContributesAndroidInjector(modules = [ChatlistEditFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class])
-  abstract fun bindChatListEditActivity(): ChatlistEditActivity
+  fun bindChatListEditActivity(): ChatlistEditActivity
 
   @ActivityScope
   @ContributesAndroidInjector(modules = [IgnoreListFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class])
-  abstract fun bindIgnoreListActivity(): IgnoreListActivity
+  fun bindIgnoreListActivity(): IgnoreListActivity
 
   @ActivityScope
   @ContributesAndroidInjector(modules = [IgnoreItemFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class])
-  abstract fun bindIgnoreItemActivity(): IgnoreItemActivity
+  fun bindIgnoreItemActivity(): IgnoreItemActivity
 
   @ActivityScope
   @ContributesAndroidInjector(modules = [HighlightListFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class])
-  abstract fun bindHighlightListActivity(): HighlightListActivity
+  fun bindHighlightListActivity(): HighlightListActivity
 
   @ActivityScope
   @ContributesAndroidInjector(modules = [HighlightRuleFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class])
-  abstract fun bindHighlightRuleActivity(): HighlightRuleActivity
+  fun bindHighlightRuleActivity(): HighlightRuleActivity
 
   @ActivityScope
   @ContributesAndroidInjector(modules = [AliasListFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class])
-  abstract fun bindAliasListActivity(): AliasListActivity
+  fun bindAliasListActivity(): AliasListActivity
 
   @ActivityScope
   @ContributesAndroidInjector(modules = [AliasItemFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class])
-  abstract fun bindAliasItemActivity(): AliasItemActivity
+  fun bindAliasItemActivity(): AliasItemActivity
 
   @ActivityScope
   @ContributesAndroidInjector(modules = [NetworkConfigFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class])
-  abstract fun bindNetworkConfigActivity(): NetworkConfigActivity
+  fun bindNetworkConfigActivity(): NetworkConfigActivity
 
   @ActivityScope
   @ContributesAndroidInjector(modules = [PasswordChangeFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class])
-  abstract fun bindPasswordChangeActivity(): PasswordChangeActivity
+  fun bindPasswordChangeActivity(): PasswordChangeActivity
 
   // Setup
 
   @ActivityScope
   @ContributesAndroidInjector(modules = [AccountSetupFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class])
-  abstract fun bindAccountSetupActivity(): AccountSetupActivity
+  fun bindAccountSetupActivity(): AccountSetupActivity
 
   @ActivityScope
   @ContributesAndroidInjector(modules = [AccountSelectionFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class])
-  abstract fun bindAccountSelectionActivity(): AccountSelectionActivity
+  fun bindAccountSelectionActivity(): AccountSelectionActivity
 
   @ActivityScope
   @ContributesAndroidInjector(modules = [AccountEditFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class])
-  abstract fun bindAccountEditActivity(): AccountEditActivity
+  fun bindAccountEditActivity(): AccountEditActivity
 
   @ActivityScope
   @ContributesAndroidInjector(modules = [UserSetupFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class])
-  abstract fun bindUserSetupActivity(): UserSetupActivity
+  fun bindUserSetupActivity(): UserSetupActivity
 
   @ActivityScope
   @ContributesAndroidInjector(modules = [NetworkSetupFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class])
-  abstract fun bindNetworkSetupActivity(): NetworkSetupActivity
+  fun bindNetworkSetupActivity(): NetworkSetupActivity
 
   @ActivityScope
   @ContributesAndroidInjector(modules = [CoreSetupFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class])
-  abstract fun bindCoreSetupActivity(): CoreSetupActivity
+  fun bindCoreSetupActivity(): CoreSetupActivity
 
   // Service
 
   @ActivityScope
   @ContributesAndroidInjector(modules = [QuasselServiceModule::class, SettingsModule::class, DatabaseModule::class])
-  abstract fun bindQuasselService(): QuasselService
+  fun bindQuasselService(): QuasselService
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/dagger/AppComponent.kt b/app/src/main/java/de/kuschku/quasseldroid/dagger/AppComponent.kt
index e070a7bd6..e3a6fdc8f 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/dagger/AppComponent.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/dagger/AppComponent.kt
@@ -23,17 +23,20 @@ import dagger.Component
 import dagger.android.AndroidInjector
 import dagger.android.support.AndroidSupportInjectionModule
 import de.kuschku.quasseldroid.Quasseldroid
+import de.kuschku.quasseldroid.util.listener.ListenerModule
 import javax.inject.Singleton
 
+
 @Singleton
 @Component(
   modules = [
     AndroidSupportInjectionModule::class,
     AppModule::class,
-    ActivityModule::class
+    ActivityModule::class,
+    ListenerModule::class,
   ]
 )
 interface AppComponent : AndroidInjector<Quasseldroid> {
-  @Component.Builder
-  abstract class Builder : AndroidInjector.Builder<Quasseldroid>()
+  @Component.Factory
+  interface Factory : AndroidInjector.Factory<Quasseldroid>
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/service/QuasselNotificationBackend.kt b/app/src/main/java/de/kuschku/quasseldroid/service/QuasselNotificationBackend.kt
index 883ca16ac..225b2ab49 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/service/QuasselNotificationBackend.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/service/QuasselNotificationBackend.kt
@@ -55,6 +55,7 @@ import de.kuschku.quasseldroid.util.helper.letIf
 import de.kuschku.quasseldroid.util.helper.loadWithFallbacks
 import de.kuschku.quasseldroid.util.helper.styledAttributes
 import de.kuschku.quasseldroid.util.irc.format.ContentFormatter
+import de.kuschku.quasseldroid.util.listener.LinkClickListener
 import de.kuschku.quasseldroid.util.ui.drawable.TextDrawable
 import de.kuschku.quasseldroid.viewmodel.helper.EditorViewModelHelper.Companion.IGNORED_CHARS
 import org.threeten.bp.Instant
@@ -66,6 +67,7 @@ class QuasselNotificationBackend @Inject constructor(
   private val context: Context,
   private val database: QuasselDatabase,
   private val contentFormatter: ContentFormatter,
+  private val linkClickListener: LinkClickListener,
   private val notificationHandler: QuasseldroidNotificationManager
 ) : NotificationManager {
   private var notificationSettings: NotificationSettings
@@ -353,7 +355,7 @@ class QuasselNotificationBackend @Inject constructor(
             selfColor = selfColor
           ))
         }
-        val (content, _) = contentFormatter.formatContent(it.content, false, false, it.networkId)
+        val (content, _) = contentFormatter.formatContent(it.content, false, false, it.networkId, linkClickListener)
 
         NotificationMessage(
           messageId = it.messageId,
diff --git a/app/src/main/java/de/kuschku/quasseldroid/service/QuasselServiceModule.kt b/app/src/main/java/de/kuschku/quasseldroid/service/QuasselServiceModule.kt
index bdb1a9e68..f18948de9 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/service/QuasselServiceModule.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/service/QuasselServiceModule.kt
@@ -26,10 +26,10 @@ import de.kuschku.quasseldroid.ui.chat.messages.MessageRenderer
 import de.kuschku.quasseldroid.ui.chat.messages.QuasselMessageRenderer
 
 @Module
-abstract class QuasselServiceModule {
+interface QuasselServiceModule {
   @Binds
-  abstract fun bindContext(service: QuasselService): Context
+  fun bindContext(service: QuasselService): Context
 
   @Binds
-  abstract fun bindMessageRenderer(messageRenderer: QuasselMessageRenderer): MessageRenderer
+  fun bindMessageRenderer(messageRenderer: QuasselMessageRenderer): MessageRenderer
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatActivity.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatActivity.kt
index 59c206299..c1b9366ad 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatActivity.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatActivity.kt
@@ -22,14 +22,17 @@ package de.kuschku.quasseldroid.ui.chat
 import android.Manifest
 import android.annotation.SuppressLint
 import android.app.Activity
+import android.content.ActivityNotFoundException
 import android.content.Context
 import android.content.Intent
 import android.content.SharedPreferences
 import android.content.pm.PackageManager
+import android.net.Uri
 import android.os.Build
 import android.os.Bundle
 import android.system.ErrnoException
 import android.text.Html
+import android.util.Log
 import android.view.ActionMode
 import android.view.Menu
 import android.view.MenuItem
@@ -41,8 +44,6 @@ import androidx.appcompat.app.ActionBarDrawerToggle
 import androidx.core.app.ActivityCompat
 import androidx.core.view.GravityCompat
 import androidx.drawerlayout.widget.DrawerLayout
-import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.Observer
 import androidx.recyclerview.widget.DefaultItemAnimator
 import androidx.recyclerview.widget.LinearLayoutManager
 import com.afollestad.materialdialogs.MaterialDialog
@@ -95,6 +96,10 @@ import de.kuschku.quasseldroid.util.backport.OsConstants
 import de.kuschku.quasseldroid.util.deceptive_networks.DeceptiveNetworkDialog
 import de.kuschku.quasseldroid.util.helper.*
 import de.kuschku.quasseldroid.util.irc.format.IrcFormatDeserializer
+import de.kuschku.quasseldroid.util.listener.AutocompleteTextHandler
+import de.kuschku.quasseldroid.util.listener.AutocompleteTextListener
+import de.kuschku.quasseldroid.util.listener.LinkClickHandler
+import de.kuschku.quasseldroid.util.listener.LinkClickListener
 import de.kuschku.quasseldroid.util.missingfeatures.MissingFeature
 import de.kuschku.quasseldroid.util.missingfeatures.MissingFeaturesDialog
 import de.kuschku.quasseldroid.util.missingfeatures.RequiredFeatures
@@ -110,7 +115,6 @@ import de.kuschku.quasseldroid.viewmodel.helper.ChatViewModelHelper
 import io.reactivex.BackpressureStrategy
 import io.reactivex.Observable
 import io.reactivex.subjects.BehaviorSubject
-import io.reactivex.subjects.PublishSubject
 import org.threeten.bp.Instant
 import org.threeten.bp.ZoneId
 import org.threeten.bp.format.DateTimeFormatter
@@ -122,7 +126,8 @@ import java.security.cert.CertificateNotYetValidException
 import javax.inject.Inject
 
 @SuppressLint("ResourceType")
-class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenceChangeListener {
+class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenceChangeListener,
+  AutocompleteTextListener, LinkClickListener {
   lateinit var binding: ActivityMainBinding
 
   @Inject
@@ -155,6 +160,12 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
   @Inject
   lateinit var notificationBackend: QuasselNotificationBackend
 
+  @Inject
+  lateinit var autocompleteTextHandler: AutocompleteTextHandler
+
+  @Inject
+  lateinit var linkClickHandler: LinkClickHandler
+
   lateinit var editorBottomSheet: DragInterceptBottomSheetBehavior<View>
 
   private val dateTimeFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
@@ -180,111 +191,17 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
       }
 
       intent.hasExtra(KEY_BUFFER_ID) -> {
-        chatViewModel.bufferId.onNext(BufferId(intent.getIntExtra(KEY_BUFFER_ID, -1)))
-        chatViewModel.bufferOpened.onNext(Unit)
+        val bufferId = BufferId(intent.getIntExtra(KEY_BUFFER_ID, -1))
         if (intent.hasExtra(KEY_ACCOUNT_ID)) {
-          val accountId = AccountId(intent.getLongExtra(KEY_ACCOUNT_ID, -1L))
-          if (accountId != this.accountId) {
-            resetAccount()
-            connectToAccount(accountId)
-            startedSelection = false
-            connectedAccount = AccountId(-1L)
-            checkConnection()
-            recreate()
-          }
+          openBuffer(
+            bufferId,
+            AccountId(intent.getLongExtra(KEY_ACCOUNT_ID, -1L))
+          )
+        } else {
+          openBuffer(bufferId)
         }
       }
 
-      intent.hasExtra(KEY_AUTOCOMPLETE_TEXT) -> {
-        chatlineFragment?.editorHelper?.appendText(
-          intent.getStringExtra(KEY_AUTOCOMPLETE_TEXT),
-          intent.getStringExtra(KEY_AUTOCOMPLETE_SUFFIX)
-        )
-        binding.drawerLayout.closeDrawers()
-      }
-
-      intent.hasExtra(KEY_NETWORK_ID) && intent.hasExtra(KEY_CHANNEL) -> {
-        val networkId = NetworkId(intent.getIntExtra(KEY_NETWORK_ID, -1))
-        val channel = intent.getStringExtra(KEY_CHANNEL) ?: ""
-
-        val forceJoin = intent.getBooleanExtra(KEY_FORCE_JOIN, false)
-
-        modelHelper.connectedSession.filter(Optional<ISession>::isPresent).firstElement()
-          .subscribe {
-            it.orNull()?.also { session ->
-              val info = session.bufferSyncer.find(
-                bufferName = channel,
-                networkId = networkId,
-                type = Buffer_Type.of(Buffer_Type.ChannelBuffer)
-              )
-
-              if (info != null && !forceJoin) {
-                ChatActivity.launch(this, bufferId = info.bufferId)
-              } else {
-                modelHelper.chat.chatToJoin.onNext(
-                  Optional.of(
-                    Pair(networkId, channel)
-                  )
-                )
-
-                session.bufferSyncer.find(
-                  networkId = networkId,
-                  type = Buffer_Type.of(Buffer_Type.StatusBuffer)
-                )?.let { statusInfo ->
-                  session.rpcHandler.sendInput(
-                    statusInfo, "/join $channel"
-                  )
-                }
-              }
-            }
-          }
-      }
-
-      intent.hasExtra(KEY_NETWORK_ID) && intent.hasExtra(KEY_NICK_NAME) -> {
-        val networkId = NetworkId(intent.getIntExtra(KEY_NETWORK_ID, -1))
-        val channel = intent.getStringExtra(KEY_NICK_NAME)
-
-        val forceJoin = intent.getBooleanExtra(KEY_FORCE_JOIN, false)
-
-        modelHelper.connectedSession.filter(Optional<ISession>::isPresent).firstElement()
-          .subscribe {
-            it.orNull()?.also { session ->
-              val info = session.bufferSyncer.find(
-                bufferName = channel,
-                networkId = networkId,
-                type = Buffer_Type.of(Buffer_Type.QueryBuffer)
-              )
-
-              if (info != null && !forceJoin) {
-                ChatActivity.launch(this, bufferId = info.bufferId)
-              } else {
-                modelHelper.allBuffers.map {
-                  listOfNotNull(it.find {
-                    it.networkId == networkId &&
-                      it.bufferName == channel &&
-                      it.type.hasFlag(Buffer_Type.QueryBuffer)
-                  })
-                }.filter {
-                  it.isNotEmpty()
-                }.firstElement().toLiveData().observeForever {
-                  it?.firstOrNull()?.let { info ->
-                    ChatActivity.launch(this, bufferId = info.bufferId)
-                  }
-                }
-
-                session.bufferSyncer.find(
-                  networkId = networkId,
-                  type = Buffer_Type.of(Buffer_Type.StatusBuffer)
-                )?.let { statusInfo ->
-                  session.rpcHandler.sendInput(
-                    statusInfo, "/query $channel"
-                  )
-                }
-              }
-            }
-          }
-      }
-
       intent.scheme == "irc" ||
         intent.scheme == "ircs" -> {
         val uri = intent.data
@@ -316,6 +233,9 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
     binding = ActivityMainBinding.inflate(layoutInflater)
     setContentView(binding.root)
 
+    linkClickHandler.register(this)
+    autocompleteTextHandler.register(this)
+
     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
       permissionGranted.onNext(
         PackageManager.PERMISSION_GRANTED == ActivityCompat.checkSelfPermission(
@@ -420,7 +340,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
         R.attr.colorTintNotification
       )
       maxBufferActivity.toLiveData()
-        .observe(this@ChatActivity, Observer { (activity, hasNotifications) ->
+        .observe(this@ChatActivity) { (activity, hasNotifications) ->
           setHomeAsUpIndicator(
             when {
               notificationSettings.showAllActivitiesInToolbar &&
@@ -442,7 +362,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
                 toggleDefault
             }
           )
-        })
+        }
     }
 
     if (autoCompleteSettings.prefix || autoCompleteSettings.auto) {
@@ -467,14 +387,14 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
       else Optional.ofNullable(buffers.firstOrNull {
         it.networkId == NetworkId(-current.id) && it.type.hasFlag(Buffer_Type.StatusBuffer)
       })
-    }.toLiveData().observe(this, Observer { info ->
+    }.toLiveData().observe(this) { info ->
       info?.orNull()?.let {
-        ChatActivity.launch(this, bufferId = it.bufferId)
+        openBuffer(bufferId = it.bufferId)
       }
-    })
+    }
 
     // User-actionable errors that require immediate action, and should show up as dialog
-    modelHelper.errors.toLiveData(BackpressureStrategy.BUFFER).observe(this, Observer { error ->
+    modelHelper.errors.toLiveData(BackpressureStrategy.BUFFER).observe(this) { error ->
       error?.let {
         when (it) {
           is Error.HandshakeError -> it.message.let {
@@ -801,7 +721,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
           }
         }
       }
-    })
+    }
 
     val isConnected: Observable<Boolean> = modelHelper.connectionProgress.map { (state, _, _) ->
       state == ConnectionState.CONNECTED
@@ -898,7 +818,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
 
     // Show Connection Progress Bar
     combineLatest(modelHelper.connectionProgress, modelHelper.deceptiveNetwork)
-      .toLiveData().observe(this, Observer {
+      .toLiveData().observe(this) {
         val (connection, deceptive) = it ?: Pair(Triple(ConnectionState.DISCONNECTED, 0, 0), false)
         val (state, progress, max) = connection
         when (state) {
@@ -949,10 +869,10 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
             }
           }
         }
-      })
+      }
 
     // Only show nick list when we’re in a channel bufferId
-    modelHelper.bufferDataThrottled.toLiveData().observe(this, Observer {
+    modelHelper.bufferDataThrottled.toLiveData().observe(this) {
       bufferData = it
       if (bufferData?.info?.type?.hasFlag(Buffer_Type.ChannelBuffer) == true) {
         binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.END)
@@ -964,7 +884,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
       }
 
       invalidateOptionsMenu()
-    })
+    }
 
     editorBottomSheet =
       DragInterceptBottomSheetBehavior.from(binding.root.findViewById(R.id.fragment_chatline))
@@ -1003,11 +923,17 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
       it.isNotEmpty()
     }.firstElement().toLiveData().observeForever {
       it?.firstOrNull()?.let { info ->
-        launch(this, bufferId = info.bufferId)
+        openBuffer(bufferId = info.bufferId)
       }
     }
   }
 
+  override fun onDestroy() {
+    linkClickHandler.unregister(this)
+    autocompleteTextHandler.unregister(this)
+    super.onDestroy()
+  }
+
   override fun onNewIntent(intent: Intent?) {
     super.onNewIntent(intent)
     setIntent(intent)
@@ -1255,7 +1181,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
     startedSelection = true
     connectedAccount = AccountId(-1L)
     restoredDrawerState = false
-    ChatActivity.launch(this, bufferId = BufferId.MAX_VALUE)
+    openBuffer(bufferId = BufferId.MAX_VALUE)
     chatViewModel.resetAccount()
   }
 
@@ -1266,16 +1192,133 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
     }
   }
 
+  override fun autocompleteText(
+    text: CharSequence,
+    suffix: String?
+  ) {
+    Log.i("ChatActivity", "autocompleteText text=\"$text\", suffix=\"$suffix\"")
+    chatlineFragment?.editorHelper?.appendText(
+      text,
+      suffix,
+    )
+    binding.drawerLayout.closeDrawers()
+  }
+
+  override fun openBuffer(
+    bufferId: BufferId,
+    accountId: AccountId?,
+    forceJoin: Boolean
+  ) {
+    Log.i("ChatActivity", "openBuffer bufferId=$bufferId, accountId=$accountId, forceJoin=$forceJoin")
+    chatViewModel.bufferId.onNext(bufferId)
+    chatViewModel.bufferOpened.onNext(Unit)
+    if (accountId != null && accountId != this.accountId) {
+      resetAccount()
+      connectToAccount(accountId)
+      startedSelection = false
+      connectedAccount = AccountId(-1L)
+      checkConnection()
+      recreate()
+    }
+  }
+
+  override fun openChannel(
+    networkId: NetworkId,
+    channel: String,
+    forceJoin: Boolean
+  ) {
+    Log.i("ChatActivity", "openChannel networkId=$networkId, channel=$channel, forceJoin=$forceJoin")
+    modelHelper.connectedSession.filter(Optional<ISession>::isPresent).firstElement()
+      .subscribe {
+        it.orNull()?.also { session ->
+          val info = session.bufferSyncer.find(
+            bufferName = channel,
+            networkId = networkId,
+            type = Buffer_Type.of(Buffer_Type.ChannelBuffer)
+          )
+
+          if (info != null && !forceJoin) {
+            openBuffer(bufferId = info.bufferId)
+          } else {
+            modelHelper.chat.chatToJoin.onNext(
+              Optional.of(
+                Pair(networkId, channel)
+              )
+            )
+
+            session.bufferSyncer.find(
+              networkId = networkId,
+              type = Buffer_Type.of(Buffer_Type.StatusBuffer)
+            )?.let { statusInfo ->
+              session.rpcHandler.sendInput(
+                statusInfo, "/join $channel"
+              )
+            }
+          }
+        }
+      }
+  }
+
+  override fun openDirectMessage(
+    networkId: NetworkId,
+    nickName: String,
+    forceJoin: Boolean
+  ) {
+    Log.i("ChatActivity", "openDirectMessage networkId=$networkId, nickName=$nickName, forceJoin=$forceJoin")
+    modelHelper.connectedSession.filter(Optional<ISession>::isPresent).firstElement()
+      .subscribe {
+        it.orNull()?.also { session ->
+          val info = session.bufferSyncer.find(
+            bufferName = nickName,
+            networkId = networkId,
+            type = Buffer_Type.of(Buffer_Type.QueryBuffer)
+          )
+
+          if (info != null && !forceJoin) {
+            openBuffer(bufferId = info.bufferId)
+          } else {
+            modelHelper.allBuffers.map {
+              listOfNotNull(it.find {
+                it.networkId == networkId &&
+                  it.bufferName == nickName &&
+                  it.type.hasFlag(Buffer_Type.QueryBuffer)
+              })
+            }.filter {
+              it.isNotEmpty()
+            }.firstElement().toLiveData().observeForever {
+              it?.firstOrNull()?.let { info ->
+                openBuffer(bufferId = info.bufferId)
+              }
+            }
+
+            session.bufferSyncer.find(
+              networkId = networkId,
+              type = Buffer_Type.of(Buffer_Type.StatusBuffer)
+            )?.let { statusInfo ->
+              session.rpcHandler.sendInput(
+                statusInfo, "/query $nickName"
+              )
+            }
+          }
+        }
+      }
+  }
+
+  override fun openUrl(url: String) {
+    Log.i("ChatActivity", "openUrl url=$url")
+    try {
+      startActivity(Intent(Intent.ACTION_VIEW).apply {
+        data = Uri.parse(url)
+      })
+    } catch (e: ActivityNotFoundException) {
+      Log.w("ChatActivity", "Unable to open URL: Activity was not found for $url")
+    }
+  }
+
   companion object {
     // Intent keys
-    private const val KEY_AUTOCOMPLETE_TEXT = "autocomplete_text"
-    private const val KEY_AUTOCOMPLETE_SUFFIX = "autocomplete_suffix"
     private const val KEY_BUFFER_ID = "buffer_id"
     private const val KEY_ACCOUNT_ID = "account_id"
-    private const val KEY_NETWORK_ID = "network_id"
-    private const val KEY_CHANNEL = "channel"
-    private const val KEY_NICK_NAME = "nick_name"
-    private const val KEY_FORCE_JOIN = "force_join"
 
     // Instance state keys
     private const val KEY_OPEN_BUFFER = "open_buffer"
@@ -1284,78 +1327,25 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
     private const val KEY_OPEN_DRAWER_START = "open_drawer_start"
     private const val KEY_OPEN_DRAWER_END = "open_drawer_end"
 
-    fun launch(
-      context: Context,
-      sharedText: CharSequence? = null,
-      autoCompleteText: CharSequence? = null,
-      autoCompleteSuffix: String? = null,
-      channel: String? = null,
-      nickName: String? = null,
-      networkId: NetworkId? = null,
-      bufferId: BufferId? = null,
-      accountId: Long? = null,
-      forceJoin: Boolean? = null
-    ) = context.startActivity(
-      intent(
-        context,
-        sharedText,
-        autoCompleteText,
-        autoCompleteSuffix,
-        channel,
-        nickName,
-        networkId,
-        bufferId,
-        accountId,
-        forceJoin
-      )
-    )
-
     fun intent(
       context: Context,
-      sharedText: CharSequence? = null,
-      autoCompleteText: CharSequence? = null,
-      autoCompleteSuffix: String? = null,
-      channel: String? = null,
-      nickName: String? = null,
-      networkId: NetworkId? = null,
       bufferId: BufferId? = null,
       accountId: Long? = null,
-      forceJoin: Boolean? = null
     ) = Intent(context, ChatActivity::class.java).apply {
-      if (sharedText != null) {
-        type = "text/plain"
-        putExtra(Intent.EXTRA_TEXT, sharedText)
-      }
-      if (autoCompleteText != null) {
-        putExtra(KEY_AUTOCOMPLETE_TEXT, autoCompleteText)
-        if (autoCompleteSuffix != null) {
-          putExtra(KEY_AUTOCOMPLETE_SUFFIX, autoCompleteSuffix)
-        }
-      }
       if (bufferId != null) {
         putExtra(KEY_BUFFER_ID, bufferId.id)
         if (accountId != null) {
           putExtra(KEY_ACCOUNT_ID, accountId)
         }
       }
-      if (networkId != null && channel != null) {
-        putExtra(KEY_NETWORK_ID, networkId.id)
-        putExtra(KEY_CHANNEL, channel)
-      } else if (networkId != null && nickName != null) {
-        putExtra(KEY_NETWORK_ID, networkId.id)
-        putExtra(KEY_NICK_NAME, nickName)
-        if (forceJoin != null) {
-          putExtra(KEY_NICK_NAME, nickName)
-        }
-      }
     }
   }
 
   sealed class PostConnectionState {
-    object Connecting : PostConnectionState()
-    object SetupIdentity : PostConnectionState()
+    data object Connecting : PostConnectionState()
+    data object SetupIdentity : PostConnectionState()
     data class MissingFeatures(val features: List<MissingFeature>) : PostConnectionState()
-    object RequestingPermissions : PostConnectionState()
-    object Done : PostConnectionState()
+    data object RequestingPermissions : PostConnectionState()
+    data object Done : PostConnectionState()
   }
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatActivityModule.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatActivityModule.kt
index a29bd4342..c088b3657 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatActivityModule.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatActivityModule.kt
@@ -25,7 +25,7 @@ import de.kuschku.quasseldroid.ui.chat.messages.MessageRenderer
 import de.kuschku.quasseldroid.ui.chat.messages.QuasselMessageRenderer
 
 @Module
-abstract class ChatActivityModule {
+interface ChatActivityModule {
   @Binds
-  abstract fun bindMessageRenderer(messageRenderer: QuasselMessageRenderer): MessageRenderer
+  fun bindMessageRenderer(messageRenderer: QuasselMessageRenderer): MessageRenderer
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatFragmentProvider.kt
index a38d77d20..e1c97f8cd 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatFragmentProvider.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatFragmentProvider.kt
@@ -29,22 +29,22 @@ import de.kuschku.quasseldroid.ui.chat.messages.MessageListFragment
 import de.kuschku.quasseldroid.ui.chat.nicks.NickListFragment
 
 @Module
-abstract class ChatFragmentProvider {
+interface ChatFragmentProvider {
   @Binds
-  abstract fun bindFragmentActivity(activity: ChatActivity): FragmentActivity
+  fun bindFragmentActivity(activity: ChatActivity): FragmentActivity
 
   @ContributesAndroidInjector
-  abstract fun bindBufferViewConfigFragment(): BufferViewConfigFragment
+  fun bindBufferViewConfigFragment(): BufferViewConfigFragment
 
   @ContributesAndroidInjector
-  abstract fun bindMessageListFragment(): MessageListFragment
+  fun bindMessageListFragment(): MessageListFragment
 
   @ContributesAndroidInjector
-  abstract fun bindNickListFragment(): NickListFragment
+  fun bindNickListFragment(): NickListFragment
 
   @ContributesAndroidInjector
-  abstract fun bindToolbarFragment(): ToolbarFragment
+  fun bindToolbarFragment(): ToolbarFragment
 
   @ContributesAndroidInjector
-  abstract fun bindChatlineFragment(): ChatlineFragment
+  fun bindChatlineFragment(): ChatlineFragment
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ToolbarFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ToolbarFragment.kt
index 10292d879..fa2b5fe77 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ToolbarFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ToolbarFragment.kt
@@ -19,15 +19,14 @@
 
 package de.kuschku.quasseldroid.ui.chat
 
+import android.content.Context
 import android.os.Bundle
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
 import android.widget.TextView
 import androidx.appcompat.widget.AppCompatImageView
-import androidx.lifecycle.Observer
 import com.bumptech.glide.Glide
-
 import de.kuschku.libquassel.protocol.Buffer_Type
 import de.kuschku.libquassel.quassel.BufferInfo
 import de.kuschku.libquassel.util.flag.hasFlag
@@ -45,6 +44,8 @@ import de.kuschku.quasseldroid.util.helper.setTooltip
 import de.kuschku.quasseldroid.util.helper.toLiveData
 import de.kuschku.quasseldroid.util.helper.visibleIf
 import de.kuschku.quasseldroid.util.irc.format.IrcFormatDeserializer
+import de.kuschku.quasseldroid.util.listener.LinkClickListener
+import de.kuschku.quasseldroid.util.listener.QuasselLinkClickListener
 import de.kuschku.quasseldroid.util.service.ServiceBoundFragment
 import de.kuschku.quasseldroid.util.ui.SpanFormatter
 import de.kuschku.quasseldroid.viewmodel.helper.EditorViewModelHelper
@@ -71,6 +72,10 @@ class ToolbarFragment : ServiceBoundFragment() {
   @Inject
   lateinit var messageSettings: MessageSettings
 
+  @Inject
+  lateinit var internalLinkClickListener: LinkClickListener
+  lateinit var linkClickListener: LinkClickListener
+
   var title: CharSequence?
     get() = toolbarTitle.text
     set(value) {
@@ -87,6 +92,17 @@ class ToolbarFragment : ServiceBoundFragment() {
       toolbarSubtitle.visibleIf(value?.isNotEmpty() == true)
     }
 
+  override fun onAttach(context: Context) {
+    super.onAttach(context)
+    linkClickListener = QuasselLinkClickListener(internalLinkClickListener) {
+      activity?.let {
+        if (it !is ChatActivity) {
+          it.finish()
+        }
+      }
+    }
+  }
+
   override fun onCreateView(
     inflater: LayoutInflater,
     container: ViewGroup?,
@@ -99,7 +115,9 @@ class ToolbarFragment : ServiceBoundFragment() {
     this.actionArea = view.findViewById(R.id.toolbar_action_area)
 
     fun colorizeDescription(description: String?) = ircFormatDeserializer.formatString(
-      description, messageSettings.colorizeMirc
+      description,
+      messageSettings.colorizeMirc,
+      linkClickListener,
     )
 
     val avatarSize = resources.getDimensionPixelSize(R.dimen.avatar_size_buffer)
@@ -125,7 +143,7 @@ class ToolbarFragment : ServiceBoundFragment() {
 
       Triple(it.first, it.second, avatarInfo)
     }.toLiveData()
-      .observe(viewLifecycleOwner, Observer {
+      .observe(viewLifecycleOwner) {
         if (it != null) {
           val (data, lag, avatarInfo) = it
 
@@ -159,7 +177,7 @@ class ToolbarFragment : ServiceBoundFragment() {
             }
           }
         }
-      })
+      }
 
     actionArea.setOnClickListener {
       val bufferData = modelHelper.bufferData.value
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/create/ChannelCreateFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/create/ChannelCreateFragment.kt
index b51d438e6..1686b8622 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/create/ChannelCreateFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/create/ChannelCreateFragment.kt
@@ -19,6 +19,7 @@
 
 package de.kuschku.quasseldroid.ui.chat.add.create
 
+import android.content.Context
 import android.os.Bundle
 import android.view.LayoutInflater
 import android.view.View
@@ -28,7 +29,6 @@ import android.widget.Button
 import android.widget.EditText
 import androidx.appcompat.widget.AppCompatSpinner
 import androidx.appcompat.widget.SwitchCompat
-import androidx.lifecycle.Observer
 import de.kuschku.libquassel.protocol.Buffer_Type
 import de.kuschku.libquassel.protocol.NetworkId
 import de.kuschku.libquassel.quassel.syncables.IrcChannel
@@ -43,10 +43,11 @@ import de.kuschku.quasseldroid.ui.chat.add.NetworkAdapter
 import de.kuschku.quasseldroid.ui.chat.add.NetworkItem
 import de.kuschku.quasseldroid.util.helper.setDependent
 import de.kuschku.quasseldroid.util.helper.toLiveData
+import de.kuschku.quasseldroid.util.listener.LinkClickListener
+import de.kuschku.quasseldroid.util.listener.QuasselLinkClickListener
 import de.kuschku.quasseldroid.util.ui.settings.fragment.ServiceBoundSettingsFragment
 import de.kuschku.quasseldroid.viewmodel.helper.QuasselViewModelHelper
 import io.reactivex.Observable
-import io.reactivex.disposables.Disposable
 import javax.inject.Inject
 
 class ChannelCreateFragment : ServiceBoundSettingsFragment() {
@@ -62,9 +63,24 @@ class ChannelCreateFragment : ServiceBoundSettingsFragment() {
   @Inject
   lateinit var modelHelper: QuasselViewModelHelper
 
+  @Inject
+  lateinit var internalLinkClickListener: LinkClickListener
+  lateinit var linkClickListener: LinkClickListener
+
   private var hasSelectedNetwork = false
   private var networkId = NetworkId(0)
 
+  override fun onAttach(context: Context) {
+    super.onAttach(context)
+    linkClickListener = QuasselLinkClickListener(internalLinkClickListener) {
+      activity?.let {
+        if (it !is ChatActivity) {
+          it.finish()
+        }
+      }
+    }
+  }
+
   override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                             savedInstanceState: Bundle?): View? {
     val view = inflater.inflate(R.layout.add_create, container, false)
@@ -105,7 +121,7 @@ class ChannelCreateFragment : ServiceBoundSettingsFragment() {
           NetworkItem(it.networkId, it.networkName)
         }.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, NetworkItem::name))
       }
-    }.toLiveData().observe(viewLifecycleOwner, Observer {
+    }.toLiveData().observe(viewLifecycleOwner) {
       if (it != null) {
         networkAdapter.submitList(it)
         if (!hasSetNetwork && networkId.isValidId() && it.isNotEmpty()) {
@@ -119,7 +135,7 @@ class ChannelCreateFragment : ServiceBoundSettingsFragment() {
           hasSetNetwork = true
         }
       }
-    })
+    }
 
     passwordProtected.setDependent(passwordGroup)
 
@@ -156,12 +172,7 @@ class ChannelCreateFragment : ServiceBoundSettingsFragment() {
             }
           }
 
-          activity?.let {
-            it.finish()
-            ChatActivity.launch(it,
-                                bufferId = existingBuffer.bufferId
-            )
-          }
+          linkClickListener.openBuffer(bufferId = existingBuffer.bufferId)
         } else {
           bufferSyncer.find(
             networkId = selectedNetworkId,
@@ -189,7 +200,7 @@ class ChannelCreateFragment : ServiceBoundSettingsFragment() {
 
                 activity?.let {
                   it.finish()
-                  ChatActivity.launch(it,
+                  linkClickListener.openChannel(
                     networkId = selectedNetworkId,
                     channel = channelName
                   )
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/create/ChannelCreateFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/create/ChannelCreateFragmentProvider.kt
index be6c9a8f3..dc8dfc67c 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/create/ChannelCreateFragmentProvider.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/create/ChannelCreateFragmentProvider.kt
@@ -25,10 +25,10 @@ import dagger.Module
 import dagger.android.ContributesAndroidInjector
 
 @Module
-abstract class ChannelCreateFragmentProvider {
+interface ChannelCreateFragmentProvider {
   @Binds
-  abstract fun bindFragmentActivity(activity: ChannelCreateActivity): FragmentActivity
+  fun bindFragmentActivity(activity: ChannelCreateActivity): FragmentActivity
 
   @ContributesAndroidInjector
-  abstract fun bindChannelCreateFragment(): ChannelCreateFragment
+  fun bindChannelCreateFragment(): ChannelCreateFragment
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/join/ChannelJoinFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/join/ChannelJoinFragment.kt
index c03d2d1e5..c03e74b5e 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/join/ChannelJoinFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/join/ChannelJoinFragment.kt
@@ -19,6 +19,7 @@
 
 package de.kuschku.quasseldroid.ui.chat.add.join
 
+import android.content.Context
 import android.os.Bundle
 import android.view.LayoutInflater
 import android.view.View
@@ -27,7 +28,6 @@ import android.widget.AdapterView
 import android.widget.Button
 import android.widget.EditText
 import androidx.appcompat.widget.AppCompatSpinner
-import androidx.lifecycle.Observer
 import de.kuschku.libquassel.protocol.NetworkId
 import de.kuschku.libquassel.quassel.syncables.Network
 import de.kuschku.libquassel.util.helper.combineLatest
@@ -37,6 +37,8 @@ import de.kuschku.quasseldroid.ui.chat.ChatActivity
 import de.kuschku.quasseldroid.ui.chat.add.NetworkAdapter
 import de.kuschku.quasseldroid.ui.chat.add.NetworkItem
 import de.kuschku.quasseldroid.util.helper.toLiveData
+import de.kuschku.quasseldroid.util.listener.LinkClickListener
+import de.kuschku.quasseldroid.util.listener.QuasselLinkClickListener
 import de.kuschku.quasseldroid.util.service.ServiceBoundFragment
 import de.kuschku.quasseldroid.viewmodel.helper.QuasselViewModelHelper
 import javax.inject.Inject
@@ -49,9 +51,24 @@ class ChannelJoinFragment : ServiceBoundFragment() {
   @Inject
   lateinit var modelHelper: QuasselViewModelHelper
 
+  @Inject
+  lateinit var internalLinkClickListener: LinkClickListener
+  lateinit var linkClickListener: LinkClickListener
+
   private var hasSelectedNetwork = false
   private var networkId = NetworkId(0)
 
+  override fun onAttach(context: Context) {
+    super.onAttach(context)
+    linkClickListener = QuasselLinkClickListener(internalLinkClickListener) {
+      activity?.let {
+        if (it !is ChatActivity) {
+          it.finish()
+        }
+      }
+    }
+  }
+
   override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                             savedInstanceState: Bundle?): View? {
     val view = inflater.inflate(R.layout.add_join, container, false)
@@ -87,7 +104,7 @@ class ChannelJoinFragment : ServiceBoundFragment() {
           NetworkItem(it.networkId, it.networkName)
         }.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, NetworkItem::name))
       }
-    }.toLiveData().observe(viewLifecycleOwner, Observer {
+    }.toLiveData().observe(viewLifecycleOwner) {
       if (it != null) {
         networkAdapter.submitList(it)
         if (!hasSetNetwork && networkId.isValidId() && it.isNotEmpty()) {
@@ -101,24 +118,17 @@ class ChannelJoinFragment : ServiceBoundFragment() {
           hasSetNetwork = true
         }
       }
-    })
+    }
 
     join.setOnClickListener {
       join.setText(R.string.label_saving)
       join.isEnabled = false
 
-      val selectedNetworkId = NetworkId(network.selectedItemId.toInt())
-      val channelName = name.text.toString().trim()
-
-      activity?.let {
-        it.finish()
-        ChatActivity.launch(
-          it,
-          networkId = selectedNetworkId,
-          channel = channelName,
-          forceJoin = true
-        )
-      }
+      linkClickListener.openChannel(
+        networkId = NetworkId(network.selectedItemId.toInt()),
+        channel = name.text.toString().trim(),
+        forceJoin = true
+      )
     }
 
     return view
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/join/ChannelJoinFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/join/ChannelJoinFragmentProvider.kt
index e031f56cb..77c5b3d24 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/join/ChannelJoinFragmentProvider.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/join/ChannelJoinFragmentProvider.kt
@@ -25,10 +25,10 @@ import dagger.Module
 import dagger.android.ContributesAndroidInjector
 
 @Module
-abstract class ChannelJoinFragmentProvider {
+interface ChannelJoinFragmentProvider {
   @Binds
-  abstract fun bindFragmentActivity(activity: ChannelJoinActivity): FragmentActivity
+  fun bindFragmentActivity(activity: ChannelJoinActivity): FragmentActivity
 
   @ContributesAndroidInjector
-  abstract fun bindChannelJoinFragment(): ChannelJoinFragment
+  fun bindChannelJoinFragment(): ChannelJoinFragment
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/query/QueryCreateFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/query/QueryCreateFragment.kt
index 6a4e78aaa..3cd4cded6 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/query/QueryCreateFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/query/QueryCreateFragment.kt
@@ -19,6 +19,7 @@
 
 package de.kuschku.quasseldroid.ui.chat.add.query
 
+import android.content.Context
 import android.os.Bundle
 import android.text.Editable
 import android.text.TextWatcher
@@ -29,7 +30,6 @@ import android.widget.AdapterView
 import android.widget.Button
 import android.widget.EditText
 import androidx.appcompat.widget.AppCompatSpinner
-import androidx.lifecycle.Observer
 import androidx.recyclerview.widget.DefaultItemAnimator
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
@@ -60,6 +60,8 @@ import de.kuschku.quasseldroid.util.helper.styledAttributes
 import de.kuschku.quasseldroid.util.helper.toLiveData
 import de.kuschku.quasseldroid.util.irc.format.ContentFormatter
 import de.kuschku.quasseldroid.util.irc.format.IrcFormatDeserializer
+import de.kuschku.quasseldroid.util.listener.LinkClickListener
+import de.kuschku.quasseldroid.util.listener.QuasselLinkClickListener
 import de.kuschku.quasseldroid.util.service.ServiceBoundFragment
 import de.kuschku.quasseldroid.viewmodel.data.Avatar
 import de.kuschku.quasseldroid.viewmodel.data.IrcUserItem
@@ -69,6 +71,7 @@ import de.kuschku.quasseldroid.viewmodel.helper.QueryCreateViewModelHelper
 import io.reactivex.Observable
 import java.util.concurrent.TimeUnit
 import javax.inject.Inject
+import kotlin.math.min
 
 class QueryCreateFragment : ServiceBoundFragment() {
   lateinit var network: AppCompatSpinner
@@ -88,9 +91,24 @@ class QueryCreateFragment : ServiceBoundFragment() {
   @Inject
   lateinit var contentFormatter: ContentFormatter
 
+  @Inject
+  lateinit var internalLinkClickListener: LinkClickListener
+  lateinit var linkClickListener: LinkClickListener
+
   private var hasSelectedNetwork = false
   private var networkId = NetworkId(0)
 
+  override fun onAttach(context: Context) {
+    super.onAttach(context)
+    linkClickListener = QuasselLinkClickListener(internalLinkClickListener) {
+      activity?.let {
+        if (it !is ChatActivity) {
+          it.finish()
+        }
+      }
+    }
+  }
+
   override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                             savedInstanceState: Bundle?): View? {
     val view = inflater.inflate(R.layout.add_query, container, false)
@@ -129,7 +147,7 @@ class QueryCreateFragment : ServiceBoundFragment() {
           NetworkItem(it.networkId, it.networkName)
         }.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, NetworkItem::name))
       }
-    }.toLiveData().observe(viewLifecycleOwner, Observer {
+    }.toLiveData().observe(viewLifecycleOwner) {
       if (it != null) {
         networkAdapter.submitList(it)
         if (!hasSetNetwork && networkId.isValidId() && it.isNotEmpty()) {
@@ -143,7 +161,7 @@ class QueryCreateFragment : ServiceBoundFragment() {
           hasSetNetwork = true
         }
       }
-    })
+    }
 
     name.addTextChangedListener(object : TextWatcher {
       override fun afterTextChanged(s: Editable?) {
@@ -266,10 +284,12 @@ class QueryCreateFragment : ServiceBoundFragment() {
             MessageSettings.ShowPrefixMode.ALL ->
               it.modes
             else                               ->
-              it.modes.substring(0, Math.min(it.modes.length, 1))
+              it.modes.substring(0, min(it.modes.length, 1))
           },
           realname = ircFormatDeserializer.formatString(
-            it.realname.toString(), messageSettings.colorizeMirc
+            it.realname.toString(),
+            messageSettings.colorizeMirc,
+            linkClickListener,
           ),
           avatarUrls = AvatarHelper.avatar(messageSettings, it, avatarSize)
         )
@@ -279,9 +299,9 @@ class QueryCreateFragment : ServiceBoundFragment() {
       }.sortedBy {
         it.lowestMode
       }.toList()
-    }.toLiveData().observe(viewLifecycleOwner, Observer {
+    }.toLiveData().observe(viewLifecycleOwner) {
       nickListAdapter.submitList(it)
-    })
+    }
 
     query.setOnClickListener {
       val selectedNetworkId = NetworkId(network.selectedItemId.toInt())
@@ -297,15 +317,12 @@ class QueryCreateFragment : ServiceBoundFragment() {
     query.setText(R.string.label_saving)
     query.isEnabled = false
 
-    activity?.let {
-      it.finish()
-      ChatActivity.launch(
-        it,
-        networkId = selectedNetworkId,
-        nickName = nickName,
-        forceJoin = true
-      )
-    }
+    linkClickListener.openDirectMessage(
+      networkId = selectedNetworkId,
+      nickName = nickName,
+      forceJoin = true
+    )
+    activity?.finish()
   }
 
   override fun onSaveInstanceState(outState: Bundle) {
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/query/QueryCreateFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/query/QueryCreateFragmentProvider.kt
index 3f8158210..f000e75b3 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/query/QueryCreateFragmentProvider.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/query/QueryCreateFragmentProvider.kt
@@ -25,10 +25,10 @@ import dagger.Module
 import dagger.android.ContributesAndroidInjector
 
 @Module
-abstract class QueryCreateFragmentProvider {
+interface QueryCreateFragmentProvider {
   @Binds
-  abstract fun bindFragmentActivity(activity: QueryCreateActivity): FragmentActivity
+  fun bindFragmentActivity(activity: QueryCreateActivity): FragmentActivity
 
   @ContributesAndroidInjector
-  abstract fun bindQueryCreateFragment(): QueryCreateFragment
+  fun bindQueryCreateFragment(): QueryCreateFragment
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/archive/ArchiveFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/archive/ArchiveFragment.kt
index 9df3fcca9..3ef8f321f 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/archive/ArchiveFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/archive/ArchiveFragment.kt
@@ -19,10 +19,15 @@
 
 package de.kuschku.quasseldroid.ui.chat.archive
 
+import android.content.Context
 import android.os.Bundle
-import android.view.*
+import android.view.ActionMode
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
 import androidx.appcompat.app.AppCompatActivity
-import androidx.lifecycle.Observer
 import androidx.recyclerview.widget.DefaultItemAnimator
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
@@ -39,6 +44,8 @@ import de.kuschku.quasseldroid.persistence.models.Filtered
 import de.kuschku.quasseldroid.settings.MessageSettings
 import de.kuschku.quasseldroid.ui.chat.ChatActivity
 import de.kuschku.quasseldroid.util.helper.toLiveData
+import de.kuschku.quasseldroid.util.listener.LinkClickListener
+import de.kuschku.quasseldroid.util.listener.QuasselLinkClickListener
 import de.kuschku.quasseldroid.util.service.ServiceBoundFragment
 import de.kuschku.quasseldroid.util.ui.presenter.BufferContextPresenter
 import de.kuschku.quasseldroid.util.ui.presenter.BufferPresenter
@@ -64,6 +71,10 @@ class ArchiveFragment : ServiceBoundFragment() {
   @Inject
   lateinit var messageSettings: MessageSettings
 
+  @Inject
+  lateinit var internalLinkClickListener: LinkClickListener
+  lateinit var linkClickListener: LinkClickListener
+
   private lateinit var listAdapter: ArchiveListAdapter
 
   private var actionMode: ActionMode? = null
@@ -110,6 +121,17 @@ class ArchiveFragment : ServiceBoundFragment() {
     }
   }
 
+  override fun onAttach(context: Context) {
+    super.onAttach(context)
+    linkClickListener = QuasselLinkClickListener(internalLinkClickListener) {
+      activity?.let {
+        if (it !is ChatActivity) {
+          it.finish()
+        }
+      }
+    }
+  }
+
   override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                             savedInstanceState: Bundle?): View? {
     val view = inflater.inflate(R.layout.chat_archive, container, false)
@@ -163,15 +185,15 @@ class ArchiveFragment : ServiceBoundFragment() {
           content = getString(R.string.label_permanently_archived_empty)
         ))
       }
-    }.toLiveData().observe(viewLifecycleOwner, Observer { processedList ->
+    }.toLiveData().observe(viewLifecycleOwner) { processedList ->
       listAdapter.submitList(processedList)
-    })
+    }
 
-    modelHelper.selectedBuffer.toLiveData().observe(viewLifecycleOwner, Observer { buffer ->
+    modelHelper.selectedBuffer.toLiveData().observe(viewLifecycleOwner) { buffer ->
       actionMode?.let {
         BufferContextPresenter.present(it, buffer)
       }
-    })
+    }
 
     return view
   }
@@ -188,9 +210,7 @@ class ArchiveFragment : ServiceBoundFragment() {
     if (actionMode != null) {
       longClickListener(bufferId)
     } else {
-      context?.let {
-        ChatActivity.launch(it, bufferId = bufferId)
-      }
+      linkClickListener.openBuffer(bufferId = bufferId)
     }
   }
 
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/archive/ArchiveFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/archive/ArchiveFragmentProvider.kt
index 0f00b8244..4cbf6582d 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/archive/ArchiveFragmentProvider.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/archive/ArchiveFragmentProvider.kt
@@ -25,10 +25,10 @@ import dagger.Module
 import dagger.android.ContributesAndroidInjector
 
 @Module
-abstract class ArchiveFragmentProvider {
+interface ArchiveFragmentProvider {
   @Binds
-  abstract fun bindFragmentActivity(activity: ArchiveActivity): FragmentActivity
+  fun bindFragmentActivity(activity: ArchiveActivity): FragmentActivity
 
   @ContributesAndroidInjector
-  abstract fun bindArchiveFragment(): ArchiveFragment
+  fun bindArchiveFragment(): ArchiveFragment
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferViewConfigFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferViewConfigFragment.kt
index 77293d7c5..0da794fb9 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferViewConfigFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferViewConfigFragment.kt
@@ -19,18 +19,23 @@
 
 package de.kuschku.quasseldroid.ui.chat.buffers
 
+import android.content.Context
 import android.os.Bundle
 import android.os.Parcelable
 import android.text.Editable
 import android.text.TextWatcher
-import android.view.*
+import android.view.ActionMode
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
 import android.widget.AdapterView
 import android.widget.EditText
 import androidx.annotation.ColorInt
 import androidx.appcompat.widget.AppCompatImageButton
 import androidx.appcompat.widget.AppCompatSpinner
 import androidx.appcompat.widget.Toolbar
-import androidx.lifecycle.Observer
 import androidx.recyclerview.widget.DefaultItemAnimator
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
@@ -40,7 +45,14 @@ import de.kuschku.libquassel.protocol.BufferId
 import de.kuschku.libquassel.protocol.NetworkId
 import de.kuschku.libquassel.quassel.ExtendedFeature
 import de.kuschku.libquassel.quassel.syncables.BufferViewConfig
-import de.kuschku.libquassel.util.helper.*
+import de.kuschku.libquassel.util.helper.combineLatest
+import de.kuschku.libquassel.util.helper.mapMap
+import de.kuschku.libquassel.util.helper.mapOrElse
+import de.kuschku.libquassel.util.helper.nullIf
+import de.kuschku.libquassel.util.helper.or
+import de.kuschku.libquassel.util.helper.safeSwitchMap
+import de.kuschku.libquassel.util.helper.safeValue
+import de.kuschku.libquassel.util.helper.value
 import de.kuschku.quasseldroid.BuildConfig
 import de.kuschku.quasseldroid.R
 import de.kuschku.quasseldroid.persistence.dao.listenDefaultFiltered
@@ -61,6 +73,8 @@ import de.kuschku.quasseldroid.util.helper.styledAttributes
 import de.kuschku.quasseldroid.util.helper.toLiveData
 import de.kuschku.quasseldroid.util.helper.visibleIf
 import de.kuschku.quasseldroid.util.irc.format.IrcFormatDeserializer
+import de.kuschku.quasseldroid.util.listener.LinkClickListener
+import de.kuschku.quasseldroid.util.listener.QuasselLinkClickListener
 import de.kuschku.quasseldroid.util.service.ServiceBoundFragment
 import de.kuschku.quasseldroid.util.ui.presenter.BufferContextPresenter
 import de.kuschku.quasseldroid.util.ui.presenter.BufferPresenter
@@ -102,6 +116,10 @@ class BufferViewConfigFragment : ServiceBoundFragment() {
   @Inject
   lateinit var bufferPresenter: BufferPresenter
 
+  @Inject
+  lateinit var internalLinkClickListener: LinkClickListener
+  lateinit var linkClickListener: LinkClickListener
+
   private var actionMode: ActionMode? = null
 
   private val actionModeCallback = object : ActionMode.Callback {
@@ -147,6 +165,17 @@ class BufferViewConfigFragment : ServiceBoundFragment() {
 
   private lateinit var listAdapter: BufferListAdapter
 
+  override fun onAttach(context: Context) {
+    super.onAttach(context)
+    linkClickListener = QuasselLinkClickListener(internalLinkClickListener) {
+      activity?.let {
+        if (it !is ChatActivity) {
+          it.finish()
+        }
+      }
+    }
+  }
+
   override fun onCreateView(
     inflater: LayoutInflater, container: ViewGroup?,
     savedInstanceState: Bundle?
@@ -164,11 +193,11 @@ class BufferViewConfigFragment : ServiceBoundFragment() {
     val adapter = BufferViewConfigAdapter()
     modelHelper.bufferViewConfigs.safeSwitchMap {
       combineLatest(it.map(BufferViewConfig::liveUpdates))
-    }.toLiveData().observe(viewLifecycleOwner, Observer {
+    }.toLiveData().observe(viewLifecycleOwner) {
       if (it != null) {
         adapter.submitList(it)
       }
-    })
+    }
 
     var hasSetBufferViewConfigId = false
     adapter.setOnUpdateFinishedListener {
@@ -212,14 +241,15 @@ class BufferViewConfigFragment : ServiceBoundFragment() {
       }
     }
 
-    modelHelper.negotiatedFeatures.toLiveData().observe(viewLifecycleOwner,
-                                                        Observer { (connected, features) ->
-                                                          featureContextBufferActivitySync.setMode(
-                                                            if (!connected || features.hasFeature(
-                                                                ExtendedFeature.BufferActivitySync)) WarningBarView.MODE_NONE
-                                                            else WarningBarView.MODE_ICON
-                                                          )
-                                                        })
+    modelHelper.negotiatedFeatures.toLiveData().observe(viewLifecycleOwner) { (connected, features) ->
+      featureContextBufferActivitySync.setMode(
+        if (!connected || features.hasFeature(
+            ExtendedFeature.BufferActivitySync
+          )
+        ) WarningBarView.MODE_NONE
+        else WarningBarView.MODE_ICON
+      )
+    }
 
     val filtered = combineLatest(
       database.filtered().listenRx(accountId).toObservable().map {
@@ -230,23 +260,23 @@ class BufferViewConfigFragment : ServiceBoundFragment() {
 
     modelHelper.processChatBufferList(filtered).map { buffers ->
       bufferPresenter.render(buffers)
-    }.toLiveData().observe(viewLifecycleOwner, Observer { processedList ->
+    }.toLiveData().observe(viewLifecycleOwner) { processedList ->
       if (hasRestoredChatListState) {
         chatListState = chatList.layoutManager?.onSaveInstanceState()
         hasRestoredChatListState = false
       }
       listAdapter.submitList(processedList)
-    })
+    }
 
     listAdapter.setOnClickListener(this@BufferViewConfigFragment::clickListener)
     listAdapter.setOnLongClickListener(this@BufferViewConfigFragment::longClickListener)
     chatList.adapter = listAdapter
 
-    modelHelper.selectedBuffer.toLiveData().observe(viewLifecycleOwner, Observer { buffer ->
+    modelHelper.selectedBuffer.toLiveData().observe(viewLifecycleOwner) { buffer ->
       actionMode?.let {
         BufferContextPresenter.present(it, buffer)
       }
-    })
+    }
 
     chatListToolbar.inflateMenu(R.menu.context_bufferlist)
     chatListToolbar.menu.findItem(R.id.action_search).isChecked = modelHelper.chat.bufferSearchTemporarilyVisible.or(
@@ -276,10 +306,10 @@ class BufferViewConfigFragment : ServiceBoundFragment() {
     chatList.itemAnimator = DefaultItemAnimator()
     chatList.setItemViewCacheSize(10)
 
-    modelHelper.chat.stateReset.toLiveData().observe(viewLifecycleOwner, Observer {
+    modelHelper.chat.stateReset.toLiveData().observe(viewLifecycleOwner) {
       listAdapter.submitList(emptyList())
       hasSetBufferViewConfigId = false
-    })
+    }
 
     val bufferSearchPermanentlyVisible = modelHelper.bufferViewConfig
       .mapMap(BufferViewConfig::showSearch)
@@ -287,7 +317,7 @@ class BufferViewConfigFragment : ServiceBoundFragment() {
 
     combineLatest(modelHelper.chat.bufferSearchTemporarilyVisible.distinctUntilChanged(),
                   bufferSearchPermanentlyVisible)
-      .toLiveData().observe(viewLifecycleOwner, Observer { (temporarily, permanently) ->
+      .toLiveData().observe(viewLifecycleOwner) { (temporarily, permanently) ->
         val visible = temporarily || permanently
 
         val menuItem = chatListToolbar.menu.findItem(R.id.action_search)
@@ -297,7 +327,7 @@ class BufferViewConfigFragment : ServiceBoundFragment() {
 
         bufferSearchContainer.visibleIf(visible)
         if (!visible) bufferSearch.setText("")
-      })
+      }
 
     bufferSearch.addTextChangedListener(object : TextWatcher {
       override fun afterTextChanged(s: Editable) {
@@ -411,10 +441,8 @@ class BufferViewConfigFragment : ServiceBoundFragment() {
     if (actionMode != null) {
       longClickListener(bufferId)
     } else {
-      context?.let {
-        modelHelper.chat.bufferSearchTemporarilyVisible.onNext(false)
-        ChatActivity.launch(it, bufferId = bufferId)
-      }
+      modelHelper.chat.bufferSearchTemporarilyVisible.onNext(false)
+      linkClickListener.openBuffer(bufferId = bufferId)
     }
   }
 
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/AutoCompleteHelper.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/AutoCompleteHelper.kt
index bdfe45ac2..f9eb517b7 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/AutoCompleteHelper.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/AutoCompleteHelper.kt
@@ -20,7 +20,6 @@
 package de.kuschku.quasseldroid.ui.chat.input
 
 import androidx.fragment.app.FragmentActivity
-import androidx.lifecycle.Observer
 import de.kuschku.libquassel.protocol.BufferId
 import de.kuschku.libquassel.protocol.Buffer_Type
 import de.kuschku.libquassel.quassel.syncables.IrcChannel
@@ -43,6 +42,7 @@ import de.kuschku.quasseldroid.util.helper.styledAttributes
 import de.kuschku.quasseldroid.util.helper.toLiveData
 import de.kuschku.quasseldroid.util.irc.format.ContentFormatter
 import de.kuschku.quasseldroid.util.irc.format.IrcFormatDeserializer
+import de.kuschku.quasseldroid.util.listener.LinkClickListener
 import de.kuschku.quasseldroid.viewmodel.data.AutoCompleteItem
 import de.kuschku.quasseldroid.viewmodel.data.BufferStatus
 import de.kuschku.quasseldroid.viewmodel.helper.EditorViewModelHelper
@@ -53,6 +53,7 @@ class AutoCompleteHelper(
   private val autoCompleteSettings: AutoCompleteSettings,
   private val messageSettings: MessageSettings,
   private val ircFormatDeserializer: IrcFormatDeserializer,
+  private val linkClickListener: LinkClickListener,
   private val contentFormatter: ContentFormatter,
   private val helper: EditorViewModelHelper,
   private val emojiHandler: EmojiHandler,
@@ -86,32 +87,32 @@ class AutoCompleteHelper(
   private val colorContext = ColorContext(activity, messageSettings)
 
   init {
-    helper.autoCompleteData.toLiveData().observe(activity, Observer {
+    helper.autoCompleteData.toLiveData().observe(activity) {
       val query = it?.first ?: ""
       val shouldShowResults =
         (autoCompleteSettings.auto && query.length >= 3) ||
-        (autoCompleteSettings.prefix && autoCompleteSettings.nicks && query.startsWith('@')) ||
-        (autoCompleteSettings.prefix && autoCompleteSettings.buffers && query.startsWith('#')) ||
-        (autoCompleteSettings.prefix && autoCompleteSettings.aliases && query.startsWith('/')) ||
-        (autoCompleteSettings.prefix && autoCompleteSettings.emoji && query.startsWith(':'))
+          (autoCompleteSettings.prefix && autoCompleteSettings.nicks && query.startsWith('@')) ||
+          (autoCompleteSettings.prefix && autoCompleteSettings.buffers && query.startsWith('#')) ||
+          (autoCompleteSettings.prefix && autoCompleteSettings.aliases && query.startsWith('/')) ||
+          (autoCompleteSettings.prefix && autoCompleteSettings.emoji && query.startsWith(':'))
       val list = if (shouldShowResults) it?.second.orEmpty() else emptyList()
       val data = list.filter {
         it is AutoCompleteItem.AliasItem && autoCompleteSettings.aliases ||
-        it is AutoCompleteItem.UserItem && autoCompleteSettings.nicks ||
-        it is AutoCompleteItem.ChannelItem && autoCompleteSettings.buffers ||
-        it is AutoCompleteItem.EmojiItem && autoCompleteSettings.emoji
+          it is AutoCompleteItem.UserItem && autoCompleteSettings.nicks ||
+          it is AutoCompleteItem.ChannelItem && autoCompleteSettings.buffers ||
+          it is AutoCompleteItem.EmojiItem && autoCompleteSettings.emoji
       }.map {
         when (it) {
-          is AutoCompleteItem.UserItem    -> {
+          is AutoCompleteItem.UserItem -> {
             val nickName = it.nick
             val senderColorIndex = SenderColorUtil.senderColor(nickName)
             val rawInitial = nickName.trimStart(*IGNORED_CHARS).firstOrNull()
-                             ?: nickName.firstOrNull()
+              ?: nickName.firstOrNull()
             val initial = rawInitial?.uppercase().toString()
             val useSelfColor = when (messageSettings.colorizeNicknames) {
-              MessageSettings.SenderColorMode.ALL          -> false
+              MessageSettings.SenderColorMode.ALL -> false
               MessageSettings.SenderColorMode.ALL_BUT_MINE -> it.self
-              MessageSettings.SenderColorMode.NONE         -> true
+              MessageSettings.SenderColorMode.NONE -> true
             }
             val senderColor = if (useSelfColor) selfColor else senderColors[senderColorIndex]
             it.copy(
@@ -120,15 +121,19 @@ class AutoCompleteHelper(
               modes = when (messageSettings.showPrefix) {
                 MessageSettings.ShowPrefixMode.ALL ->
                   it.modes
-                else                               ->
+
+                else ->
                   it.modes.substring(0, Math.min(it.modes.length, 1))
               },
               realname = ircFormatDeserializer.formatString(
-                it.realname.toString(), messageSettings.colorizeMirc
+                it.realname.toString(),
+                messageSettings.colorizeMirc,
+                linkClickListener,
               ),
               avatarUrls = AvatarHelper.avatar(messageSettings, it)
             )
           }
+
           is AutoCompleteItem.ChannelItem -> {
             val color = if (it.bufferStatus == BufferStatus.ONLINE) colorAccent
             else colorAway
@@ -137,13 +142,14 @@ class AutoCompleteHelper(
               icon = colorContext.buildTextDrawable("#", color)
             )
           }
-          else                            -> it
+
+          else -> it
         }
       }
       for (dataListener in dataListeners) {
         dataListener(data)
       }
-    })
+    }
   }
 
   fun setAutocompleteListener(listener: ((AutoCompletionState) -> Unit)?) {
@@ -233,7 +239,7 @@ class AutoCompleteHelper(
         val prefixModes = network.prefixModes()
 
         val lowestMode = userModes.mapNotNull(prefixModes::indexOf).minOrNull()
-                         ?: prefixModes.size
+          ?: prefixModes.size
 
         AutoCompleteItem.UserItem(
           user.nick(),
@@ -271,9 +277,9 @@ class AutoCompleteHelper(
           fullAutoComplete(sessionOptional, id, lastWord)
         }?.filter {
           it is AutoCompleteItem.AliasItem && autoCompleteSettings.aliases ||
-          it is AutoCompleteItem.UserItem && autoCompleteSettings.nicks ||
-          it is AutoCompleteItem.ChannelItem && autoCompleteSettings.buffers ||
-          it is AutoCompleteItem.EmojiItem && autoCompleteSettings.emoji
+            it is AutoCompleteItem.UserItem && autoCompleteSettings.nicks ||
+            it is AutoCompleteItem.ChannelItem && autoCompleteSettings.buffers ||
+            it is AutoCompleteItem.EmojiItem && autoCompleteSettings.emoji
         }.orEmpty()
 
         if (previous != null && originalWord.first == previous.originalWord && originalWord.second.start == previous.range.start) {
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/ChatlineFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/ChatlineFragment.kt
index d00526885..6ada9bd27 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/ChatlineFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/ChatlineFragment.kt
@@ -19,6 +19,7 @@
 
 package de.kuschku.quasseldroid.ui.chat.input
 
+import android.content.Context
 import android.os.Bundle
 import android.text.SpannableString
 import android.text.Spanned
@@ -38,11 +39,18 @@ import de.kuschku.quasseldroid.R
 import de.kuschku.quasseldroid.settings.AppearanceSettings
 import de.kuschku.quasseldroid.settings.AutoCompleteSettings
 import de.kuschku.quasseldroid.settings.MessageSettings
+import de.kuschku.quasseldroid.ui.chat.ChatActivity
 import de.kuschku.quasseldroid.util.emoji.EmojiHandler
-import de.kuschku.quasseldroid.util.helper.*
+import de.kuschku.quasseldroid.util.helper.lineSequence
+import de.kuschku.quasseldroid.util.helper.retint
+import de.kuschku.quasseldroid.util.helper.setTooltip
+import de.kuschku.quasseldroid.util.helper.toLiveData
+import de.kuschku.quasseldroid.util.helper.visibleIf
 import de.kuschku.quasseldroid.util.irc.format.ContentFormatter
 import de.kuschku.quasseldroid.util.irc.format.IrcFormatDeserializer
 import de.kuschku.quasseldroid.util.irc.format.IrcFormatSerializer
+import de.kuschku.quasseldroid.util.listener.LinkClickListener
+import de.kuschku.quasseldroid.util.listener.QuasselLinkClickListener
 import de.kuschku.quasseldroid.util.service.ServiceBoundFragment
 import de.kuschku.quasseldroid.viewmodel.helper.EditorViewModelHelper
 import javax.inject.Inject
@@ -76,6 +84,10 @@ class ChatlineFragment : ServiceBoundFragment() {
   @Inject
   lateinit var ircFormatSerializer: IrcFormatSerializer
 
+  @Inject
+  lateinit var internalLinkClickListener: LinkClickListener
+  lateinit var linkClickListener: LinkClickListener
+
   @Inject
   lateinit var autoCompleteAdapter: AutoCompleteAdapter
 
@@ -99,6 +111,17 @@ class ChatlineFragment : ServiceBoundFragment() {
     }
   }
 
+  override fun onAttach(context: Context) {
+    super.onAttach(context)
+    linkClickListener = QuasselLinkClickListener(internalLinkClickListener) {
+      activity?.let {
+        if (it !is ChatActivity) {
+          it.finish()
+        }
+      }
+    }
+  }
+
   override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                             savedInstanceState: Bundle?): View? {
     val view = inflater.inflate(R.layout.chat_chatline, container, false)
@@ -117,6 +140,7 @@ class ChatlineFragment : ServiceBoundFragment() {
       autoCompleteSettings,
       messageSettings,
       ircFormatDeserializer,
+      linkClickListener,
       contentFormatter,
       modelHelper,
       emojiHandler
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 1545b6ec3..af87047a2 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
@@ -26,8 +26,12 @@ import android.content.Intent
 import android.os.Bundle
 import android.text.SpannableStringBuilder
 import android.util.TypedValue
-import android.view.*
-import androidx.lifecycle.Observer
+import android.view.ActionMode
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
 import androidx.paging.LivePagedListBuilder
 import androidx.paging.PagedList
 import androidx.recyclerview.widget.LinearLayoutManager
@@ -39,25 +43,51 @@ import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader
 import com.bumptech.glide.util.FixedPreloadSizeProvider
 import com.google.android.material.floatingactionbutton.FloatingActionButton
 import de.kuschku.libquassel.connection.ConnectionState
-import de.kuschku.libquassel.protocol.*
+import de.kuschku.libquassel.protocol.BufferId
+import de.kuschku.libquassel.protocol.Buffer_Type
+import de.kuschku.libquassel.protocol.Message_Type
+import de.kuschku.libquassel.protocol.MsgId
+import de.kuschku.libquassel.protocol.NetworkId
 import de.kuschku.libquassel.quassel.BufferInfo
 import de.kuschku.libquassel.quassel.syncables.BufferSyncer
 import de.kuschku.libquassel.session.SessionManager
 import de.kuschku.libquassel.util.flag.hasFlag
-import de.kuschku.libquassel.util.helper.*
+import de.kuschku.libquassel.util.helper.combineLatest
+import de.kuschku.libquassel.util.helper.invoke
+import de.kuschku.libquassel.util.helper.mapSwitchMap
+import de.kuschku.libquassel.util.helper.nullIf
+import de.kuschku.libquassel.util.helper.safeValue
+import de.kuschku.libquassel.util.helper.value
 import de.kuschku.libquassel.util.irc.HostmaskHelper
 import de.kuschku.quasseldroid.R
-import de.kuschku.quasseldroid.persistence.dao.*
+import de.kuschku.quasseldroid.persistence.dao.findByBufferIdPaged
+import de.kuschku.quasseldroid.persistence.dao.findById
+import de.kuschku.quasseldroid.persistence.dao.findFirstByBufferId
+import de.kuschku.quasseldroid.persistence.dao.get
+import de.kuschku.quasseldroid.persistence.dao.hasVisibleMessages
+import de.kuschku.quasseldroid.persistence.dao.lastMsgId
+import de.kuschku.quasseldroid.persistence.dao.listen
 import de.kuschku.quasseldroid.persistence.db.AccountDatabase
 import de.kuschku.quasseldroid.persistence.db.QuasselDatabase
 import de.kuschku.quasseldroid.persistence.models.MessageData
 import de.kuschku.quasseldroid.service.BacklogRequester
-import de.kuschku.quasseldroid.settings.*
-import de.kuschku.quasseldroid.ui.chat.ChatActivity
+import de.kuschku.quasseldroid.settings.AppearanceSettings
+import de.kuschku.quasseldroid.settings.AutoCompleteSettings
+import de.kuschku.quasseldroid.settings.BacklogSettings
+import de.kuschku.quasseldroid.settings.MessageSettings
+import de.kuschku.quasseldroid.settings.RedirectionSettings
 import de.kuschku.quasseldroid.ui.info.user.UserInfoActivity
 import de.kuschku.quasseldroid.util.Patterns
 import de.kuschku.quasseldroid.util.avatars.AvatarHelper
-import de.kuschku.quasseldroid.util.helper.*
+import de.kuschku.quasseldroid.util.helper.loadWithFallbacks
+import de.kuschku.quasseldroid.util.helper.mapReverse
+import de.kuschku.quasseldroid.util.helper.safeSwitchMap
+import de.kuschku.quasseldroid.util.helper.styledAttributes
+import de.kuschku.quasseldroid.util.helper.switchMapNotNull
+import de.kuschku.quasseldroid.util.helper.toLiveData
+import de.kuschku.quasseldroid.util.helper.toggle
+import de.kuschku.quasseldroid.util.helper.zip
+import de.kuschku.quasseldroid.util.listener.AutocompleteTextListener
 import de.kuschku.quasseldroid.util.service.ServiceBoundFragment
 import de.kuschku.quasseldroid.util.ui.LinkLongClickMenuHelper
 import de.kuschku.quasseldroid.util.ui.SpanFormatter
@@ -105,6 +135,9 @@ class MessageListFragment : ServiceBoundFragment() {
   @Inject
   lateinit var modelHelper: ChatViewModelHelper
 
+  @Inject
+  lateinit var autocompleteTextListener: AutocompleteTextListener
+
   private lateinit var linearLayoutManager: LinearLayoutManager
 
   private lateinit var backlogRequester: BacklogRequester
@@ -270,11 +303,7 @@ class MessageListFragment : ServiceBoundFragment() {
     }
     if (autoCompleteSettings.senderDoubleClick)
       adapter.setOnDoubleClickListener { msg ->
-        ChatActivity.launch(
-          requireContext(),
-          autoCompleteText = HostmaskHelper.nick(msg.sender),
-          autoCompleteSuffix = ": "
-        )
+        autocompleteTextListener.autocompleteText(HostmaskHelper.nick(msg.sender), ": ")
       }
     adapter.setOnSenderIconClickListener { msg ->
       modelHelper.connectedSession.value?.orNull()?.bufferSyncer?.let { bufferSyncer ->
@@ -402,35 +431,39 @@ class MessageListFragment : ServiceBoundFragment() {
       database.message().lastMsgId(it)
     }
 
-    modelHelper.chat.bufferId.toLiveData().observe(viewLifecycleOwner, Observer { bufferId ->
+    modelHelper.chat.bufferId.toLiveData().observe(viewLifecycleOwner) { bufferId ->
       swipeRefreshLayout.isEnabled = (bufferId != null || bufferId?.isValidId() == true)
-    })
+    }
 
     combineLatest(modelHelper.bufferSyncer,
                   modelHelper.sessionManager.mapSwitchMap(SessionManager::state).distinctUntilChanged())
-      .toLiveData().observe(viewLifecycleOwner, Observer { (bufferSyncer, state) ->
+      .toLiveData().observe(viewLifecycleOwner) { (bufferSyncer, state) ->
         if (state?.orNull() == ConnectionState.CONNECTED) {
           runInBackgroundDelayed(16) {
             modelHelper.chat.bufferId { bufferId ->
               val currentNetwork = bufferSyncer.orNull()?.bufferInfo(bufferId)?.networkId
-                                   ?: NetworkId(0)
+                ?: NetworkId(0)
               val currentServerBuffer = bufferSyncer.orNull()?.find(
                 networkId = currentNetwork,
                 type = Buffer_Type.of(Buffer_Type.StatusBuffer)
               )?.bufferId ?: BufferId(0)
 
-              val filtered = database.filtered().get(accountId,
-                                                     bufferId,
-                                                     accountDatabase.accounts().findById(accountId)?.defaultFiltered
-                                                     ?: 0)
+              val filtered = database.filtered().get(
+                accountId,
+                bufferId,
+                accountDatabase.accounts().findById(accountId)?.defaultFiltered
+                  ?: 0
+              )
               // Try loading messages when switching to isEmpty bufferId
-              val hasVisibleMessages = database.message().hasVisibleMessages(currentNetwork,
-                                                                             currentServerBuffer,
-                                                                             bufferId,
-                                                                             filtered,
-                                                                             redirectionSettings.userNotices,
-                                                                             redirectionSettings.serverNotices,
-                                                                             redirectionSettings.errors)
+              val hasVisibleMessages = database.message().hasVisibleMessages(
+                currentNetwork,
+                currentServerBuffer,
+                bufferId,
+                filtered,
+                redirectionSettings.userNotices,
+                redirectionSettings.serverNotices,
+                redirectionSettings.errors
+              )
               if (!hasVisibleMessages) {
                 if (bufferId.isValidId() && bufferId != BufferId.MAX_VALUE) {
                   loadMore(initial = true)
@@ -439,10 +472,9 @@ class MessageListFragment : ServiceBoundFragment() {
             }
           }
         }
-      })
+      }
 
-    modelHelper.connectedSession.toLiveData().zip(lastMessageId).observe(
-      viewLifecycleOwner, Observer {
+    modelHelper.connectedSession.toLiveData().zip(lastMessageId).observe(viewLifecycleOwner) {
       runInBackground {
         val session = it?.first?.orNull()
         val message = it?.second
@@ -452,7 +484,7 @@ class MessageListFragment : ServiceBoundFragment() {
           previousMessageId = message.messageId
         }
       }
-    })
+    }
 
     var hasLoaded = false
     fun checkScroll() {
@@ -512,7 +544,7 @@ class MessageListFragment : ServiceBoundFragment() {
       lastBuffer = BufferId(getInt(KEY_STATE_BUFFER)).nullIf { !it.isValidId() }
     }
 
-    data.observe(viewLifecycleOwner, Observer { list ->
+    data.observe(viewLifecycleOwner) { list ->
       previousLoadKey = list?.lastKey as? Int
       val firstVisibleItemPosition = linearLayoutManager.findFirstVisibleItemPosition()
       val firstVisibleMessageId = adapter[firstVisibleItemPosition]?.content?.messageId
@@ -522,9 +554,9 @@ class MessageListFragment : ServiceBoundFragment() {
         }
 
         val buffer = modelHelper.chat.bufferId.safeValue
-                     ?: BufferId(-1)
+          ?: BufferId(-1)
         val network = modelHelper.bufferSyncer.value?.orNull()?.bufferInfo(buffer)?.networkId
-                      ?: NetworkId(0)
+          ?: NetworkId(0)
         val serverBuffer = modelHelper.bufferSyncer.value?.orNull()?.find(
           networkId = network,
           type = Buffer_Type.of(Buffer_Type.StatusBuffer)
@@ -532,18 +564,20 @@ class MessageListFragment : ServiceBoundFragment() {
         if (buffer != lastBuffer) {
           adapter.clearCache()
           modelHelper.connectedSession.value?.orNull()?.bufferSyncer?.let { bufferSyncer ->
-            onBufferChange(lastBuffer,
-                           network,
-                           buffer,
-                           serverBuffer,
-                           firstVisibleMessageId,
-                           bufferSyncer)
+            onBufferChange(
+              lastBuffer,
+              network,
+              buffer,
+              serverBuffer,
+              firstVisibleMessageId,
+              bufferSyncer
+            )
           }
           modelHelper.backend.value?.orNull()?.setCurrentBuffer(buffer)
           lastBuffer = buffer
         }
       }
-    })
+    }
 
     return view
   }
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 66d2c3ece..615f51db4 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
@@ -29,7 +29,24 @@ import android.view.Gravity
 import android.view.View
 import android.widget.FrameLayout
 import android.widget.LinearLayout
-import de.kuschku.libquassel.protocol.Message.MessageType.*
+import de.kuschku.libquassel.protocol.Message.MessageType.Action
+import de.kuschku.libquassel.protocol.Message.MessageType.DayChange
+import de.kuschku.libquassel.protocol.Message.MessageType.Error
+import de.kuschku.libquassel.protocol.Message.MessageType.Info
+import de.kuschku.libquassel.protocol.Message.MessageType.Invite
+import de.kuschku.libquassel.protocol.Message.MessageType.Join
+import de.kuschku.libquassel.protocol.Message.MessageType.Kick
+import de.kuschku.libquassel.protocol.Message.MessageType.Kill
+import de.kuschku.libquassel.protocol.Message.MessageType.Mode
+import de.kuschku.libquassel.protocol.Message.MessageType.NetsplitJoin
+import de.kuschku.libquassel.protocol.Message.MessageType.NetsplitQuit
+import de.kuschku.libquassel.protocol.Message.MessageType.Nick
+import de.kuschku.libquassel.protocol.Message.MessageType.Notice
+import de.kuschku.libquassel.protocol.Message.MessageType.Part
+import de.kuschku.libquassel.protocol.Message.MessageType.Plain
+import de.kuschku.libquassel.protocol.Message.MessageType.Quit
+import de.kuschku.libquassel.protocol.Message.MessageType.Server
+import de.kuschku.libquassel.protocol.Message.MessageType.Topic
 import de.kuschku.libquassel.protocol.Message_Flag
 import de.kuschku.libquassel.protocol.Message_Type
 import de.kuschku.libquassel.util.flag.hasFlag
@@ -44,6 +61,7 @@ import de.kuschku.quasseldroid.util.helper.styledAttributes
 import de.kuschku.quasseldroid.util.helper.visibleIf
 import de.kuschku.quasseldroid.util.irc.format.ContentFormatter
 import de.kuschku.quasseldroid.util.irc.format.IrcFormatDeserializer
+import de.kuschku.quasseldroid.util.listener.LinkClickListener
 import de.kuschku.quasseldroid.util.ui.SpanFormatter
 import de.kuschku.quasseldroid.viewmodel.data.FormattedMessage
 import de.kuschku.quasseldroid.viewmodel.helper.EditorViewModelHelper.Companion.IGNORED_CHARS
@@ -58,7 +76,8 @@ class QuasselMessageRenderer @Inject constructor(
   context: Context,
   private val messageSettings: MessageSettings,
   private val contentFormatter: ContentFormatter,
-  private val ircFormatDeserializer: IrcFormatDeserializer
+  private val ircFormatDeserializer: IrcFormatDeserializer,
+  private val linkClickListener: LinkClickListener,
 ) : MessageRenderer {
   private val timeFormatter = DateTimeFormatter.ofPattern(
     timePattern(messageSettings.showSeconds, messageSettings.use24hClock)
@@ -240,8 +259,11 @@ class QuasselMessageRenderer @Inject constructor(
     val monochromeForeground = highlight && monochromeHighlights
     return when (message.content.type.enabledValues().firstOrNull()) {
       Message_Type.Plain        -> {
-        val realName = ircFormatDeserializer.formatString(message.content.realName,
-                                                          !monochromeForeground)
+        val realName = ircFormatDeserializer.formatString(
+          message.content.realName,
+          !monochromeForeground,
+          linkClickListener,
+        )
         val nick = SpannableStringBuilder().apply {
           append(contentFormatter.formatPrefix(message.content.senderPrefixes))
           append(contentFormatter.formatNick(
@@ -251,10 +273,13 @@ class QuasselMessageRenderer @Inject constructor(
             false
           ))
         }
-        val (content, hasSpoilers) = contentFormatter.formatContent(message.content.content,
-                                                                    monochromeForeground,
-                                                                    message.isExpanded,
-                                                                    message.content.networkId)
+        val (content, hasSpoilers) = contentFormatter.formatContent(
+          message.content.content,
+          monochromeForeground,
+          message.isExpanded,
+          message.content.networkId,
+          linkClickListener
+        )
         val nickName = HostmaskHelper.nick(message.content.sender)
         val senderColorIndex = SenderColorUtil.senderColor(nickName)
         val rawInitial = nickName.trimStart(*IGNORED_CHARS)
@@ -303,10 +328,13 @@ class QuasselMessageRenderer @Inject constructor(
         }
         val senderColor = if (useSelfColor) selfColor else senderColors[senderColorIndex]
 
-        val (content, hasSpoilers) = contentFormatter.formatContent(message.content.content,
-                                                                    monochromeForeground,
-                                                                    message.isExpanded,
-                                                                    message.content.networkId)
+        val (content, hasSpoilers) = contentFormatter.formatContent(
+          message.content.content,
+          monochromeForeground,
+          message.isExpanded,
+          message.content.networkId,
+          linkClickListener
+        )
 
         FormattedMessage(
           original = message.content,
@@ -331,7 +359,8 @@ class QuasselMessageRenderer @Inject constructor(
         val (content, hasSpoilers) = contentFormatter.formatContent(message.content.content,
                                                                     monochromeForeground,
                                                                     message.isExpanded,
-                                                                    message.content.networkId)
+          message.content.networkId, linkClickListener
+        )
         FormattedMessage(
           original = message.content,
           time = timeFormatter.format(message.content.time.atZone(zoneId)),
@@ -448,7 +477,8 @@ class QuasselMessageRenderer @Inject constructor(
           val (content, hasSpoilers) = contentFormatter.formatContent(message.content.content,
                                                                       monochromeForeground,
                                                                       message.isExpanded,
-                                                                      message.content.networkId)
+            message.content.networkId, linkClickListener
+          )
           Pair(
             SpanFormatter.format(
               context.getString(R.string.message_format_part_2),
@@ -496,7 +526,8 @@ class QuasselMessageRenderer @Inject constructor(
           val (content, hasSpoilers) = contentFormatter.formatContent(message.content.content,
                                                                       monochromeForeground,
                                                                       message.isExpanded,
-                                                                      message.content.networkId)
+            message.content.networkId, linkClickListener
+          )
           Pair(
             SpanFormatter.format(
               context.getString(R.string.message_format_quit_2),
@@ -544,7 +575,8 @@ class QuasselMessageRenderer @Inject constructor(
           val (content, hasSpoilers) = contentFormatter.formatContent(reason,
                                                                       monochromeForeground,
                                                                       message.isExpanded,
-                                                                      message.content.networkId)
+            message.content.networkId, linkClickListener
+          )
           Pair(
             SpanFormatter.format(
               context.getString(R.string.message_format_kick_2),
@@ -591,7 +623,8 @@ class QuasselMessageRenderer @Inject constructor(
           val (content, hasSpoilers) = contentFormatter.formatContent(reason,
                                                                       monochromeForeground,
                                                                       message.isExpanded,
-                                                                      message.content.networkId)
+            message.content.networkId, linkClickListener
+          )
           Pair(
             SpanFormatter.format(
               context.getString(R.string.message_format_kill_2),
@@ -679,7 +712,8 @@ class QuasselMessageRenderer @Inject constructor(
         val (content, hasSpoilers) = contentFormatter.formatContent(message.content.content,
                                                                     monochromeForeground,
                                                                     message.isExpanded,
-                                                                    message.content.networkId)
+          message.content.networkId, linkClickListener
+        )
         FormattedMessage(
           original = message.content,
           time = timeFormatter.format(message.content.time.atZone(zoneId)),
@@ -696,7 +730,8 @@ class QuasselMessageRenderer @Inject constructor(
         val (content, hasSpoilers) = contentFormatter.formatContent(message.content.content,
                                                                     monochromeForeground,
                                                                     message.isExpanded,
-                                                                    message.content.networkId)
+          message.content.networkId, linkClickListener
+        )
         FormattedMessage(
           original = message.content,
           time = timeFormatter.format(message.content.time.atZone(zoneId)),
@@ -724,7 +759,8 @@ class QuasselMessageRenderer @Inject constructor(
         val (content, hasSpoilers) = contentFormatter.formatContent(message.content.content,
                                                                     monochromeForeground,
                                                                     message.isExpanded,
-                                                                    message.content.networkId)
+          message.content.networkId, linkClickListener
+        )
         FormattedMessage(
           original = message.content,
           time = timeFormatter.format(message.content.time.atZone(zoneId)),
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/nicks/NickListFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/nicks/NickListFragment.kt
index 0d0c11f0f..2d01cd466 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/nicks/NickListFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/nicks/NickListFragment.kt
@@ -19,11 +19,11 @@
 
 package de.kuschku.quasseldroid.ui.chat.nicks
 
+import android.content.Context
 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
@@ -40,6 +40,7 @@ import de.kuschku.libquassel.util.irc.SenderColorUtil
 import de.kuschku.quasseldroid.R
 import de.kuschku.quasseldroid.settings.AppearanceSettings
 import de.kuschku.quasseldroid.settings.MessageSettings
+import de.kuschku.quasseldroid.ui.chat.ChatActivity
 import de.kuschku.quasseldroid.ui.info.user.UserInfoActivity
 import de.kuschku.quasseldroid.util.ColorContext
 import de.kuschku.quasseldroid.util.avatars.AvatarHelper
@@ -48,6 +49,8 @@ import de.kuschku.quasseldroid.util.helper.styledAttributes
 import de.kuschku.quasseldroid.util.helper.toLiveData
 import de.kuschku.quasseldroid.util.irc.format.ContentFormatter
 import de.kuschku.quasseldroid.util.irc.format.IrcFormatDeserializer
+import de.kuschku.quasseldroid.util.listener.LinkClickListener
+import de.kuschku.quasseldroid.util.listener.QuasselLinkClickListener
 import de.kuschku.quasseldroid.util.service.ServiceBoundFragment
 import de.kuschku.quasseldroid.viewmodel.data.Avatar
 import de.kuschku.quasseldroid.viewmodel.helper.ChatViewModelHelper
@@ -66,12 +69,27 @@ class NickListFragment : ServiceBoundFragment() {
   @Inject
   lateinit var ircFormatDeserializer: IrcFormatDeserializer
 
+  @Inject
+  lateinit var internalLinkClickListener: LinkClickListener
+  lateinit var linkClickListener: LinkClickListener
+
   @Inject
   lateinit var contentFormatter: ContentFormatter
 
   @Inject
   lateinit var modelHelper: ChatViewModelHelper
 
+  override fun onAttach(context: Context) {
+    super.onAttach(context)
+    linkClickListener = QuasselLinkClickListener(internalLinkClickListener) {
+      activity?.let {
+        if (it !is ChatActivity) {
+          it.finish()
+        }
+      }
+    }
+  }
+
   override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                             savedInstanceState: Bundle?): View? {
     val view = inflater.inflate(R.layout.chat_nicklist, container, false)
@@ -102,18 +120,18 @@ class NickListFragment : ServiceBoundFragment() {
     val colorContext = ColorContext(requireContext(), messageSettings)
 
     val avatarSize = resources.getDimensionPixelSize(R.dimen.avatar_size)
-    modelHelper.nickDataThrottled.toLiveData().observe(viewLifecycleOwner, Observer {
+    modelHelper.nickDataThrottled.toLiveData().observe(viewLifecycleOwner) {
       runInBackground {
         it?.asSequence()?.map {
           val nickName = it.nick
           val senderColorIndex = SenderColorUtil.senderColor(nickName)
           val rawInitial = nickName.trimStart(*IGNORED_CHARS)
-                             .firstOrNull() ?: nickName.firstOrNull()
+            .firstOrNull() ?: nickName.firstOrNull()
           val initial = rawInitial?.uppercase().toString()
           val useSelfColor = when (messageSettings.colorizeNicknames) {
-            MessageSettings.SenderColorMode.ALL          -> false
+            MessageSettings.SenderColorMode.ALL -> false
             MessageSettings.SenderColorMode.ALL_BUT_MINE -> it.self
-            MessageSettings.SenderColorMode.NONE         -> true
+            MessageSettings.SenderColorMode.NONE -> true
           }
           val senderColor = if (useSelfColor) selfColor else senderColors[senderColorIndex]
 
@@ -124,11 +142,13 @@ class NickListFragment : ServiceBoundFragment() {
             modes = when (messageSettings.showPrefix) {
               MessageSettings.ShowPrefixMode.ALL ->
                 it.modes
-              else                               ->
+              else ->
                 it.modes.substring(0, Math.min(it.modes.length, 1))
             },
             realname = ircFormatDeserializer.formatString(
-              it.realname.toString(), messageSettings.colorizeMirc
+              it.realname.toString(),
+              messageSettings.colorizeMirc,
+              linkClickListener,
             ),
             avatarUrls = AvatarHelper.avatar(messageSettings, it, avatarSize)
           )
@@ -143,7 +163,7 @@ class NickListFragment : ServiceBoundFragment() {
           }
         }
       }
-    })
+    }
     savedInstanceState?.run {
       (nickList.layoutManager as RecyclerView.LayoutManager)
         .onRestoreInstanceState(getParcelable(KEY_STATE_LIST))
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/topic/TopicFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/topic/TopicFragment.kt
index d90e0a8b6..ad2e85f8f 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/topic/TopicFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/topic/TopicFragment.kt
@@ -19,11 +19,11 @@
 
 package de.kuschku.quasseldroid.ui.chat.topic
 
+import android.content.Context
 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
@@ -34,12 +34,19 @@ import de.kuschku.quasseldroid.R
 import de.kuschku.quasseldroid.settings.AppearanceSettings
 import de.kuschku.quasseldroid.settings.AutoCompleteSettings
 import de.kuschku.quasseldroid.settings.MessageSettings
-import de.kuschku.quasseldroid.ui.chat.input.*
+import de.kuschku.quasseldroid.ui.chat.ChatActivity
+import de.kuschku.quasseldroid.ui.chat.input.AutoCompleteAdapter
+import de.kuschku.quasseldroid.ui.chat.input.AutoCompleteHelper
+import de.kuschku.quasseldroid.ui.chat.input.EditorHelper
+import de.kuschku.quasseldroid.ui.chat.input.RichEditText
+import de.kuschku.quasseldroid.ui.chat.input.RichToolbar
 import de.kuschku.quasseldroid.util.emoji.EmojiHandler
 import de.kuschku.quasseldroid.util.helper.toLiveData
 import de.kuschku.quasseldroid.util.irc.format.ContentFormatter
 import de.kuschku.quasseldroid.util.irc.format.IrcFormatDeserializer
 import de.kuschku.quasseldroid.util.irc.format.IrcFormatSerializer
+import de.kuschku.quasseldroid.util.listener.LinkClickListener
+import de.kuschku.quasseldroid.util.listener.QuasselLinkClickListener
 import de.kuschku.quasseldroid.util.ui.settings.fragment.Savable
 import de.kuschku.quasseldroid.util.ui.settings.fragment.ServiceBoundSettingsFragment
 import de.kuschku.quasseldroid.viewmodel.helper.EditorViewModelHelper
@@ -77,8 +84,23 @@ class TopicFragment : ServiceBoundSettingsFragment(), Savable {
   @Inject
   lateinit var emojiHandler: EmojiHandler
 
+  @Inject
+  lateinit var internalLinkClickListener: LinkClickListener
+  lateinit var linkClickListener: LinkClickListener
+
   private lateinit var editorHelper: EditorHelper
 
+  override fun onAttach(context: Context) {
+    super.onAttach(context)
+    linkClickListener = QuasselLinkClickListener(internalLinkClickListener) {
+      activity?.let {
+        if (it !is ChatActivity) {
+          it.finish()
+        }
+      }
+    }
+  }
+
   override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                             savedInstanceState: Bundle?): View? {
     val view = inflater.inflate(R.layout.info_topic, container, false)
@@ -91,6 +113,7 @@ class TopicFragment : ServiceBoundSettingsFragment(), Savable {
       autoCompleteSettings,
       messageSettings,
       formatDeserializer,
+      linkClickListener,
       contentFormatter,
       modelHelper,
       emojiHandler
@@ -125,9 +148,9 @@ class TopicFragment : ServiceBoundSettingsFragment(), Savable {
     modelHelper.chat.bufferId.onNext(bufferId)
     modelHelper.bufferData.filter {
       it.info != null
-    }.firstElement().toLiveData().observe(viewLifecycleOwner, Observer {
-      chatline.setText(formatDeserializer.formatString(it?.description, true))
-    })
+    }.firstElement().toLiveData().observe(viewLifecycleOwner) {
+      chatline.setText(formatDeserializer.formatString(it?.description, true, linkClickListener))
+    }
 
     return view
   }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/topic/TopicFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/topic/TopicFragmentProvider.kt
index 94132e346..6db8784a9 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/topic/TopicFragmentProvider.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/topic/TopicFragmentProvider.kt
@@ -25,10 +25,10 @@ import dagger.Module
 import dagger.android.ContributesAndroidInjector
 
 @Module
-abstract class TopicFragmentProvider {
+interface TopicFragmentProvider {
   @Binds
-  abstract fun bindFragmentActivity(activity: TopicActivity): FragmentActivity
+  fun bindFragmentActivity(activity: TopicActivity): FragmentActivity
 
   @ContributesAndroidInjector
-  abstract fun bindTopicFragment(): TopicFragment
+  fun bindTopicFragment(): TopicFragment
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/clientsettings/about/AboutFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/clientsettings/about/AboutFragmentProvider.kt
index ad0827441..eb70b2634 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/clientsettings/about/AboutFragmentProvider.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/clientsettings/about/AboutFragmentProvider.kt
@@ -25,10 +25,10 @@ import dagger.Module
 import dagger.android.ContributesAndroidInjector
 
 @Module
-abstract class AboutFragmentProvider {
+interface AboutFragmentProvider {
   @Binds
-  abstract fun bindFragmentActivity(activity: AboutActivity): FragmentActivity
+  fun bindFragmentActivity(activity: AboutActivity): FragmentActivity
 
   @ContributesAndroidInjector
-  abstract fun bindAboutFragment(): AboutFragment
+  fun bindAboutFragment(): AboutFragment
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/clientsettings/client/ClientSettingsFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/clientsettings/client/ClientSettingsFragmentProvider.kt
index 41e06ae35..68d5c2b3e 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/clientsettings/client/ClientSettingsFragmentProvider.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/clientsettings/client/ClientSettingsFragmentProvider.kt
@@ -25,10 +25,10 @@ import dagger.Module
 import dagger.android.ContributesAndroidInjector
 
 @Module
-abstract class ClientSettingsFragmentProvider {
+interface ClientSettingsFragmentProvider {
   @Binds
-  abstract fun bindFragmentActivity(activity: ClientSettingsActivity): FragmentActivity
+  fun bindFragmentActivity(activity: ClientSettingsActivity): FragmentActivity
 
   @ContributesAndroidInjector
-  abstract fun bindClientSettingsFragment(): ClientSettingsFragment
+  fun bindClientSettingsFragment(): ClientSettingsFragment
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/clientsettings/crash/CrashFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/clientsettings/crash/CrashFragmentProvider.kt
index f20f6f460..6dbc13da7 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/clientsettings/crash/CrashFragmentProvider.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/clientsettings/crash/CrashFragmentProvider.kt
@@ -25,10 +25,10 @@ import dagger.Module
 import dagger.android.ContributesAndroidInjector
 
 @Module
-abstract class CrashFragmentProvider {
+interface CrashFragmentProvider {
   @Binds
-  abstract fun bindFragmentActivity(activity: CrashActivity): FragmentActivity
+  fun bindFragmentActivity(activity: CrashActivity): FragmentActivity
 
   @ContributesAndroidInjector
-  abstract fun bindClientSettingsFragment(): CrashFragment
+  fun bindClientSettingsFragment(): CrashFragment
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/clientsettings/license/LicenseFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/clientsettings/license/LicenseFragmentProvider.kt
index 07cf71735..8fbd4bf60 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/clientsettings/license/LicenseFragmentProvider.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/clientsettings/license/LicenseFragmentProvider.kt
@@ -25,10 +25,10 @@ import dagger.Module
 import dagger.android.ContributesAndroidInjector
 
 @Module
-abstract class LicenseFragmentProvider {
+interface LicenseFragmentProvider {
   @Binds
-  abstract fun bindFragmentActivity(activity: LicenseActivity): FragmentActivity
+  fun bindFragmentActivity(activity: LicenseActivity): FragmentActivity
 
   @ContributesAndroidInjector
-  abstract fun bindLicenseFragment(): LicenseFragment
+  fun bindLicenseFragment(): LicenseFragment
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/clientsettings/whitelist/WhitelistFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/clientsettings/whitelist/WhitelistFragmentProvider.kt
index 0dd6a2efe..f50149656 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/clientsettings/whitelist/WhitelistFragmentProvider.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/clientsettings/whitelist/WhitelistFragmentProvider.kt
@@ -25,10 +25,10 @@ import dagger.Module
 import dagger.android.ContributesAndroidInjector
 
 @Module
-abstract class WhitelistFragmentProvider {
+interface WhitelistFragmentProvider {
   @Binds
-  abstract fun bindFragmentActivity(activity: WhitelistActivity): FragmentActivity
+  fun bindFragmentActivity(activity: WhitelistActivity): FragmentActivity
 
   @ContributesAndroidInjector
-  abstract fun bindWhitelistFragment(): WhitelistFragment
+  fun bindWhitelistFragment(): WhitelistFragment
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/CoreSettingsFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/CoreSettingsFragment.kt
index 27f33b01c..24c6d6d7f 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/CoreSettingsFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/CoreSettingsFragment.kt
@@ -25,7 +25,6 @@ import android.view.View
 import android.view.ViewGroup
 import android.widget.Button
 import androidx.core.view.ViewCompat
-import androidx.lifecycle.Observer
 import androidx.recyclerview.widget.DividerItemDecoration
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
@@ -132,9 +131,9 @@ class CoreSettingsFragment : ServiceBoundFragment() {
           }.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, SettingsItem<NetworkId>::name))
         }
       }
-    }.toLiveData().observe(viewLifecycleOwner, Observer {
+    }.toLiveData().observe(viewLifecycleOwner) {
       networkAdapter.submitList(it.orEmpty())
-    })
+    }
 
     identities.adapter = identityAdapter
     identities.layoutManager = LinearLayoutManager(context)
@@ -151,9 +150,9 @@ class CoreSettingsFragment : ServiceBoundFragment() {
           }.sortedBy(SettingsItem<IdentityId>::name)
         }
       }
-    }.toLiveData().observe(viewLifecycleOwner, Observer {
+    }.toLiveData().observe(viewLifecycleOwner) {
       identityAdapter.submitList(it.orEmpty())
-    })
+    }
 
     chatlists.adapter = chatListAdapter
     chatlists.layoutManager = LinearLayoutManager(context)
@@ -166,18 +165,17 @@ class CoreSettingsFragment : ServiceBoundFragment() {
           SettingsItem(it.bufferViewId(), it.bufferViewName())
         }.sortedBy(SettingsItem<Int>::name)
       }
-    }.toLiveData().observe(viewLifecycleOwner, Observer {
+    }.toLiveData().observe(viewLifecycleOwner) {
       chatListAdapter.submitList(it.orEmpty())
-    })
+    }
 
     var missingFeatureList: List<MissingFeature> = emptyList()
-    modelHelper.negotiatedFeatures.toLiveData().observe(viewLifecycleOwner,
-                                                        Observer { (connected, features) ->
-                                                          missingFeatureList = RequiredFeatures.features.filter {
-                                                            it.feature !in features.enabledFeatures
-                                                          }
-                                                          featureContextMissing.visibleIf(connected && missingFeatureList.isNotEmpty())
-                                                        })
+    modelHelper.negotiatedFeatures.toLiveData().observe(viewLifecycleOwner) { (connected, features) ->
+      missingFeatureList = RequiredFeatures.features.filter {
+        it.feature !in features.enabledFeatures
+      }
+      featureContextMissing.visibleIf(connected && missingFeatureList.isNotEmpty())
+    }
 
     featureContextMissing.setOnClickListener {
       MissingFeaturesDialog.Builder(requireActivity())
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/CoreSettingsFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/CoreSettingsFragmentProvider.kt
index dd0be146c..5a4d370e1 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/CoreSettingsFragmentProvider.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/CoreSettingsFragmentProvider.kt
@@ -25,10 +25,10 @@ import dagger.Module
 import dagger.android.ContributesAndroidInjector
 
 @Module
-abstract class CoreSettingsFragmentProvider {
+interface CoreSettingsFragmentProvider {
   @Binds
-  abstract fun bindFragmentActivity(activity: CoreSettingsActivity): FragmentActivity
+  fun bindFragmentActivity(activity: CoreSettingsActivity): FragmentActivity
 
   @ContributesAndroidInjector
-  abstract fun bindCoreSettingsFragment(): CoreSettingsFragment
+  fun bindCoreSettingsFragment(): CoreSettingsFragment
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/aliasitem/AliasItemFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/aliasitem/AliasItemFragment.kt
index 4c149a24a..802830e26 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/aliasitem/AliasItemFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/aliasitem/AliasItemFragment.kt
@@ -20,6 +20,7 @@
 package de.kuschku.quasseldroid.ui.coresettings.aliasitem
 
 import android.app.Activity
+import android.content.Context
 import android.content.Intent
 import android.os.Bundle
 import android.view.LayoutInflater
@@ -35,11 +36,18 @@ import de.kuschku.quasseldroid.R
 import de.kuschku.quasseldroid.settings.AppearanceSettings
 import de.kuschku.quasseldroid.settings.AutoCompleteSettings
 import de.kuschku.quasseldroid.settings.MessageSettings
-import de.kuschku.quasseldroid.ui.chat.input.*
+import de.kuschku.quasseldroid.ui.chat.ChatActivity
+import de.kuschku.quasseldroid.ui.chat.input.AutoCompleteAdapter
+import de.kuschku.quasseldroid.ui.chat.input.AutoCompleteHelper
+import de.kuschku.quasseldroid.ui.chat.input.EditorHelper
+import de.kuschku.quasseldroid.ui.chat.input.RichEditText
+import de.kuschku.quasseldroid.ui.chat.input.RichToolbar
 import de.kuschku.quasseldroid.util.emoji.EmojiHandler
 import de.kuschku.quasseldroid.util.irc.format.ContentFormatter
 import de.kuschku.quasseldroid.util.irc.format.IrcFormatDeserializer
 import de.kuschku.quasseldroid.util.irc.format.IrcFormatSerializer
+import de.kuschku.quasseldroid.util.listener.LinkClickListener
+import de.kuschku.quasseldroid.util.listener.QuasselLinkClickListener
 import de.kuschku.quasseldroid.util.ui.settings.fragment.Changeable
 import de.kuschku.quasseldroid.util.ui.settings.fragment.Savable
 import de.kuschku.quasseldroid.util.ui.settings.fragment.ServiceBoundSettingsFragment
@@ -64,6 +72,10 @@ class AliasItemFragment : ServiceBoundSettingsFragment(), Savable, Changeable {
   @Inject
   lateinit var formatDeserializer: IrcFormatDeserializer
 
+  @Inject
+  lateinit var internalLinkClickListener: LinkClickListener
+  lateinit var linkClickListener: LinkClickListener
+
   @Inject
   lateinit var contentFormatter: ContentFormatter
 
@@ -83,6 +95,17 @@ class AliasItemFragment : ServiceBoundSettingsFragment(), Savable, Changeable {
 
   private var rule: IAliasManager.Alias? = null
 
+  override fun onAttach(context: Context) {
+    super.onAttach(context)
+    linkClickListener = QuasselLinkClickListener(internalLinkClickListener) {
+      activity?.let {
+        if (it !is ChatActivity) {
+          it.finish()
+        }
+      }
+    }
+  }
+
   override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                             savedInstanceState: Bundle?): View? {
 
@@ -101,6 +124,7 @@ class AliasItemFragment : ServiceBoundSettingsFragment(), Savable, Changeable {
       autoCompleteSettings,
       messageSettings,
       formatDeserializer,
+      linkClickListener,
       contentFormatter,
       modelHelper,
       emojiHandler
@@ -133,7 +157,7 @@ class AliasItemFragment : ServiceBoundSettingsFragment(), Savable, Changeable {
 
     rule?.let { data ->
       name.setText(data.name ?: "")
-      expansion.setText(formatDeserializer.formatString(data.expansion, true))
+      expansion.setText(formatDeserializer.formatString(data.expansion, true, linkClickListener))
     }
 
     return view
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/aliasitem/AliasItemFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/aliasitem/AliasItemFragmentProvider.kt
index 012df443f..ff4d38c5f 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/aliasitem/AliasItemFragmentProvider.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/aliasitem/AliasItemFragmentProvider.kt
@@ -25,10 +25,10 @@ import dagger.Module
 import dagger.android.ContributesAndroidInjector
 
 @Module
-abstract class AliasItemFragmentProvider {
+interface AliasItemFragmentProvider {
   @Binds
-  abstract fun bindFragmentActivity(activity: AliasItemActivity): FragmentActivity
+  fun bindFragmentActivity(activity: AliasItemActivity): FragmentActivity
 
   @ContributesAndroidInjector
-  abstract fun bindAliasItemFragment(): AliasItemFragment
+  fun bindAliasItemFragment(): AliasItemFragment
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/aliaslist/AliasListAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/aliaslist/AliasListAdapter.kt
index 4c885fbaa..3279fc2b5 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/aliaslist/AliasListAdapter.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/aliaslist/AliasListAdapter.kt
@@ -27,11 +27,13 @@ import androidx.recyclerview.widget.RecyclerView
 import de.kuschku.libquassel.quassel.syncables.interfaces.IAliasManager
 import de.kuschku.quasseldroid.databinding.SettingsAliaslistItemBinding
 import de.kuschku.quasseldroid.util.irc.format.IrcFormatDeserializer
+import de.kuschku.quasseldroid.util.listener.LinkClickListener
 import java.util.*
 import javax.inject.Inject
 
 class AliasListAdapter @Inject constructor(
-  private val formatDeserializer: IrcFormatDeserializer
+  private val formatDeserializer: IrcFormatDeserializer,
+  private val linkClickListener: LinkClickListener,
 ) : RecyclerView.Adapter<AliasListAdapter.AliasItemViewHolder>() {
   private var clickListener: ((IAliasManager.Alias) -> Unit)? = null
   private var dragListener: ((AliasItemViewHolder) -> Unit)? = null
@@ -83,6 +85,7 @@ class AliasListAdapter @Inject constructor(
   override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = AliasItemViewHolder(
     SettingsAliaslistItemBinding.inflate(LayoutInflater.from(parent.context), parent, false),
     formatDeserializer,
+    linkClickListener,
     clickListener,
     dragListener
   )
@@ -95,6 +98,7 @@ class AliasListAdapter @Inject constructor(
   class AliasItemViewHolder(
     private val binding: SettingsAliaslistItemBinding,
     private val formatDeserializer: IrcFormatDeserializer,
+    private val linkClickListener: LinkClickListener,
     clickListener: ((IAliasManager.Alias) -> Unit)?,
     dragListener: ((AliasItemViewHolder) -> Unit)?
   ) : RecyclerView.ViewHolder(binding.root) {
@@ -117,7 +121,7 @@ class AliasListAdapter @Inject constructor(
     fun bind(item: IAliasManager.Alias) {
       this.item = item
       binding.name.text = item.name
-      binding.expansion.text = formatDeserializer.formatString(item.expansion, true)
+      binding.expansion.text = formatDeserializer.formatString(item.expansion, true, linkClickListener)
     }
   }
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/aliaslist/AliasListFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/aliaslist/AliasListFragment.kt
index 0b49fdfc8..f6d1f3aae 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/aliaslist/AliasListFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/aliaslist/AliasListFragment.kt
@@ -25,7 +25,6 @@ import android.os.Bundle
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
-import androidx.lifecycle.Observer
 import androidx.recyclerview.widget.*
 import com.google.android.material.floatingactionbutton.FloatingActionButton
 import de.kuschku.libquassel.quassel.syncables.AliasManager
@@ -79,7 +78,7 @@ class AliasListFragment : ServiceBoundSettingsFragment(), Savable, Changeable {
     modelHelper.aliasManager
       .filter(Optional<AliasManager>::isPresent)
       .map(Optional<AliasManager>::get)
-      .toLiveData().observe(viewLifecycleOwner, Observer {
+      .toLiveData().observe(viewLifecycleOwner) {
         if (it != null) {
           if (this.aliasManager == null) {
             this.aliasManager = Pair(it, it.copy())
@@ -88,7 +87,7 @@ class AliasListFragment : ServiceBoundSettingsFragment(), Savable, Changeable {
             }
           }
         }
-      })
+      }
 
     return view
   }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/aliaslist/AliasListFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/aliaslist/AliasListFragmentProvider.kt
index ae9d501b6..3594fecbd 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/aliaslist/AliasListFragmentProvider.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/aliaslist/AliasListFragmentProvider.kt
@@ -25,10 +25,10 @@ import dagger.Module
 import dagger.android.ContributesAndroidInjector
 
 @Module
-abstract class AliasListFragmentProvider {
+interface AliasListFragmentProvider {
   @Binds
-  abstract fun bindFragmentActivity(activity: AliasListActivity): FragmentActivity
+  fun bindFragmentActivity(activity: AliasListActivity): FragmentActivity
 
   @ContributesAndroidInjector
-  abstract fun bindAliasListFragment(): AliasListFragment
+  fun bindAliasListFragment(): AliasListFragment
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/chatlist/ChatListBaseFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/chatlist/ChatListBaseFragment.kt
index 7715ba1b6..ac91b17a7 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/chatlist/ChatListBaseFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/chatlist/ChatListBaseFragment.kt
@@ -27,7 +27,6 @@ import android.widget.AdapterView
 import android.widget.EditText
 import android.widget.Spinner
 import androidx.appcompat.widget.SwitchCompat
-import androidx.lifecycle.Observer
 import de.kuschku.libquassel.protocol.Buffer_Activity
 import de.kuschku.libquassel.protocol.Buffer_Type
 import de.kuschku.libquassel.protocol.NetworkId
@@ -113,7 +112,7 @@ abstract class ChatListBaseFragment(private val initDefault: Boolean) :
       combineLatest(it.values.map(Network::liveNetworkInfo)).map {
         it.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, INetwork.NetworkInfo::networkName))
       }
-    }.toLiveData().observe(viewLifecycleOwner, Observer {
+    }.toLiveData().observe(viewLifecycleOwner) {
       if (it != null) {
         val selectOriginal = networkId.selectedItemId == Spinner.INVALID_ROW_ID
         networkAdapter.submitList(listOf(null) + it)
@@ -123,30 +122,32 @@ abstract class ChatListBaseFragment(private val initDefault: Boolean) :
           }
         }
       }
-    })
+    }
 
     if (initDefault) {
       modelHelper.connectedSession
         .filter(Optional<ISession>::isPresent)
         .map(Optional<ISession>::get)
         .firstElement()
-        .toLiveData().observe(viewLifecycleOwner, Observer {
+        .toLiveData().observe(viewLifecycleOwner) {
           it?.let {
-            update(Defaults.bufferViewConfig(requireContext(), it.proxy),
-                   minimumActivityAdapter,
-                   networkAdapter)
+            update(
+              Defaults.bufferViewConfig(requireContext(), it.proxy),
+              minimumActivityAdapter,
+              networkAdapter
+            )
           }
-        })
+        }
     } else {
       modelHelper.bufferViewConfigMap.map { Optional.ofNullable(it[chatlistId]) }
         .filter(Optional<BufferViewConfig>::isPresent)
         .map(Optional<BufferViewConfig>::get)
         .firstElement()
-        .toLiveData().observe(viewLifecycleOwner, Observer {
+        .toLiveData().observe(viewLifecycleOwner) {
           it?.let {
             update(it, minimumActivityAdapter, networkAdapter)
           }
-        })
+        }
     }
 
     networkId.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/chatlist/ChatlistCreateFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/chatlist/ChatlistCreateFragmentProvider.kt
index 7fbe2bbb3..39f06d0b3 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/chatlist/ChatlistCreateFragmentProvider.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/chatlist/ChatlistCreateFragmentProvider.kt
@@ -25,10 +25,10 @@ import dagger.Module
 import dagger.android.ContributesAndroidInjector
 
 @Module
-abstract class ChatlistCreateFragmentProvider {
+interface ChatlistCreateFragmentProvider {
   @Binds
-  abstract fun bindFragmentActivity(activity: ChatlistCreateActivity): FragmentActivity
+  fun bindFragmentActivity(activity: ChatlistCreateActivity): FragmentActivity
 
   @ContributesAndroidInjector
-  abstract fun bindChatListCreateFragment(): ChatListCreateFragment
+  fun bindChatListCreateFragment(): ChatListCreateFragment
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/chatlist/ChatlistEditFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/chatlist/ChatlistEditFragmentProvider.kt
index ac0db70e4..4a0a43f4f 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/chatlist/ChatlistEditFragmentProvider.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/chatlist/ChatlistEditFragmentProvider.kt
@@ -25,10 +25,10 @@ import dagger.Module
 import dagger.android.ContributesAndroidInjector
 
 @Module
-abstract class ChatlistEditFragmentProvider {
+interface ChatlistEditFragmentProvider {
   @Binds
-  abstract fun bindFragmentActivity(activity: ChatlistEditActivity): FragmentActivity
+  fun bindFragmentActivity(activity: ChatlistEditActivity): FragmentActivity
 
   @ContributesAndroidInjector
-  abstract fun bindChatListEditFragment(): ChatListEditFragment
+  fun bindChatListEditFragment(): ChatListEditFragment
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/highlightlist/HighlightListFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/highlightlist/HighlightListFragment.kt
index f2b9f3c9c..336a6e4a5 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/highlightlist/HighlightListFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/highlightlist/HighlightListFragment.kt
@@ -28,7 +28,6 @@ import android.view.ViewGroup
 import android.widget.Button
 import android.widget.Spinner
 import androidx.appcompat.widget.SwitchCompat
-import androidx.lifecycle.Observer
 import androidx.recyclerview.widget.DefaultItemAnimator
 import androidx.recyclerview.widget.ItemTouchHelper
 import androidx.recyclerview.widget.LinearLayoutManager
@@ -127,29 +126,32 @@ class HighlightListFragment : ServiceBoundSettingsFragment(), Savable, Changeabl
     modelHelper.highlightRuleManager
       .filter(Optional<HighlightRuleManager>::isPresent)
       .map(Optional<HighlightRuleManager>::get)
-      .toLiveData().observe(viewLifecycleOwner, Observer {
+      .toLiveData().observe(viewLifecycleOwner) {
         if (it != null) {
           if (this.ruleManager == null) {
             this.ruleManager = Pair(it, it.copy())
             this.ruleManager?.let { (_, data) ->
               rulesAdapter.list = data.highlightRuleList().filter { !it.isInverse }
               ignoreRulesAdapter.list = data.highlightRuleList().filter { it.isInverse }
-              highlightNickType.setSelection(highlightNickTypeAdapter.indexOf(data.highlightNick())
-                                             ?: 0)
+              highlightNickType.setSelection(
+                highlightNickTypeAdapter.indexOf(data.highlightNick())
+                  ?: 0
+              )
               isCaseSensitive.isChecked = data.nicksCaseSensitive()
             }
           }
         }
-      })
-
-    modelHelper.negotiatedFeatures.toLiveData().observe(viewLifecycleOwner,
-                                                        Observer { (connected, features) ->
-                                                          featureContextCoreSideHighlights.setMode(
-                                                            if (!connected || features.hasFeature(
-                                                                ExtendedFeature.CoreSideHighlights)) WarningBarView.MODE_NONE
-                                                            else WarningBarView.MODE_ICON
-                                                          )
-                                                        })
+      }
+
+    modelHelper.negotiatedFeatures.toLiveData().observe(viewLifecycleOwner) { (connected, features) ->
+      featureContextCoreSideHighlights.setMode(
+        if (!connected || features.hasFeature(
+            ExtendedFeature.CoreSideHighlights
+          )
+        ) WarningBarView.MODE_NONE
+        else WarningBarView.MODE_ICON
+      )
+    }
 
     return view
   }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/highlightlist/HighlightListFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/highlightlist/HighlightListFragmentProvider.kt
index d7804d7c6..da8baf2d0 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/highlightlist/HighlightListFragmentProvider.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/highlightlist/HighlightListFragmentProvider.kt
@@ -25,10 +25,10 @@ import dagger.Module
 import dagger.android.ContributesAndroidInjector
 
 @Module
-abstract class HighlightListFragmentProvider {
+interface HighlightListFragmentProvider {
   @Binds
-  abstract fun bindFragmentActivity(activity: HighlightListActivity): FragmentActivity
+  fun bindFragmentActivity(activity: HighlightListActivity): FragmentActivity
 
   @ContributesAndroidInjector
-  abstract fun bindHighlightListFragment(): HighlightListFragment
+  fun bindHighlightListFragment(): HighlightListFragment
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/highlightrule/HighlightRuleFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/highlightrule/HighlightRuleFragmentProvider.kt
index a2659f0be..57754aab5 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/highlightrule/HighlightRuleFragmentProvider.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/highlightrule/HighlightRuleFragmentProvider.kt
@@ -25,10 +25,10 @@ import dagger.Module
 import dagger.android.ContributesAndroidInjector
 
 @Module
-abstract class HighlightRuleFragmentProvider {
+interface HighlightRuleFragmentProvider {
   @Binds
-  abstract fun bindFragmentActivity(activity: HighlightRuleActivity): FragmentActivity
+  fun bindFragmentActivity(activity: HighlightRuleActivity): FragmentActivity
 
   @ContributesAndroidInjector
-  abstract fun bindHighlightRuleFragment(): HighlightRuleFragment
+  fun bindHighlightRuleFragment(): HighlightRuleFragment
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/identity/IdentityBaseFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/identity/IdentityBaseFragment.kt
index 38763e5ba..255cb8857 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/identity/IdentityBaseFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/identity/IdentityBaseFragment.kt
@@ -27,7 +27,6 @@ import android.widget.Button
 import android.widget.EditText
 import androidx.appcompat.widget.SwitchCompat
 import androidx.core.view.ViewCompat
-import androidx.lifecycle.Observer
 import androidx.recyclerview.widget.ItemTouchHelper
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
@@ -116,21 +115,21 @@ abstract class IdentityBaseFragment(private val initDefault: Boolean) :
         .filter(Optional<ISession>::isPresent)
         .map(Optional<ISession>::get)
         .firstElement()
-        .toLiveData().observe(viewLifecycleOwner, Observer {
+        .toLiveData().observe(viewLifecycleOwner) {
           it?.let {
             update(Defaults.identity(requireContext(), it.proxy))
           }
-        })
+        }
     } else {
       modelHelper.identities.map { Optional.ofNullable(it[identityId]) }
         .filter(Optional<Identity>::isPresent)
         .map(Optional<Identity>::get)
         .firstElement()
-        .toLiveData().observe(viewLifecycleOwner, Observer {
+        .toLiveData().observe(viewLifecycleOwner) {
           it?.let {
             update(it)
           }
-        })
+        }
     }
 
     detachAway.setDependent(detachAwayGroup)
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/identity/IdentityCreateFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/identity/IdentityCreateFragmentProvider.kt
index 2f93ae0cd..9d4d56ffd 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/identity/IdentityCreateFragmentProvider.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/identity/IdentityCreateFragmentProvider.kt
@@ -25,10 +25,10 @@ import dagger.Module
 import dagger.android.ContributesAndroidInjector
 
 @Module
-abstract class IdentityCreateFragmentProvider {
+interface IdentityCreateFragmentProvider {
   @Binds
-  abstract fun bindFragmentActivity(activity: IdentityCreateActivity): FragmentActivity
+  fun bindFragmentActivity(activity: IdentityCreateActivity): FragmentActivity
 
   @ContributesAndroidInjector
-  abstract fun bindIdentityCreateFragment(): IdentityCreateFragment
+  fun bindIdentityCreateFragment(): IdentityCreateFragment
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/identity/IdentityEditFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/identity/IdentityEditFragmentProvider.kt
index 7c7a4f455..5f4a04a73 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/identity/IdentityEditFragmentProvider.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/identity/IdentityEditFragmentProvider.kt
@@ -25,10 +25,10 @@ import dagger.Module
 import dagger.android.ContributesAndroidInjector
 
 @Module
-abstract class IdentityEditFragmentProvider {
+interface IdentityEditFragmentProvider {
   @Binds
-  abstract fun bindFragmentActivity(activity: IdentityEditActivity): FragmentActivity
+  fun bindFragmentActivity(activity: IdentityEditActivity): FragmentActivity
 
   @ContributesAndroidInjector
-  abstract fun bindIdentityEditFragment(): IdentityEditFragment
+  fun bindIdentityEditFragment(): IdentityEditFragment
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/ignoreitem/IgnoreItemFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/ignoreitem/IgnoreItemFragmentProvider.kt
index b9131763f..c86b4319b 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/ignoreitem/IgnoreItemFragmentProvider.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/ignoreitem/IgnoreItemFragmentProvider.kt
@@ -25,10 +25,10 @@ import dagger.Module
 import dagger.android.ContributesAndroidInjector
 
 @Module
-abstract class IgnoreItemFragmentProvider {
+interface IgnoreItemFragmentProvider {
   @Binds
-  abstract fun bindFragmentActivity(activity: IgnoreItemActivity): FragmentActivity
+  fun bindFragmentActivity(activity: IgnoreItemActivity): FragmentActivity
 
   @ContributesAndroidInjector
-  abstract fun bindIgnoreItemFragment(): IgnoreItemFragment
+  fun bindIgnoreItemFragment(): IgnoreItemFragment
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/ignorelist/IgnoreListFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/ignorelist/IgnoreListFragment.kt
index 76b0c429b..54acd8b40 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/ignorelist/IgnoreListFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/ignorelist/IgnoreListFragment.kt
@@ -25,7 +25,6 @@ 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.ItemTouchHelper
 import androidx.recyclerview.widget.LinearLayoutManager
@@ -86,7 +85,7 @@ class IgnoreListFragment : ServiceBoundSettingsFragment(), Savable,
     modelHelper.ignoreListManager
       .filter(Optional<IgnoreListManager>::isPresent)
       .map(Optional<IgnoreListManager>::get)
-      .toLiveData().observe(viewLifecycleOwner, Observer {
+      .toLiveData().observe(viewLifecycleOwner) {
         if (it != null) {
           if (this.ignoreListManager == null) {
             this.ignoreListManager = Pair(it, it.copy())
@@ -95,7 +94,7 @@ class IgnoreListFragment : ServiceBoundSettingsFragment(), Savable,
             }
           }
         }
-      })
+      }
 
     return view
   }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/ignorelist/IgnoreListFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/ignorelist/IgnoreListFragmentProvider.kt
index ecac100e6..db19f8fca 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/ignorelist/IgnoreListFragmentProvider.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/ignorelist/IgnoreListFragmentProvider.kt
@@ -25,10 +25,10 @@ import dagger.Module
 import dagger.android.ContributesAndroidInjector
 
 @Module
-abstract class IgnoreListFragmentProvider {
+interface IgnoreListFragmentProvider {
   @Binds
-  abstract fun bindFragmentActivity(activity: IgnoreListActivity): FragmentActivity
+  fun bindFragmentActivity(activity: IgnoreListActivity): FragmentActivity
 
   @ContributesAndroidInjector
-  abstract fun bindIgnoreFragment(): IgnoreListFragment
+  fun bindIgnoreFragment(): IgnoreListFragment
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/network/NetworkBaseFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/network/NetworkBaseFragment.kt
index 39bd8cc55..9381eeacb 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/network/NetworkBaseFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/network/NetworkBaseFragment.kt
@@ -30,7 +30,6 @@ import android.widget.EditText
 import android.widget.Spinner
 import androidx.appcompat.widget.SwitchCompat
 import androidx.core.view.ViewCompat
-import androidx.lifecycle.Observer
 import androidx.recyclerview.widget.ItemTouchHelper
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
@@ -149,7 +148,7 @@ abstract class NetworkBaseFragment(private val initDefault: Boolean) :
       combineLatest(it.values.map(Identity::liveUpdates)).map {
         it.sortedBy(Identity::identityName)
       }
-    }.toLiveData().observe(viewLifecycleOwner, Observer {
+    }.toLiveData().observe(viewLifecycleOwner) {
       if (it != null) {
         val selectOriginal = identity.selectedItemId == Spinner.INVALID_ROW_ID
         identityAdapter.submitList(it)
@@ -159,37 +158,37 @@ abstract class NetworkBaseFragment(private val initDefault: Boolean) :
           }
         }
       }
-    })
+    }
 
     if (initDefault) {
       modelHelper.connectedSession
         .filter(Optional<ISession>::isPresent)
         .map(Optional<ISession>::get)
         .firstElement()
-        .toLiveData().observe(viewLifecycleOwner, Observer {
+        .toLiveData().observe(viewLifecycleOwner) {
           it?.let {
             update(Defaults.network(requireContext(), it.proxy), identityAdapter)
           }
-        })
+        }
     } else {
       modelHelper.networks.map { Optional.ofNullable(it[networkId]) }
         .filter(Optional<Network>::isPresent)
         .map(Optional<Network>::get)
         .firstElement()
         .toLiveData()
-        .observe(viewLifecycleOwner, Observer {
+        .observe(viewLifecycleOwner) {
           it?.let {
             update(it, identityAdapter)
           }
-        })
+        }
       modelHelper.networks.map { Optional.ofNullable(it[networkId]) }
         .filter(Optional<Network>::isPresent)
         .map(Optional<Network>::get)
         .safeSwitchMap(Network::liveCaps)
         .toLiveData()
-        .observe(viewLifecycleOwner, Observer {
+        .observe(viewLifecycleOwner) {
           autoidentifyWarning.visibleIf(it.contains("sasl"))
-        })
+        }
     }
 
 
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/network/NetworkCreateFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/network/NetworkCreateFragmentProvider.kt
index 9c84fc30c..0dca07a5f 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/network/NetworkCreateFragmentProvider.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/network/NetworkCreateFragmentProvider.kt
@@ -25,10 +25,10 @@ import dagger.Module
 import dagger.android.ContributesAndroidInjector
 
 @Module
-abstract class NetworkCreateFragmentProvider {
+interface NetworkCreateFragmentProvider {
   @Binds
-  abstract fun bindFragmentActivity(activity: NetworkCreateActivity): FragmentActivity
+  fun bindFragmentActivity(activity: NetworkCreateActivity): FragmentActivity
 
   @ContributesAndroidInjector
-  abstract fun bindNetworkCreateFragment(): NetworkCreateFragment
+  fun bindNetworkCreateFragment(): NetworkCreateFragment
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/network/NetworkEditFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/network/NetworkEditFragmentProvider.kt
index fc9218f1a..0bbebbc96 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/network/NetworkEditFragmentProvider.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/network/NetworkEditFragmentProvider.kt
@@ -25,10 +25,10 @@ import dagger.Module
 import dagger.android.ContributesAndroidInjector
 
 @Module
-abstract class NetworkEditFragmentProvider {
+interface NetworkEditFragmentProvider {
   @Binds
-  abstract fun bindFragmentActivity(activity: NetworkEditActivity): FragmentActivity
+  fun bindFragmentActivity(activity: NetworkEditActivity): FragmentActivity
 
   @ContributesAndroidInjector
-  abstract fun bindNetworkEditFragment(): NetworkEditFragment
+  fun bindNetworkEditFragment(): NetworkEditFragment
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/networkconfig/NetworkConfigFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/networkconfig/NetworkConfigFragment.kt
index 6de953ceb..c1e3d5b0b 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/networkconfig/NetworkConfigFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/networkconfig/NetworkConfigFragment.kt
@@ -25,7 +25,6 @@ import android.view.View
 import android.view.ViewGroup
 import android.widget.EditText
 import androidx.appcompat.widget.SwitchCompat
-import androidx.lifecycle.Observer
 import de.kuschku.libquassel.quassel.syncables.NetworkConfig
 import de.kuschku.libquassel.util.Optional
 import de.kuschku.quasseldroid.R
@@ -73,7 +72,7 @@ class NetworkConfigFragment : ServiceBoundSettingsFragment(), Savable,
       .filter(Optional<NetworkConfig>::isPresent)
       .map(Optional<NetworkConfig>::get)
       .firstElement()
-      .toLiveData().observe(viewLifecycleOwner, Observer {
+      .toLiveData().observe(viewLifecycleOwner) {
         it?.let {
           if (this.networkConfig == null) {
             this.networkConfig = Pair(it, it.copy())
@@ -91,7 +90,7 @@ class NetworkConfigFragment : ServiceBoundSettingsFragment(), Savable,
             }
           }
         }
-      })
+      }
 
     pingTimeoutEnabled.setDependent(pingTimeoutGroup)
     autoWhoEnabled.setDependent(autoWhoGroup)
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/networkconfig/NetworkConfigFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/networkconfig/NetworkConfigFragmentProvider.kt
index 312d14e15..d59ebede1 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/networkconfig/NetworkConfigFragmentProvider.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/networkconfig/NetworkConfigFragmentProvider.kt
@@ -25,10 +25,10 @@ import dagger.Module
 import dagger.android.ContributesAndroidInjector
 
 @Module
-abstract class NetworkConfigFragmentProvider {
+interface NetworkConfigFragmentProvider {
   @Binds
-  abstract fun bindFragmentActivity(activity: NetworkConfigActivity): FragmentActivity
+  fun bindFragmentActivity(activity: NetworkConfigActivity): FragmentActivity
 
   @ContributesAndroidInjector
-  abstract fun bindNetworkConfigFragment(): NetworkConfigFragment
+  fun bindNetworkConfigFragment(): NetworkConfigFragment
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/networkserver/NetworkServerFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/networkserver/NetworkServerFragmentProvider.kt
index d7f234d07..598d398ae 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/networkserver/NetworkServerFragmentProvider.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/networkserver/NetworkServerFragmentProvider.kt
@@ -25,10 +25,10 @@ import dagger.Module
 import dagger.android.ContributesAndroidInjector
 
 @Module
-abstract class NetworkServerFragmentProvider {
+interface NetworkServerFragmentProvider {
   @Binds
-  abstract fun bindFragmentActivity(activity: NetworkServerActivity): FragmentActivity
+  fun bindFragmentActivity(activity: NetworkServerActivity): FragmentActivity
 
   @ContributesAndroidInjector
-  abstract fun bindNetworkServerFragment(): NetworkServerFragment
+  fun bindNetworkServerFragment(): NetworkServerFragment
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/passwordchange/PasswordChangeFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/passwordchange/PasswordChangeFragment.kt
index 90eb2db3b..cb6981b80 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/passwordchange/PasswordChangeFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/passwordchange/PasswordChangeFragment.kt
@@ -27,7 +27,6 @@ import android.view.ViewGroup
 import android.widget.Button
 import android.widget.EditText
 import android.widget.TextView
-import androidx.lifecycle.Observer
 import com.google.android.material.textfield.TextInputLayout
 import de.kuschku.libquassel.quassel.syncables.RpcHandler
 import de.kuschku.libquassel.session.ISession
@@ -87,7 +86,7 @@ class PasswordChangeFragment : ServiceBoundFragment() {
       .mapSwitchMap(RpcHandler::passwordChanged)
       .filter(Optional<Boolean>::isPresent)
       .map(Optional<Boolean>::get)
-      .toLiveData().observe(viewLifecycleOwner, Observer {
+      .toLiveData().observe(viewLifecycleOwner) {
         val waiting = this.waiting
         if (waiting != null) {
           if (it) {
@@ -105,7 +104,7 @@ class PasswordChangeFragment : ServiceBoundFragment() {
             error.visibility = View.VISIBLE
           }
         }
-      })
+      }
 
     oldPassword.addTextChangedListener(object : TextValidator(
       activity,
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/passwordchange/PasswordChangeFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/passwordchange/PasswordChangeFragmentProvider.kt
index 4145d74de..aaef99ae9 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/passwordchange/PasswordChangeFragmentProvider.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/passwordchange/PasswordChangeFragmentProvider.kt
@@ -25,10 +25,10 @@ import dagger.Module
 import dagger.android.ContributesAndroidInjector
 
 @Module
-abstract class PasswordChangeFragmentProvider {
+interface PasswordChangeFragmentProvider {
   @Binds
-  abstract fun bindFragmentActivity(activity: PasswordChangeActivity): FragmentActivity
+  fun bindFragmentActivity(activity: PasswordChangeActivity): FragmentActivity
 
   @ContributesAndroidInjector
-  abstract fun bindPasswordChangeFragment(): PasswordChangeFragment
+  fun bindPasswordChangeFragment(): PasswordChangeFragment
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/info/certificate/CertificateInfoFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/info/certificate/CertificateInfoFragment.kt
index ca16bd675..09f18ce96 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/info/certificate/CertificateInfoFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/info/certificate/CertificateInfoFragment.kt
@@ -24,7 +24,6 @@ import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
 import android.widget.TextView
-import androidx.lifecycle.Observer
 import de.kuschku.libquassel.ssl.X509Helper
 import de.kuschku.libquassel.ssl.commonName
 import de.kuschku.libquassel.ssl.organization
@@ -93,7 +92,7 @@ class CertificateInfoFragment : ServiceBoundSettingsFragment() {
 
     val dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
 
-    modelHelper.peerCertificateChain.toLiveData().observe(viewLifecycleOwner, Observer {
+    modelHelper.peerCertificateChain.toLiveData().observe(viewLifecycleOwner) {
       val leafCertificate = it.firstOrNull()
       if (leafCertificate != null) {
         content.visibility = View.VISIBLE
@@ -135,7 +134,7 @@ class CertificateInfoFragment : ServiceBoundSettingsFragment() {
         content.visibility = View.GONE
         error.visibility = View.VISIBLE
       }
-    })
+    }
 
     return view
   }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/info/certificate/CertificateInfoFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/info/certificate/CertificateInfoFragmentProvider.kt
index 79610c535..28cc0b521 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/info/certificate/CertificateInfoFragmentProvider.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/info/certificate/CertificateInfoFragmentProvider.kt
@@ -25,10 +25,10 @@ import dagger.Module
 import dagger.android.ContributesAndroidInjector
 
 @Module
-abstract class CertificateInfoFragmentProvider {
+interface CertificateInfoFragmentProvider {
   @Binds
-  abstract fun bindFragmentActivity(activity: CertificateInfoActivity): FragmentActivity
+  fun bindFragmentActivity(activity: CertificateInfoActivity): FragmentActivity
 
   @ContributesAndroidInjector
-  abstract fun bindCertificateInfoFragment(): CertificateInfoFragment
+  fun bindCertificateInfoFragment(): CertificateInfoFragment
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/info/channel/ChannelInfoFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/info/channel/ChannelInfoFragment.kt
index 867d9b18a..ba87307b2 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/info/channel/ChannelInfoFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/info/channel/ChannelInfoFragment.kt
@@ -19,6 +19,7 @@
 
 package de.kuschku.quasseldroid.ui.info.channel
 
+import android.content.Context
 import android.os.Build
 import android.os.Bundle
 import android.view.LayoutInflater
@@ -26,7 +27,6 @@ import android.view.View
 import android.view.ViewGroup
 import android.widget.Button
 import android.widget.TextView
-import androidx.lifecycle.Observer
 import de.kuschku.libquassel.protocol.BufferId
 import de.kuschku.libquassel.protocol.Buffer_Type
 import de.kuschku.libquassel.protocol.NetworkId
@@ -37,10 +37,17 @@ import de.kuschku.libquassel.util.helper.safeSwitchMap
 import de.kuschku.libquassel.util.helper.value
 import de.kuschku.quasseldroid.R
 import de.kuschku.quasseldroid.settings.MessageSettings
+import de.kuschku.quasseldroid.ui.chat.ChatActivity
 import de.kuschku.quasseldroid.ui.chat.topic.TopicActivity
 import de.kuschku.quasseldroid.util.ShortcutCreationHelper
-import de.kuschku.quasseldroid.util.helper.*
+import de.kuschku.quasseldroid.util.helper.getVectorDrawableCompat
+import de.kuschku.quasseldroid.util.helper.retint
+import de.kuschku.quasseldroid.util.helper.setTooltip
+import de.kuschku.quasseldroid.util.helper.toLiveData
+import de.kuschku.quasseldroid.util.helper.visibleIf
 import de.kuschku.quasseldroid.util.irc.format.ContentFormatter
+import de.kuschku.quasseldroid.util.listener.LinkClickListener
+import de.kuschku.quasseldroid.util.listener.QuasselLinkClickListener
 import de.kuschku.quasseldroid.util.service.ServiceBoundFragment
 import de.kuschku.quasseldroid.util.ui.BetterLinkMovementMethod
 import de.kuschku.quasseldroid.util.ui.LinkLongClickMenuHelper
@@ -62,9 +69,24 @@ class ChannelInfoFragment : ServiceBoundFragment() {
   @Inject
   lateinit var messageSettings: MessageSettings
 
+  @Inject
+  lateinit var internalLinkClickListener: LinkClickListener
+  lateinit var linkClickListener: LinkClickListener
+
   @Inject
   lateinit var modelHelper: EditorViewModelHelper
 
+  override fun onAttach(context: Context) {
+    super.onAttach(context)
+    linkClickListener = QuasselLinkClickListener(internalLinkClickListener) {
+      activity?.let {
+        if (it !is ChatActivity) {
+          it.finish()
+        }
+      }
+    }
+  }
+
   override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                             savedInstanceState: Bundle?): View? {
     val view = inflater.inflate(R.layout.info_channel, container, false)
@@ -104,11 +126,12 @@ class ChannelInfoFragment : ServiceBoundFragment() {
       channel.updates().map {
         Pair(info, it)
       }
-    }.toLiveData().observe(viewLifecycleOwner, Observer { (info, channel) ->
+    }.toLiveData().observe(viewLifecycleOwner) { (info, channel) ->
       name.text = channel.name()
       val (content, hasSpoilers) = contentFormatter.formatContent(
         channel.topic(),
-        networkId = channel.network().networkId()
+        networkId = channel.network().networkId(),
+        linkClickListener = linkClickListener,
       )
       topic.text = content
 
@@ -155,7 +178,7 @@ class ChannelInfoFragment : ServiceBoundFragment() {
           }
         }
       }
-    })
+    }
 
     val movementMethod = BetterLinkMovementMethod.newInstance()
     movementMethod.setOnLinkLongClickListener(LinkLongClickMenuHelper())
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/info/channel/ChannelInfoFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/info/channel/ChannelInfoFragmentProvider.kt
index f87d5ca29..8e4155050 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/info/channel/ChannelInfoFragmentProvider.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/info/channel/ChannelInfoFragmentProvider.kt
@@ -25,10 +25,10 @@ import dagger.Module
 import dagger.android.ContributesAndroidInjector
 
 @Module
-abstract class ChannelInfoFragmentProvider {
+interface ChannelInfoFragmentProvider {
   @Binds
-  abstract fun bindFragmentActivity(activity: ChannelInfoActivity): FragmentActivity
+  fun bindFragmentActivity(activity: ChannelInfoActivity): FragmentActivity
 
   @ContributesAndroidInjector
-  abstract fun bindChannelInfoFragment(): ChannelInfoFragment
+  fun bindChannelInfoFragment(): ChannelInfoFragment
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/info/channellist/ChannelListAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/info/channellist/ChannelListAdapter.kt
index e9352e809..f5d1c4415 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/info/channellist/ChannelListAdapter.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/info/channellist/ChannelListAdapter.kt
@@ -29,14 +29,15 @@ import androidx.recyclerview.widget.RecyclerView
 import de.kuschku.libquassel.quassel.syncables.IrcListHelper
 import de.kuschku.quasseldroid.R
 import de.kuschku.quasseldroid.databinding.WidgetChannelSearchBinding
-import de.kuschku.quasseldroid.ui.chat.ChatActivity
 import de.kuschku.quasseldroid.util.ColorContext
 import de.kuschku.quasseldroid.util.helper.styledAttributes
 import de.kuschku.quasseldroid.util.irc.format.ContentFormatter
+import de.kuschku.quasseldroid.util.listener.LinkClickListener
 import javax.inject.Inject
 
 class ChannelListAdapter @Inject constructor(
   private val contentFormatter: ContentFormatter,
+  private var linkClickListener: LinkClickListener,
   context: Context,
   colorContext: ColorContext
 ) :
@@ -55,12 +56,17 @@ class ChannelListAdapter @Inject constructor(
     getColor(0, 0)
   }
 
+  fun setOnClickListener(listener: LinkClickListener) {
+    this.linkClickListener = listener
+  }
+
   private val fallbackDrawable = colorContext.buildTextDrawable("#", colorAccent)
 
   override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChannelViewHolder {
     return ChannelViewHolder(
       WidgetChannelSearchBinding.inflate(LayoutInflater.from(parent.context), parent, false),
       contentFormatter,
+      linkClickListener,
       fallbackDrawable
     )
   }
@@ -72,6 +78,7 @@ class ChannelListAdapter @Inject constructor(
   class ChannelViewHolder(
     private val binding: WidgetChannelSearchBinding,
     private val contentFormatter: ContentFormatter,
+    private val linkClickListener: LinkClickListener,
     fallbackDrawable: Drawable
   ) : RecyclerView.ViewHolder(binding.root) {
     private var data: IrcListHelper.ChannelDescription? = null
@@ -80,11 +87,7 @@ class ChannelListAdapter @Inject constructor(
       binding.status.setImageDrawable(fallbackDrawable)
       itemView.setOnClickListener {
         data?.let {
-          ChatActivity.launch(
-            itemView.context,
-            networkId = it.netId,
-            channel = it.channelName
-          )
+          linkClickListener.openChannel(it.netId, it.channelName)
         }
       }
     }
@@ -93,7 +96,8 @@ class ChannelListAdapter @Inject constructor(
       binding.name.text = data.channelName
       val (content, hasSpoilers) = contentFormatter.formatContent(
         data.topic,
-        networkId = data.netId
+        networkId = data.netId,
+        linkClickListener = linkClickListener
       )
       binding.topic.text = content
       binding.users.text = itemView.context.resources.getQuantityString(R.plurals.label_user_count,
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/info/channellist/ChannelListFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/info/channellist/ChannelListFragment.kt
index 8df314a05..fa73457b3 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/info/channellist/ChannelListFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/info/channellist/ChannelListFragment.kt
@@ -19,12 +19,12 @@
 
 package de.kuschku.quasseldroid.ui.info.channellist
 
+import android.content.Context
 import android.os.Bundle
 import android.view.*
 import android.widget.EditText
 import androidx.appcompat.app.AppCompatActivity
 import androidx.appcompat.widget.AppCompatImageButton
-import androidx.lifecycle.Observer
 import androidx.recyclerview.widget.DefaultItemAnimator
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
@@ -36,8 +36,11 @@ import de.kuschku.libquassel.util.helper.combineLatest
 import de.kuschku.libquassel.util.helper.mapSwitchMap
 import de.kuschku.libquassel.util.helper.value
 import de.kuschku.quasseldroid.R
+import de.kuschku.quasseldroid.ui.chat.ChatActivity
 import de.kuschku.quasseldroid.util.helper.retint
 import de.kuschku.quasseldroid.util.helper.toLiveData
+import de.kuschku.quasseldroid.util.listener.LinkClickListener
+import de.kuschku.quasseldroid.util.listener.QuasselLinkClickListener
 import de.kuschku.quasseldroid.util.ui.settings.fragment.ServiceBoundSettingsFragment
 import de.kuschku.quasseldroid.util.ui.view.MaterialContentLoadingProgressBar
 import de.kuschku.quasseldroid.util.ui.view.WarningBarView
@@ -59,6 +62,10 @@ class ChannelListFragment : ServiceBoundSettingsFragment() {
   @Inject
   lateinit var modelHelper: EditorViewModelHelper
 
+  @Inject
+  lateinit var internalLinkClickListener: LinkClickListener
+  lateinit var linkClickListener: LinkClickListener
+
   private var query: Query? = null
   private var state: State = State()
 
@@ -98,6 +105,17 @@ class ChannelListFragment : ServiceBoundSettingsFragment() {
   val results = BehaviorSubject.createDefault(emptyList<IrcListHelper.ChannelDescription>())
   val sort = BehaviorSubject.createDefault(Sort(Sort.Field.CHANNEL_NAME, Sort.Direction.ASC))
 
+  override fun onAttach(context: Context) {
+    super.onAttach(context)
+    linkClickListener = QuasselLinkClickListener(internalLinkClickListener) {
+      activity?.let {
+        if (it !is ChatActivity) {
+          it.finish()
+        }
+      }
+    }
+  }
+
   override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                             savedInstanceState: Bundle?): View? {
     val view = inflater.inflate(R.layout.info_channellist, container, false)
@@ -109,66 +127,76 @@ class ChannelListFragment : ServiceBoundSettingsFragment() {
 
     val networkId = NetworkId(arguments?.getInt("network_id", -1) ?: -1)
 
+    adapter.setOnClickListener(linkClickListener)
     searchResults.adapter = adapter
     searchResults.layoutManager = LinearLayoutManager(view.context)
     searchResults.itemAnimator = DefaultItemAnimator()
 
-    combineLatest(results, sort).toLiveData().observe(viewLifecycleOwner,
-                                                      Observer { (results, sort) ->
-                                                        adapter.submitList(results.let {
-                                                          when (sort.field) {
-                                                            Sort.Field.CHANNEL_NAME -> {
-                                                              when (sort.direction) {
-                                                                Sort.Direction.ASC  ->
-                                                                  it.sortedBy(IrcListHelper.ChannelDescription::channelName)
-                                                                Sort.Direction.DESC ->
-                                                                  it.sortedByDescending(
-                                                                    IrcListHelper.ChannelDescription::channelName)
-                                                              }
-                                                            }
-                                                            Sort.Field.USER_COUNT   -> {
-                                                              when (sort.direction) {
-                                                                Sort.Direction.ASC  ->
-                                                                  it.sortedBy(IrcListHelper.ChannelDescription::userCount)
-                                                                Sort.Direction.DESC ->
-                                                                  it.sortedByDescending(
-                                                                    IrcListHelper.ChannelDescription::userCount)
-                                                              }
-                                                            }
-                                                            Sort.Field.TOPIC        -> {
-                                                              when (sort.direction) {
-                                                                Sort.Direction.ASC  ->
-                                                                  it.sortedBy(IrcListHelper.ChannelDescription::topic)
-                                                                Sort.Direction.DESC ->
-                                                                  it.sortedByDescending(
-                                                                    IrcListHelper.ChannelDescription::topic)
-                                                              }
-                                                            }
-                                                          }
-                                                        })
-                                                      })
+    combineLatest(results, sort).toLiveData().observe(viewLifecycleOwner) { (results, sort) ->
+      adapter.submitList(results.let {
+        when (sort.field) {
+          Sort.Field.CHANNEL_NAME -> {
+            when (sort.direction) {
+              Sort.Direction.ASC ->
+                it.sortedBy(IrcListHelper.ChannelDescription::channelName)
+
+              Sort.Direction.DESC ->
+                it.sortedByDescending(
+                  IrcListHelper.ChannelDescription::channelName
+                )
+            }
+          }
+
+          Sort.Field.USER_COUNT -> {
+            when (sort.direction) {
+              Sort.Direction.ASC ->
+                it.sortedBy(IrcListHelper.ChannelDescription::userCount)
+
+              Sort.Direction.DESC ->
+                it.sortedByDescending(
+                  IrcListHelper.ChannelDescription::userCount
+                )
+            }
+          }
+
+          Sort.Field.TOPIC -> {
+            when (sort.direction) {
+              Sort.Direction.ASC ->
+                it.sortedBy(IrcListHelper.ChannelDescription::topic)
+
+              Sort.Direction.DESC ->
+                it.sortedByDescending(
+                  IrcListHelper.ChannelDescription::topic
+                )
+            }
+          }
+        }
+      })
+    }
 
     modelHelper.ircListHelper
       .mapSwitchMap(IrcListHelper::observable)
       .filter(Optional<IrcListHelper.Event>::isPresent)
       .map(Optional<IrcListHelper.Event>::get)
-      .toLiveData(BackpressureStrategy.BUFFER).observe(viewLifecycleOwner, Observer {
+      .toLiveData(BackpressureStrategy.BUFFER).observe(viewLifecycleOwner) {
         when (it) {
           is IrcListHelper.Event.ChannelList -> {
             if (it.netId == query?.networkId) {
               results.onNext(it.data)
             }
           }
-          is IrcListHelper.Event.Finished    -> {
+
+          is IrcListHelper.Event.Finished -> {
             if (it.netId == query?.networkId) {
               updateState(false, null)
             }
           }
-          is IrcListHelper.Event.Error       -> {
+
+          is IrcListHelper.Event.Error -> {
             updateState(false, it.error)
           }
         }
-      })
+      }
 
     searchButton.setOnClickListener {
       modelHelper.ircListHelper.value?.orNull()?.let { ircListHelper ->
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/info/channellist/ChannelListFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/info/channellist/ChannelListFragmentProvider.kt
index 6844f76b6..cd1f1f664 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/info/channellist/ChannelListFragmentProvider.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/info/channellist/ChannelListFragmentProvider.kt
@@ -25,10 +25,10 @@ import dagger.Module
 import dagger.android.ContributesAndroidInjector
 
 @Module
-abstract class ChannelListFragmentProvider {
+interface ChannelListFragmentProvider {
   @Binds
-  abstract fun bindFragmentActivity(activity: ChannelListActivity): FragmentActivity
+  fun bindFragmentActivity(activity: ChannelListActivity): FragmentActivity
 
   @ContributesAndroidInjector
-  abstract fun bindChannelListFragment(): ChannelListFragment
+  fun bindChannelListFragment(): ChannelListFragment
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/info/core/CoreInfoFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/info/core/CoreInfoFragment.kt
index e1ce42197..16093c07f 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/info/core/CoreInfoFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/info/core/CoreInfoFragment.kt
@@ -27,7 +27,6 @@ import android.view.ViewGroup
 import android.widget.Button
 import android.widget.ImageView
 import android.widget.TextView
-import androidx.lifecycle.Observer
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
 import de.kuschku.libquassel.quassel.QuasselFeatures
@@ -38,7 +37,12 @@ import de.kuschku.libquassel.util.helper.combineLatest
 import de.kuschku.libquassel.util.helper.value
 import de.kuschku.quasseldroid.R
 import de.kuschku.quasseldroid.ui.info.certificate.CertificateInfoActivity
-import de.kuschku.quasseldroid.util.helper.*
+import de.kuschku.quasseldroid.util.helper.getVectorDrawableCompat
+import de.kuschku.quasseldroid.util.helper.isValid
+import de.kuschku.quasseldroid.util.helper.styledAttributes
+import de.kuschku.quasseldroid.util.helper.tint
+import de.kuschku.quasseldroid.util.helper.toLiveData
+import de.kuschku.quasseldroid.util.helper.visibleIf
 import de.kuschku.quasseldroid.util.missingfeatures.MissingFeature
 import de.kuschku.quasseldroid.util.missingfeatures.MissingFeaturesDialog
 import de.kuschku.quasseldroid.util.missingfeatures.RequiredFeatures
@@ -100,12 +104,12 @@ class CoreInfoFragment : ServiceBoundFragment() {
 
     var missingFeatureList: List<MissingFeature> = emptyList()
     combineLatest(modelHelper.coreInfo, modelHelper.coreFeatures).toLiveData()
-      .observe(viewLifecycleOwner, Observer {
+      .observe(viewLifecycleOwner) {
         val data = it?.first?.orNull()
         val connected = it?.second?.first
-                        ?: false
+          ?: false
         val features = it?.second?.second
-                       ?: QuasselFeatures.empty()
+          ?: QuasselFeatures.empty()
 
         version.text = data?.quasselVersion?.let(Html::fromHtml)
         val versionTime = data?.quasselBuildDate?.toLongOrNull()
@@ -120,10 +124,12 @@ class CoreInfoFragment : ServiceBoundFragment() {
         missingFeatures.visibleIf(connected && missingFeatureList.isNotEmpty())
 
         val startTime = data?.startTime?.atZone(ZoneId.systemDefault())?.let(dateTimeFormatter::format)
-        uptime.text = requireContext().getString(R.string.label_core_online_since,
-                                                 startTime.toString())
+        uptime.text = requireContext().getString(
+          R.string.label_core_online_since,
+          startTime.toString()
+        )
         uptimeContainer.visibleIf(startTime != null)
-      })
+      }
     missingFeatures.setOnClickListener {
       MissingFeaturesDialog.Builder(requireActivity())
         .missingFeatures(missingFeatureList)
@@ -150,7 +156,7 @@ class CoreInfoFragment : ServiceBoundFragment() {
       CertificateInfoActivity.launch(it.context)
     }
 
-    modelHelper.sslSession.toLiveData().observe(viewLifecycleOwner, Observer {
+    modelHelper.sslSession.toLiveData().observe(viewLifecycleOwner) {
       val peerCertificateChain = try {
         it?.orNull()?.peerCertificateChain
       } catch (ignored: SSLPeerUnverifiedException) {
@@ -190,22 +196,28 @@ class CoreInfoFragment : ServiceBoundFragment() {
       if (cipherSuite != null && keyExchangeMechanism != null && protocol != null) {
         // TLSv1.3 has no key exchange mechanism in the ciphersuite
         if (keyExchangeMechanism.isEmpty()) {
-          secureConnectionCiphersuite.text = context?.getString(R.string.label_core_connection_ciphersuite_13,
-                                                                cipherSuite)
+          secureConnectionCiphersuite.text = context?.getString(
+            R.string.label_core_connection_ciphersuite_13,
+            cipherSuite
+          )
         } else {
-          secureConnectionCiphersuite.text = context?.getString(R.string.label_core_connection_ciphersuite,
-                                                                cipherSuite,
-                                                                keyExchangeMechanism)
+          secureConnectionCiphersuite.text = context?.getString(
+            R.string.label_core_connection_ciphersuite,
+            cipherSuite,
+            keyExchangeMechanism
+          )
         }
-        secureConnectionProtocol.text = context?.getString(R.string.label_core_connection_protocol,
-                                                           protocol)
+        secureConnectionProtocol.text = context?.getString(
+          R.string.label_core_connection_protocol,
+          protocol
+        )
         secureConnectionProtocol.visibility = View.VISIBLE
         secureConnectionCiphersuite.visibility = View.VISIBLE
       } else {
         secureConnectionProtocol.visibility = View.GONE
         secureConnectionCiphersuite.visibility = View.GONE
       }
-    })
+    }
 
     clients.layoutManager = LinearLayoutManager(requireContext())
     val adapter = ClientAdapter()
@@ -216,10 +228,10 @@ class CoreInfoFragment : ServiceBoundFragment() {
       rpcHandler?.requestKickClient(it)
     }
     clients.adapter = adapter
-    modelHelper.coreInfoClients.toLiveData().observe(viewLifecycleOwner, Observer {
+    modelHelper.coreInfoClients.toLiveData().observe(viewLifecycleOwner) {
       clientsTitle.visibleIf(it?.isNotEmpty() == true)
       adapter.submitList(it)
-    })
+    }
 
     return view
   }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/info/core/CoreInfoFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/info/core/CoreInfoFragmentProvider.kt
index 441573d02..d30add9e6 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/info/core/CoreInfoFragmentProvider.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/info/core/CoreInfoFragmentProvider.kt
@@ -25,10 +25,10 @@ import dagger.Module
 import dagger.android.ContributesAndroidInjector
 
 @Module
-abstract class CoreInfoFragmentProvider {
+interface CoreInfoFragmentProvider {
   @Binds
-  abstract fun bindFragmentActivity(activity: CoreInfoActivity): FragmentActivity
+  fun bindFragmentActivity(activity: CoreInfoActivity): FragmentActivity
 
   @ContributesAndroidInjector
-  abstract fun bindCoreInfoFragment(): CoreInfoFragment
+  fun bindCoreInfoFragment(): CoreInfoFragment
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/info/user/ChannelAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/info/user/ChannelAdapter.kt
index 365450c1d..399cb5729 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/info/user/ChannelAdapter.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/info/user/ChannelAdapter.kt
@@ -26,7 +26,6 @@ import androidx.recyclerview.widget.RecyclerView
 import de.kuschku.libquassel.protocol.NetworkId
 import de.kuschku.libquassel.quassel.BufferInfo
 import de.kuschku.quasseldroid.databinding.WidgetBufferBinding
-import de.kuschku.quasseldroid.ui.chat.ChatActivity
 import de.kuschku.quasseldroid.util.helper.visibleIf
 import de.kuschku.quasseldroid.util.lists.ListAdapter
 import de.kuschku.quasseldroid.viewmodel.data.BufferProps
@@ -61,12 +60,10 @@ class ChannelAdapter : ListAdapter<BufferProps, ChannelAdapter.ChannelViewHolder
 
     init {
       itemView.setOnClickListener {
-        info?.let {
-          ChatActivity.launch(
-            itemView.context,
-            networkId = it.networkId,
-            channel = it.bufferName
-          )
+        info?.let { bufferInfo ->
+          bufferInfo.bufferName?.let { bufferName ->
+            clickListener?.invoke(bufferInfo.networkId, bufferName)
+          }
         }
       }
     }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/info/user/UserInfoFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/info/user/UserInfoFragment.kt
index 5dc6b3626..b79caaecb 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/info/user/UserInfoFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/info/user/UserInfoFragment.kt
@@ -33,7 +33,6 @@ import android.widget.Button
 import android.widget.ImageView
 import android.widget.TextView
 import androidx.appcompat.widget.PopupMenu
-import androidx.lifecycle.Observer
 import androidx.recyclerview.widget.DefaultItemAnimator
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
@@ -63,6 +62,9 @@ import de.kuschku.quasseldroid.util.helper.*
 import de.kuschku.quasseldroid.util.irc.format.ContentFormatter
 import de.kuschku.quasseldroid.util.irc.format.IrcFormatDeserializer
 import de.kuschku.quasseldroid.util.irc.format.spans.IrcItalicSpan
+import de.kuschku.quasseldroid.util.listener.AutocompleteTextListener
+import de.kuschku.quasseldroid.util.listener.LinkClickListener
+import de.kuschku.quasseldroid.util.listener.QuasselLinkClickListener
 import de.kuschku.quasseldroid.util.service.ServiceBoundFragment
 import de.kuschku.quasseldroid.util.ui.BetterLinkMovementMethod
 import de.kuschku.quasseldroid.util.ui.LinkLongClickMenuHelper
@@ -110,8 +112,26 @@ class UserInfoFragment : ServiceBoundFragment() {
   @Inject
   lateinit var modelHelper: EditorViewModelHelper
 
+  @Inject
+  lateinit var internalLinkClickListener: LinkClickListener
+  lateinit var linkClickListener: LinkClickListener
+
+  @Inject
+  lateinit var autocompleteTextListener: AutocompleteTextListener
+
   private var actualUrl: String? = null
 
+  override fun onAttach(context: Context) {
+    super.onAttach(context)
+    linkClickListener = QuasselLinkClickListener(internalLinkClickListener) {
+      activity?.let {
+        if (it !is ChatActivity) {
+          it.finish()
+        }
+      }
+    }
+  }
+
   override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                             savedInstanceState: Bundle?): View? {
     val view = inflater.inflate(R.layout.info_user, container, false)
@@ -146,6 +166,9 @@ class UserInfoFragment : ServiceBoundFragment() {
     var currentIrcUser: IrcUser?
 
     val commonChannelsAdapter = ChannelAdapter()
+    commonChannelsAdapter.setOnClickListener { networkId, channel ->
+      linkClickListener.openChannel(networkId, channel)
+    }
     commonChannels.layoutManager = LinearLayoutManager(context)
     commonChannels.itemAnimator = DefaultItemAnimator()
     commonChannels.adapter = commonChannelsAdapter
@@ -256,7 +279,7 @@ class UserInfoFragment : ServiceBoundFragment() {
           )
         }
       }
-    }.toLiveData().observe(viewLifecycleOwner, Observer {
+    }.toLiveData().observe(viewLifecycleOwner) {
       val live = it
       val user = it.meta
 
@@ -275,6 +298,7 @@ class UserInfoFragment : ServiceBoundFragment() {
             is String -> {
               actualUrl = model
             }
+
             is Avatar.MatrixAvatar -> {
               runInBackground {
                 matrixApi.avatarUrl(model.userId).execute().body()
@@ -295,10 +319,11 @@ class UserInfoFragment : ServiceBoundFragment() {
           }
         }
       }
-      nick.text = ircFormatDeserializer.formatString(user.nick, messageSettings.colorizeMirc)
+      nick.text = ircFormatDeserializer.formatString(user.nick, messageSettings.colorizeMirc, linkClickListener)
       val (content, _) = contentFormatter.formatContent(
         user.realName ?: "",
-        networkId = user.networkId
+        networkId = user.networkId,
+        linkClickListener = linkClickListener,
       )
       realName.text = content
       realName.visibleIf(!user.realName.isNullOrBlank() && user.realName != user.nick)
@@ -314,13 +339,15 @@ class UserInfoFragment : ServiceBoundFragment() {
       accountContainer.visibleIf(!user.account.isNullOrBlank())
       val (userIdent, _) = contentFormatter.formatContent(
         user.user ?: "",
-        networkId = user.networkId
+        networkId = user.networkId,
+        linkClickListener = linkClickListener,
       )
       ident.text = userIdent
       identContainer.visibleIf(userIdent.isNotBlank())
       val (userHost, _) = contentFormatter.formatContent(
         user.host ?: "",
-        networkId = user.networkId
+        networkId = user.networkId,
+        linkClickListener = linkClickListener,
       )
       host.text = userHost
       hostContainer.visibleIf(userHost.isNotBlank())
@@ -336,7 +363,8 @@ class UserInfoFragment : ServiceBoundFragment() {
           )
 
           if (info != null) {
-            ChatActivity.launch(view.context, bufferId = info.bufferId)
+            linkClickListener.openBuffer(bufferId = info.bufferId)
+            activity?.finish()
           } else {
             modelHelper.allBuffers.map {
               listOfNotNull(it.find {
@@ -344,11 +372,12 @@ class UserInfoFragment : ServiceBoundFragment() {
               })
             }.filter {
               it.isNotEmpty()
-            }.firstElement().toLiveData().observe(viewLifecycleOwner, Observer {
+            }.firstElement().toLiveData().observe(viewLifecycleOwner) {
               it?.firstOrNull()?.let { info ->
-                ChatActivity.launch(view.context, bufferId = info.bufferId)
+                linkClickListener.openBuffer(bufferId = info.bufferId)
+                activity?.finish()
               }
-            })
+            }
 
             session.bufferSyncer.find(
               networkId = user.networkId,
@@ -381,6 +410,7 @@ class UserInfoFragment : ServiceBoundFragment() {
                 ignoreMenu = null
                 true
               }
+
               it.itemId == R.id.action_show -> {
                 IgnoreListActivity.launch(
                   view.context
@@ -389,11 +419,13 @@ class UserInfoFragment : ServiceBoundFragment() {
                 ignoreMenu = null
                 true
               }
+
               it.isCheckable -> {
                 modelHelper.ignoreListManager.value?.orNull()
                   ?.requestToggleIgnoreRule(it.title.toString())
                 true
               }
+
               else -> false
             }
           }
@@ -404,7 +436,8 @@ class UserInfoFragment : ServiceBoundFragment() {
         }
       }
       actionMention.setOnClickListener { view ->
-        ChatActivity.launch(view.context, sharedText = "${user.nick}: ")
+        autocompleteTextListener.autocompleteText(user.nick, ": ")
+        activity?.finish()
       }
       actionWhois.setOnClickListener { view ->
         modelHelper.connectedSession {
@@ -432,7 +465,7 @@ class UserInfoFragment : ServiceBoundFragment() {
         }
       }
       commonChannelsAdapter.submitList(live.channels)
-    })
+    }
 
     avatar.setOnClickListener {
       actualUrl?.let {
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/info/user/UserInfoFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/info/user/UserInfoFragmentProvider.kt
index 73b41ea10..62163c89c 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/info/user/UserInfoFragmentProvider.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/info/user/UserInfoFragmentProvider.kt
@@ -25,10 +25,10 @@ import dagger.Module
 import dagger.android.ContributesAndroidInjector
 
 @Module
-abstract class UserInfoFragmentProvider {
+interface UserInfoFragmentProvider {
   @Binds
-  abstract fun bindFragmentActivity(activity: UserInfoActivity): FragmentActivity
+  fun bindFragmentActivity(activity: UserInfoActivity): FragmentActivity
 
   @ContributesAndroidInjector
-  abstract fun bindUserInfoFragment(): UserInfoFragment
+  fun bindUserInfoFragment(): UserInfoFragment
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/setup/accounts/edit/AccountEditFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/setup/accounts/edit/AccountEditFragmentProvider.kt
index 292bee3a1..5fae3486a 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/setup/accounts/edit/AccountEditFragmentProvider.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/setup/accounts/edit/AccountEditFragmentProvider.kt
@@ -25,10 +25,10 @@ import dagger.Module
 import dagger.android.ContributesAndroidInjector
 
 @Module
-abstract class AccountEditFragmentProvider {
+interface AccountEditFragmentProvider {
   @Binds
-  abstract fun bindFragmentActivity(activity: AccountEditActivity): FragmentActivity
+  fun bindFragmentActivity(activity: AccountEditActivity): FragmentActivity
 
   @ContributesAndroidInjector
-  abstract fun bindAccountEditFragment(): AccountEditFragment
+  fun bindAccountEditFragment(): AccountEditFragment
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/setup/accounts/selection/AccountSelectionFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/setup/accounts/selection/AccountSelectionFragmentProvider.kt
index db97a1407..ba38bd171 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/setup/accounts/selection/AccountSelectionFragmentProvider.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/setup/accounts/selection/AccountSelectionFragmentProvider.kt
@@ -25,10 +25,10 @@ import dagger.Module
 import dagger.android.ContributesAndroidInjector
 
 @Module
-abstract class AccountSelectionFragmentProvider {
+interface AccountSelectionFragmentProvider {
   @Binds
-  abstract fun bindFragmentActivity(activity: AccountSelectionActivity): FragmentActivity
+  fun bindFragmentActivity(activity: AccountSelectionActivity): FragmentActivity
 
   @ContributesAndroidInjector
-  abstract fun bindAccountSelectionSlide(): AccountSelectionSlide
+  fun bindAccountSelectionSlide(): AccountSelectionSlide
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/setup/accounts/setup/AccountSetupFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/setup/accounts/setup/AccountSetupFragmentProvider.kt
index d78ec563c..7df489c06 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/setup/accounts/setup/AccountSetupFragmentProvider.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/setup/accounts/setup/AccountSetupFragmentProvider.kt
@@ -25,16 +25,16 @@ import dagger.Module
 import dagger.android.ContributesAndroidInjector
 
 @Module
-abstract class AccountSetupFragmentProvider {
+interface AccountSetupFragmentProvider {
   @Binds
-  abstract fun bindFragmentActivity(activity: AccountSetupActivity): FragmentActivity
+  fun bindFragmentActivity(activity: AccountSetupActivity): FragmentActivity
 
   @ContributesAndroidInjector
-  abstract fun bindAccountSetupConnectionSlide(): AccountSetupConnectionSlide
+  fun bindAccountSetupConnectionSlide(): AccountSetupConnectionSlide
 
   @ContributesAndroidInjector
-  abstract fun bindAccountSetupNameSlide(): AccountSetupNameSlide
+  fun bindAccountSetupNameSlide(): AccountSetupNameSlide
 
   @ContributesAndroidInjector
-  abstract fun bindAccountSetupUserSlide(): AccountSetupUserSlide
+  fun bindAccountSetupUserSlide(): AccountSetupUserSlide
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/setup/core/CoreSetupFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/setup/core/CoreSetupFragmentProvider.kt
index a4da3a29d..4728ee1e5 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/setup/core/CoreSetupFragmentProvider.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/setup/core/CoreSetupFragmentProvider.kt
@@ -25,19 +25,19 @@ import dagger.Module
 import dagger.android.ContributesAndroidInjector
 
 @Module
-abstract class CoreSetupFragmentProvider {
+interface CoreSetupFragmentProvider {
   @Binds
-  abstract fun bindFragmentActivity(activity: CoreSetupActivity): FragmentActivity
+  fun bindFragmentActivity(activity: CoreSetupActivity): FragmentActivity
 
   @ContributesAndroidInjector
-  abstract fun bindCoreStorageBackendChooseSlide(): CoreStorageBackendChooseSlide
+  fun bindCoreStorageBackendChooseSlide(): CoreStorageBackendChooseSlide
 
   @ContributesAndroidInjector
-  abstract fun bindCoreStorageBackendSetupSlide(): CoreStorageBackendSetupSlide
+  fun bindCoreStorageBackendSetupSlide(): CoreStorageBackendSetupSlide
 
   @ContributesAndroidInjector
-  abstract fun bindCoreAuthenticatorBackendChooseSlide(): CoreAuthenticatorBackendChooseSlide
+  fun bindCoreAuthenticatorBackendChooseSlide(): CoreAuthenticatorBackendChooseSlide
 
   @ContributesAndroidInjector
-  abstract fun bindCoreAuthenticatorBackendSetupSlide(): CoreAuthenticatorBackendSetupSlide
+  fun bindCoreAuthenticatorBackendSetupSlide(): CoreAuthenticatorBackendSetupSlide
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/setup/network/NetworkSetupFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/setup/network/NetworkSetupFragmentProvider.kt
index 923317f7f..8f1c4ce54 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/setup/network/NetworkSetupFragmentProvider.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/setup/network/NetworkSetupFragmentProvider.kt
@@ -25,13 +25,13 @@ import dagger.Module
 import dagger.android.ContributesAndroidInjector
 
 @Module
-abstract class NetworkSetupFragmentProvider {
+interface NetworkSetupFragmentProvider {
   @Binds
-  abstract fun bindFragmentActivity(activity: NetworkSetupActivity): FragmentActivity
+  fun bindFragmentActivity(activity: NetworkSetupActivity): FragmentActivity
 
   @ContributesAndroidInjector
-  abstract fun bindNetworkSetupNetworkSlide(): NetworkSetupNetworkSlide
+  fun bindNetworkSetupNetworkSlide(): NetworkSetupNetworkSlide
 
   @ContributesAndroidInjector
-  abstract fun bindNetworkSetupChannelsSlide(): NetworkSetupChannelsSlide
+  fun bindNetworkSetupChannelsSlide(): NetworkSetupChannelsSlide
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/setup/network/NetworkSetupNetworkSlide.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/setup/network/NetworkSetupNetworkSlide.kt
index 3acb9c93f..4edd9857e 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/setup/network/NetworkSetupNetworkSlide.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/setup/network/NetworkSetupNetworkSlide.kt
@@ -28,7 +28,6 @@ import android.widget.AdapterView
 import android.widget.EditText
 import android.widget.Spinner
 import androidx.appcompat.widget.SwitchCompat
-import androidx.lifecycle.Observer
 import com.google.android.material.textfield.TextInputLayout
 import de.kuschku.libquassel.protocol.IdentityId
 import de.kuschku.libquassel.protocol.NetworkId
@@ -180,22 +179,22 @@ class NetworkSetupNetworkSlide : ServiceBoundSlideFragment() {
       combineLatest(it.values.map(Identity::liveUpdates)).map {
         it.sortedBy(Identity::identityName)
       }
-    }.toLiveData().observe(viewLifecycleOwner, Observer {
+    }.toLiveData().observe(viewLifecycleOwner) {
       if (it != null) {
         identityAdapter.submitList(it)
       }
-    })
+    }
 
     modelHelper.networks.safeSwitchMap {
       combineLatest(it.values.map(Network::liveNetworkInfo)).map {
         it.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, INetwork.NetworkInfo::networkName))
       }
-    }.toLiveData().observe(viewLifecycleOwner, Observer {
+    }.toLiveData().observe(viewLifecycleOwner) {
       if (it != null) {
         this.networks = it
         update()
       }
-    })
+    }
 
     identity.adapter = identityAdapter
 
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/setup/user/UserSetupActivity.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/setup/user/UserSetupActivity.kt
index 58641d0aa..71b4d8ead 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/setup/user/UserSetupActivity.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/setup/user/UserSetupActivity.kt
@@ -23,7 +23,6 @@ import android.app.Activity
 import android.content.Context
 import android.content.Intent
 import android.os.Bundle
-import androidx.lifecycle.Observer
 import de.kuschku.libquassel.protocol.IdentityId
 import de.kuschku.libquassel.quassel.syncables.Identity
 import de.kuschku.libquassel.quassel.syncables.interfaces.INetwork
@@ -63,9 +62,10 @@ class UserSetupActivity : ServiceBoundSetupActivity() {
             .filter(Collection<Identity>::isNotEmpty)
             .map(Collection<Identity>::first)
             .firstElement()
-            .toLiveData().observe(this@UserSetupActivity, Observer {
+            .toLiveData().observe(this@UserSetupActivity) {
               if (it != null) {
-                createNetwork(INetwork.NetworkInfo(
+                createNetwork(
+                  INetwork.NetworkInfo(
                   networkName = network.name,
                   identity = it.id(),
                   serverList = network.servers.map {
@@ -79,7 +79,7 @@ class UserSetupActivity : ServiceBoundSetupActivity() {
 
                 backend.requestConnectNewNetwork()
               }
-            })
+            }
         }
       }
     }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/setup/user/UserSetupFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/setup/user/UserSetupFragmentProvider.kt
index e76e3f416..88fe5f44a 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/setup/user/UserSetupFragmentProvider.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/setup/user/UserSetupFragmentProvider.kt
@@ -25,16 +25,16 @@ import dagger.Module
 import dagger.android.ContributesAndroidInjector
 
 @Module
-abstract class UserSetupFragmentProvider {
+interface UserSetupFragmentProvider {
   @Binds
-  abstract fun bindFragmentActivity(activity: UserSetupActivity): FragmentActivity
+  fun bindFragmentActivity(activity: UserSetupActivity): FragmentActivity
 
   @ContributesAndroidInjector
-  abstract fun bindUserSetupIdentitySlide(): UserSetupIdentitySlide
+  fun bindUserSetupIdentitySlide(): UserSetupIdentitySlide
 
   @ContributesAndroidInjector
-  abstract fun bindUserSetupNetworkSlide(): UserSetupNetworkSlide
+  fun bindUserSetupNetworkSlide(): UserSetupNetworkSlide
 
   @ContributesAndroidInjector
-  abstract fun bindUserSetupChannelsSlide(): UserSetupChannelsSlide
+  fun bindUserSetupChannelsSlide(): UserSetupChannelsSlide
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/setup/user/UserSetupIdentitySlide.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/setup/user/UserSetupIdentitySlide.kt
index de98aa393..47081bddc 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/setup/user/UserSetupIdentitySlide.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/setup/user/UserSetupIdentitySlide.kt
@@ -32,6 +32,7 @@ import de.kuschku.quasseldroid.util.Patterns
 import de.kuschku.quasseldroid.util.TextValidator
 import de.kuschku.quasseldroid.util.irc.format.IrcFormatDeserializer
 import de.kuschku.quasseldroid.util.irc.format.IrcFormatSerializer
+import de.kuschku.quasseldroid.util.listener.LinkClickListener
 import javax.inject.Inject
 
 class UserSetupIdentitySlide : SlideFragment() {
@@ -46,6 +47,10 @@ class UserSetupIdentitySlide : SlideFragment() {
   @Inject
   lateinit var ircFormatDeserializer: IrcFormatDeserializer
 
+  @Inject
+  lateinit var internalLinkClickListener: LinkClickListener
+  lateinit var linkClickListener: LinkClickListener
+
   override fun isValid(): Boolean {
     return nickValidator.isValid
   }
@@ -57,7 +62,7 @@ class UserSetupIdentitySlide : SlideFragment() {
     if (data.containsKey("nick"))
       nickField.setText(data.getString("nick"))
     if (data.containsKey("realname"))
-      realnameField.setText(ircFormatDeserializer.formatString(data.getString("realname"), true))
+      realnameField.setText(ircFormatDeserializer.formatString(data.getString("realname"), true, linkClickListener))
     updateValidity()
   }
 
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/ContentFormatter.kt b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/ContentFormatter.kt
index 8a50b32c5..ef88427b9 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/ContentFormatter.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/ContentFormatter.kt
@@ -33,6 +33,7 @@ import de.kuschku.quasseldroid.settings.MessageSettings
 import de.kuschku.quasseldroid.util.helper.styledAttributes
 import de.kuschku.quasseldroid.util.irc.format.model.FormatInfo
 import de.kuschku.quasseldroid.util.irc.format.model.IrcFormat
+import de.kuschku.quasseldroid.util.listener.LinkClickListener
 import de.kuschku.quasseldroid.util.ui.SpanFormatter
 import org.intellij.lang.annotations.Language
 import javax.inject.Inject
@@ -77,15 +78,19 @@ class ContentFormatter @Inject constructor(
     getColor(0, 0)
   }
 
-  fun formatContent(content: String,
-                    highlight: Boolean = false,
-                    unhideSpoilers: Boolean = false,
-                    networkId: NetworkId?): Pair<CharSequence, Boolean> {
+  fun formatContent(
+    content: String,
+    highlight: Boolean = false,
+    unhideSpoilers: Boolean = false,
+    networkId: NetworkId?,
+    linkClickListener: LinkClickListener,
+  ): Pair<CharSequence, Boolean> {
     val spans = mutableListOf<FormatInfo>()
     val formattedText = SpannableString(
       ircFormatDeserializer.formatString(
         content,
         messageSettings.colorizeMirc,
+        linkClickListener,
         spans
       )
     )
@@ -139,7 +144,7 @@ class ContentFormatter @Inject constructor(
     }
 
     for (span in spans) {
-      span.apply(formattedText)
+      span.apply(formattedText, linkClickListener)
     }
 
     return Pair(formattedText, hasSpoilers)
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/IrcFormatDeserializer.kt b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/IrcFormatDeserializer.kt
index 30619f43e..796a2d54a 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/IrcFormatDeserializer.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/IrcFormatDeserializer.kt
@@ -26,6 +26,7 @@ import de.kuschku.quasseldroid.util.helper.getColorCompat
 import de.kuschku.quasseldroid.util.irc.format.model.FormatDescription
 import de.kuschku.quasseldroid.util.irc.format.model.FormatInfo
 import de.kuschku.quasseldroid.util.irc.format.model.IrcFormat
+import de.kuschku.quasseldroid.util.listener.LinkClickListener
 import javax.inject.Inject
 
 /**
@@ -71,6 +72,7 @@ class IrcFormatDeserializer(private val mircColors: IntArray) {
    * @return a CharSequence with Android’s span format representing the input string
    */
   fun formatString(str: String?, colorize: Boolean,
+                   onClickListener: LinkClickListener,
                    output: MutableList<FormatInfo>? = null): CharSequence {
     if (str == null) return ""
 
@@ -87,7 +89,7 @@ class IrcFormatDeserializer(private val mircColors: IntArray) {
       if (output != null) {
         output.add(FormatInfo(desc.start, plainText.length, desc.format))
       } else {
-        desc.apply(plainText, plainText.length)
+        desc.apply(plainText, plainText.length, onClickListener)
       }
     }
 
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/model/FormatDescription.kt b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/model/FormatDescription.kt
index 47f3328de..7826bd733 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/model/FormatDescription.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/model/FormatDescription.kt
@@ -20,9 +20,10 @@
 package de.kuschku.quasseldroid.util.irc.format.model
 
 import android.text.SpannableStringBuilder
+import de.kuschku.quasseldroid.util.listener.LinkClickListener
 
 class FormatDescription<out U : IrcFormat>(val start: Int, val format: U) {
-  fun apply(editable: SpannableStringBuilder, end: Int) {
-    format.applyTo(editable, start, end)
+  fun apply(editable: SpannableStringBuilder, end: Int, onClickListener: LinkClickListener) {
+    format.applyTo(editable, start, end, onClickListener)
   }
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/model/FormatInfo.kt b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/model/FormatInfo.kt
index 60abcb748..d8ea4ffc8 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/model/FormatInfo.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/model/FormatInfo.kt
@@ -20,9 +20,10 @@
 package de.kuschku.quasseldroid.util.irc.format.model
 
 import android.text.Spannable
+import de.kuschku.quasseldroid.util.listener.LinkClickListener
 
 data class FormatInfo(val start: Int, val end: Int, val format: IrcFormat) {
-  fun apply(editable: Spannable) {
-    format.applyTo(editable, start, end)
+  fun apply(editable: Spannable, onClickListener: LinkClickListener) {
+    format.applyTo(editable, start, end, onClickListener)
   }
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/model/IrcFormat.kt b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/model/IrcFormat.kt
index 63fc1135a..23f59564d 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/model/IrcFormat.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/model/IrcFormat.kt
@@ -22,48 +22,57 @@ package de.kuschku.quasseldroid.util.irc.format.model
 import android.text.Spannable
 import android.text.Spanned
 import de.kuschku.libquassel.protocol.NetworkId
-import de.kuschku.quasseldroid.util.irc.format.spans.*
+import de.kuschku.quasseldroid.util.irc.format.spans.ChannelLinkSpan
+import de.kuschku.quasseldroid.util.irc.format.spans.IrcBackgroundColorSpan
+import de.kuschku.quasseldroid.util.irc.format.spans.IrcBoldSpan
+import de.kuschku.quasseldroid.util.irc.format.spans.IrcForegroundColorSpan
+import de.kuschku.quasseldroid.util.irc.format.spans.IrcItalicSpan
+import de.kuschku.quasseldroid.util.irc.format.spans.IrcMonospaceSpan
+import de.kuschku.quasseldroid.util.irc.format.spans.IrcStrikethroughSpan
+import de.kuschku.quasseldroid.util.irc.format.spans.IrcUnderlineSpan
+import de.kuschku.quasseldroid.util.irc.format.spans.QuasselURLSpan
+import de.kuschku.quasseldroid.util.listener.LinkClickListener
 
 sealed class IrcFormat {
-  abstract fun applyTo(editable: Spannable, from: Int, to: Int)
+  abstract fun applyTo(editable: Spannable, from: Int, to: Int, onClickListener: LinkClickListener)
 
-  object Italic : IrcFormat() {
-    override fun applyTo(editable: Spannable, from: Int, to: Int) {
+  data object Italic : IrcFormat() {
+    override fun applyTo(editable: Spannable, from: Int, to: Int, onClickListener: LinkClickListener) {
       editable.setSpan(IrcItalicSpan(), from, to,
                        Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
     }
   }
 
-  object Underline : IrcFormat() {
-    override fun applyTo(editable: Spannable, from: Int, to: Int) {
+  data object Underline : IrcFormat() {
+    override fun applyTo(editable: Spannable, from: Int, to: Int, onClickListener: LinkClickListener) {
       editable.setSpan(IrcUnderlineSpan(), from, to,
                        Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
     }
   }
 
-  object Strikethrough : IrcFormat() {
-    override fun applyTo(editable: Spannable, from: Int, to: Int) {
+  data object Strikethrough : IrcFormat() {
+    override fun applyTo(editable: Spannable, from: Int, to: Int, onClickListener: LinkClickListener) {
       editable.setSpan(IrcStrikethroughSpan(), from, to,
                        Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
     }
   }
 
-  object Monospace : IrcFormat() {
-    override fun applyTo(editable: Spannable, from: Int, to: Int) {
+  data object Monospace : IrcFormat() {
+    override fun applyTo(editable: Spannable, from: Int, to: Int, onClickListener: LinkClickListener) {
       editable.setSpan(IrcMonospaceSpan(), from, to,
                        Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
     }
   }
 
-  object Bold : IrcFormat() {
-    override fun applyTo(editable: Spannable, from: Int, to: Int) {
+  data object Bold : IrcFormat() {
+    override fun applyTo(editable: Spannable, from: Int, to: Int, onClickListener: LinkClickListener) {
       editable.setSpan(IrcBoldSpan(), from, to,
                        Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
     }
   }
 
   data class Hex(val foreground: Int, val background: Int) : IrcFormat() {
-    override fun applyTo(editable: Spannable, from: Int, to: Int) {
+    override fun applyTo(editable: Spannable, from: Int, to: Int, onClickListener: LinkClickListener) {
       if (foreground >= 0) {
         editable.setSpan(
           IrcForegroundColorSpan.HEX(foreground or 0xFFFFFF.inv()), from, to,
@@ -81,7 +90,7 @@ sealed class IrcFormat {
 
   data class Color(val foreground: Byte, val background: Byte,
                    private val mircColors: IntArray) : IrcFormat() {
-    override fun applyTo(editable: Spannable, from: Int, to: Int) {
+    override fun applyTo(editable: Spannable, from: Int, to: Int, onClickListener: LinkClickListener) {
       if (foreground.toInt() >= 0 && foreground.toInt() < mircColors.size) {
         editable.setSpan(
           IrcForegroundColorSpan.MIRC(foreground.toInt(),
@@ -130,18 +139,18 @@ sealed class IrcFormat {
   }
 
   data class Url(val target: String, val highlight: Boolean) : IrcFormat() {
-    override fun applyTo(editable: Spannable, from: Int, to: Int) {
+    override fun applyTo(editable: Spannable, from: Int, to: Int, onClickListener: LinkClickListener) {
       editable.setSpan(
-        QuasselURLSpan(target, highlight), from, to, Spanned.SPAN_INCLUSIVE_EXCLUSIVE
+        QuasselURLSpan(target, highlight, onClickListener), from, to, Spanned.SPAN_INCLUSIVE_EXCLUSIVE
       )
     }
   }
 
   data class Channel(val networkId: NetworkId, val target: String, val highlight: Boolean) :
     IrcFormat() {
-    override fun applyTo(editable: Spannable, from: Int, to: Int) {
+    override fun applyTo(editable: Spannable, from: Int, to: Int, onClickListener: LinkClickListener) {
       editable.setSpan(
-        ChannelLinkSpan(networkId, target, highlight), from, to, Spanned.SPAN_INCLUSIVE_EXCLUSIVE
+        ChannelLinkSpan(networkId, target, highlight, onClickListener), from, to, Spanned.SPAN_INCLUSIVE_EXCLUSIVE
       )
     }
   }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/spans/ChannelLinkSpan.kt b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/spans/ChannelLinkSpan.kt
index 81f23cc15..f866e8829 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/spans/ChannelLinkSpan.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/spans/ChannelLinkSpan.kt
@@ -23,12 +23,13 @@ import android.text.TextPaint
 import android.text.style.ClickableSpan
 import android.view.View
 import de.kuschku.libquassel.protocol.NetworkId
-import de.kuschku.quasseldroid.ui.chat.ChatActivity
+import de.kuschku.quasseldroid.util.listener.LinkClickListener
 
 class ChannelLinkSpan(
   private val networkId: NetworkId,
   private val text: String,
-  private val highlight: Boolean
+  private val highlight: Boolean,
+  private val onClickListener: LinkClickListener
 ) : ClickableSpan() {
   override fun updateDrawState(ds: TextPaint) {
     if (!highlight) ds.color = ds.linkColor
@@ -36,8 +37,7 @@ class ChannelLinkSpan(
   }
 
   override fun onClick(widget: View) {
-    ChatActivity.launch(
-      widget.context,
+    onClickListener.openChannel(
       networkId = networkId,
       channel = text
     )
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/spans/QuasselURLSpan.kt b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/spans/QuasselURLSpan.kt
index d13cffac9..bab01a2cc 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/spans/QuasselURLSpan.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/spans/QuasselURLSpan.kt
@@ -19,27 +19,22 @@
 
 package de.kuschku.quasseldroid.util.irc.format.spans
 
-import android.content.ActivityNotFoundException
-import android.content.Intent
-import android.net.Uri
 import android.text.TextPaint
 import android.text.style.URLSpan
-import android.util.Log
 import android.view.View
+import de.kuschku.quasseldroid.util.listener.LinkClickListener
 
-class QuasselURLSpan(text: String, private val highlight: Boolean) : URLSpan(text) {
+class QuasselURLSpan(
+  text: String,
+  private val highlight: Boolean,
+  private val onClickListener: LinkClickListener
+) : URLSpan(text) {
   override fun updateDrawState(ds: TextPaint) {
     if (!highlight) ds.color = ds.linkColor
     ds.isUnderlineText = true
   }
 
   override fun onClick(widget: View) {
-    try {
-      widget.context?.startActivity(Intent(Intent.ACTION_VIEW).apply {
-        data = Uri.parse(url)
-      })
-    } catch (e: ActivityNotFoundException) {
-      Log.w("QuasselURLSpan", "Actvity was not found for $url")
-    }
+    onClickListener.openUrl(url)
   }
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/listener/AutocompleteTextHandler.kt b/app/src/main/java/de/kuschku/quasseldroid/util/listener/AutocompleteTextHandler.kt
new file mode 100644
index 000000000..39c72d890
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/listener/AutocompleteTextHandler.kt
@@ -0,0 +1,25 @@
+package de.kuschku.quasseldroid.util.listener
+
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class AutocompleteTextHandler @Inject constructor() : AutocompleteTextListener {
+  private val listeners = mutableListOf<AutocompleteTextListener>()
+  fun register(listener: AutocompleteTextListener) {
+    listeners.add(listener)
+  }
+
+  fun unregister(listener: AutocompleteTextListener) {
+    listeners.remove(listener)
+  }
+
+  override fun autocompleteText(
+    text: CharSequence,
+    suffix: String?,
+  ) {
+    for (listener in listeners) {
+      listener.autocompleteText(text, suffix)
+    }
+  }
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/listener/AutocompleteTextListener.kt b/app/src/main/java/de/kuschku/quasseldroid/util/listener/AutocompleteTextListener.kt
new file mode 100644
index 000000000..b74457ef5
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/listener/AutocompleteTextListener.kt
@@ -0,0 +1,8 @@
+package de.kuschku.quasseldroid.util.listener
+
+interface AutocompleteTextListener {
+  fun autocompleteText(
+    text: CharSequence,
+    suffix: String?
+  )
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/listener/LinkClickHandler.kt b/app/src/main/java/de/kuschku/quasseldroid/util/listener/LinkClickHandler.kt
new file mode 100644
index 000000000..035a3200c
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/listener/LinkClickHandler.kt
@@ -0,0 +1,61 @@
+package de.kuschku.quasseldroid.util.listener
+
+import de.kuschku.libquassel.protocol.BufferId
+import de.kuschku.libquassel.protocol.NetworkId
+import de.kuschku.quasseldroid.persistence.util.AccountId
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class LinkClickHandler @Inject constructor() : LinkClickListener {
+  private val listeners = mutableListOf<LinkClickListener>()
+  fun register(listener: LinkClickListener) {
+    listeners.add(listener)
+  }
+
+  fun unregister(listener: LinkClickListener) {
+    listeners.remove(listener)
+  }
+
+  override fun openBuffer(
+    bufferId: BufferId,
+    accountId: AccountId?,
+    forceJoin: Boolean
+  ) {
+    for (listener in listeners) {
+      listener.openBuffer(
+        bufferId, accountId, forceJoin
+      )
+    }
+  }
+
+  override fun openDirectMessage(
+    networkId: NetworkId,
+    nickName: String,
+    forceJoin: Boolean
+  ) {
+    for (listener in listeners) {
+      listener.openDirectMessage(
+        networkId, nickName, forceJoin
+      )
+    }
+  }
+
+  override fun openChannel(
+    networkId: NetworkId,
+    channel: String,
+    forceJoin: Boolean
+  ) {
+    for (listener in listeners) {
+      listener.openChannel(
+        networkId, channel, forceJoin
+      )
+    }
+  }
+
+  override fun openUrl(url: String) {
+    for (listener in listeners) {
+      listener.openUrl(url)
+    }
+  }
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/listener/LinkClickListener.kt b/app/src/main/java/de/kuschku/quasseldroid/util/listener/LinkClickListener.kt
new file mode 100644
index 000000000..04ed879b2
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/listener/LinkClickListener.kt
@@ -0,0 +1,27 @@
+package de.kuschku.quasseldroid.util.listener
+
+import de.kuschku.libquassel.protocol.BufferId
+import de.kuschku.libquassel.protocol.NetworkId
+import de.kuschku.quasseldroid.persistence.util.AccountId
+
+interface LinkClickListener {
+  fun openBuffer(
+    bufferId: BufferId,
+    accountId: AccountId? = null,
+    forceJoin: Boolean = false
+  )
+
+  fun openDirectMessage(
+    networkId: NetworkId,
+    nickName: String,
+    forceJoin: Boolean = false
+  )
+
+  fun openChannel(
+    networkId: NetworkId,
+    channel: String,
+    forceJoin: Boolean = false
+  )
+
+  fun openUrl(url: String)
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/listener/ListenerModule.kt b/app/src/main/java/de/kuschku/quasseldroid/util/listener/ListenerModule.kt
new file mode 100644
index 000000000..d4b3d3e03
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/listener/ListenerModule.kt
@@ -0,0 +1,13 @@
+package de.kuschku.quasseldroid.util.listener
+
+import dagger.Binds
+import dagger.Module
+
+@Module
+interface ListenerModule {
+  @Binds
+  fun bindAutocompleteTextListener(handler: AutocompleteTextHandler): AutocompleteTextListener
+
+  @Binds
+  fun bindlinkClickListener(handler: LinkClickHandler): LinkClickListener
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/listener/QuasselLinkClickListener.kt b/app/src/main/java/de/kuschku/quasseldroid/util/listener/QuasselLinkClickListener.kt
new file mode 100644
index 000000000..74b85299a
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/listener/QuasselLinkClickListener.kt
@@ -0,0 +1,27 @@
+package de.kuschku.quasseldroid.util.listener
+
+import de.kuschku.libquassel.protocol.BufferId
+import de.kuschku.libquassel.protocol.NetworkId
+import de.kuschku.quasseldroid.persistence.util.AccountId
+
+class QuasselLinkClickListener(
+  private val wrapped: LinkClickListener,
+  private val onClickQuasselLink: () -> Unit,
+) : LinkClickListener {
+  override fun openBuffer(bufferId: BufferId, accountId: AccountId?, forceJoin: Boolean) {
+    wrapped.openBuffer(bufferId, accountId, forceJoin)
+    onClickQuasselLink()
+  }
+
+  override fun openDirectMessage(networkId: NetworkId, nickName: String, forceJoin: Boolean) {
+    wrapped.openDirectMessage(networkId, nickName, forceJoin)
+    onClickQuasselLink()
+  }
+
+  override fun openChannel(networkId: NetworkId, channel: String, forceJoin: Boolean) {
+    wrapped.openChannel(networkId, channel, forceJoin)
+    onClickQuasselLink()
+  }
+
+  override fun openUrl(url: String) = wrapped.openUrl(url)
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/ui/BetterLinkMovementMethod.java b/app/src/main/java/de/kuschku/quasseldroid/util/ui/BetterLinkMovementMethod.java
index 572ebfd44..f9f587cae 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/util/ui/BetterLinkMovementMethod.java
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/ui/BetterLinkMovementMethod.java
@@ -53,7 +53,7 @@ public class BetterLinkMovementMethod extends LinkMovementMethod {
   private static final int LINKIFY_NONE = -2;
   private static BetterLinkMovementMethod singleInstance;
   private final RectF touchedLineBounds = new RectF();
-  private OnLinkClickListener onLinkClickListener;
+  private OnlinkClickListener onlinkClickListener;
   private OnLinkLongClickListener onLinkLongClickListener;
   private boolean isUrlHighlighted;
   private ClickableSpan clickableSpanUnderTouchOnActionDown;
@@ -178,13 +178,13 @@ public class BetterLinkMovementMethod extends LinkMovementMethod {
   /**
    * Set a listener that will get called whenever any link is clicked on the TextView.
    */
-  public BetterLinkMovementMethod setOnLinkClickListener(OnLinkClickListener clickListener) {
+  public BetterLinkMovementMethod setOnlinkClickListener(OnlinkClickListener clickListener) {
     if (this == singleInstance) {
       throw new UnsupportedOperationException("Setting a click listener on the instance returned by getInstance() is not supported to avoid memory " +
         "leaks. Please use newInstance() or any of the linkify() methods instead.");
     }
 
-    this.onLinkClickListener = clickListener;
+    this.onlinkClickListener = clickListener;
     return this;
   }
 
@@ -383,7 +383,9 @@ public class BetterLinkMovementMethod extends LinkMovementMethod {
 
   protected void dispatchUrlClick(TextView textView, ClickableSpan clickableSpan) {
     ClickableSpanWithText clickableSpanWithText = ClickableSpanWithText.ofSpan(textView, clickableSpan);
-    boolean handled = onLinkClickListener != null && onLinkClickListener.onClick(textView, clickableSpanWithText.text());
+    boolean handled = onlinkClickListener != null && onlinkClickListener.onClick(
+      textView,
+      clickableSpanWithText.text());
 
     if (!handled) {
       // Let Android handle this click.
@@ -401,7 +403,7 @@ public class BetterLinkMovementMethod extends LinkMovementMethod {
     }
   }
 
-  public interface OnLinkClickListener {
+  public interface OnlinkClickListener {
     /**
      * @param textView The TextView on which a click was registered.
      * @param url      The clicked URL.
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/ui/presenter/BufferPresenter.kt b/app/src/main/java/de/kuschku/quasseldroid/util/ui/presenter/BufferPresenter.kt
index 0bfa57680..5c8656860 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/util/ui/presenter/BufferPresenter.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/ui/presenter/BufferPresenter.kt
@@ -27,6 +27,7 @@ import de.kuschku.quasseldroid.util.ColorContext
 import de.kuschku.quasseldroid.util.avatars.AvatarHelper
 import de.kuschku.quasseldroid.util.irc.format.ContentFormatter
 import de.kuschku.quasseldroid.util.irc.format.IrcFormatDeserializer
+import de.kuschku.quasseldroid.util.listener.LinkClickListener
 import de.kuschku.quasseldroid.viewmodel.data.BufferListItem
 import de.kuschku.quasseldroid.viewmodel.data.BufferProps
 import de.kuschku.quasseldroid.viewmodel.data.BufferStatus
@@ -36,6 +37,7 @@ class BufferPresenter @Inject constructor(
   val appearanceSettings: AppearanceSettings,
   val messageSettings: MessageSettings,
   val ircFormatDeserializer: IrcFormatDeserializer,
+  val linkClickListener: LinkClickListener,
   val contentFormatter: ContentFormatter,
   val colorContext: ColorContext
 ) {
@@ -43,7 +45,7 @@ class BufferPresenter @Inject constructor(
     return props.copy(
       name = when {
         props.info.type.hasFlag(Buffer_Type.QueryBuffer)  ->
-          ircFormatDeserializer.formatString(props.info.bufferName, messageSettings.colorizeMirc)
+          ircFormatDeserializer.formatString(props.info.bufferName, messageSettings.colorizeMirc, linkClickListener)
         props.info.type.hasFlag(Buffer_Type.StatusBuffer) ->
           props.network.networkName
         else                                              ->
@@ -51,7 +53,8 @@ class BufferPresenter @Inject constructor(
       },
       description = ircFormatDeserializer.formatString(
         props.description.toString(),
-        colorize = messageSettings.colorizeMirc
+        messageSettings.colorizeMirc,
+        linkClickListener
       ),
       fallbackDrawable = if (props.info.type.hasFlag(Buffer_Type.QueryBuffer)) {
         props.ircUser?.let {
diff --git a/app/src/test/java/de/kuschku/quasseldroid/util/irc/format/IrcFormatDeserializerTest.kt b/app/src/test/java/de/kuschku/quasseldroid/util/irc/format/IrcFormatDeserializerTest.kt
index 63c0ee68f..5403b23f2 100644
--- a/app/src/test/java/de/kuschku/quasseldroid/util/irc/format/IrcFormatDeserializerTest.kt
+++ b/app/src/test/java/de/kuschku/quasseldroid/util/irc/format/IrcFormatDeserializerTest.kt
@@ -23,6 +23,8 @@ import android.os.Build
 import de.kuschku.quasseldroid.QuasseldroidTest
 import de.kuschku.quasseldroid.util.irc.format.model.FormatInfo
 import de.kuschku.quasseldroid.util.irc.format.model.IrcFormat
+import de.kuschku.quasseldroid.util.listener.LinkClickHandler
+import de.kuschku.quasseldroid.util.listener.LinkClickListener
 import org.junit.Assert.assertEquals
 import org.junit.Before
 import org.junit.Test
@@ -34,10 +36,12 @@ import org.robolectric.annotation.Config
 @RunWith(RobolectricTestRunner::class)
 class IrcFormatDeserializerTest {
   private lateinit var deserializer: IrcFormatDeserializer
+  private lateinit var linkClickListener: LinkClickListener
 
   @Before
   fun setUp() {
     deserializer = IrcFormatDeserializer(mircColors = colors)
+    linkClickListener = LinkClickHandler()
   }
 
   @Test
@@ -46,6 +50,7 @@ class IrcFormatDeserializerTest {
     val text = deserializer.formatString(
       str = "\u000301,01weeeeeeeeee",
       colorize = true,
+      onClickListener = linkClickListener,
       output = spans
     )
     assertEquals("weeeeeeeeee", text.toString())
-- 
GitLab