From e557c94575153622db794f5ba69a6fcdc8e5e46f Mon Sep 17 00:00:00 2001
From: Janne Koschinski <janne@kuschku.de>
Date: Wed, 17 Apr 2019 14:00:36 +0200
Subject: [PATCH] Major cleanup of viewmodel handling

---
 app/src/main/AndroidManifest.xml              |   2 +-
 .../quasseldroid/dagger/ActivityBaseModule.kt |  11 +-
 .../quasseldroid/dagger/ActivityModule.kt     |   2 +-
 .../quasseldroid/service/BacklogRequester.kt  |   7 +-
 .../service/QuasselNotificationBackend.kt     |   3 +-
 .../quasseldroid/ui/chat/ChatActivity.kt      |  52 +-
 .../quasseldroid/ui/chat/ToolbarFragment.kt   |   8 +-
 .../chat/add/create/ChannelCreateFragment.kt  |  17 +-
 .../chat/buffers/BufferViewConfigFragment.kt  |  55 +-
 .../ui/chat/input/AutoCompleteHelper.kt       |  11 +-
 .../ui/chat/input/ChatlineFragment.kt         |  24 +-
 .../ui/chat/messages/MessageListFragment.kt   |  60 +-
 .../chat/messages/QuasselMessageRenderer.kt   |   5 +-
 .../ui/chat/nicks/NickListFragment.kt         |  12 +-
 .../ui/{info => chat}/topic/TopicActivity.kt  |   3 +-
 .../ui/chat/topic/TopicFragment.kt            |  17 +-
 .../ui/chat/topic/TopicFragmentProvider.kt    |   1 -
 .../ui/coresettings/CoreSettingsFragment.kt   |  13 +-
 .../aliasitem/AliasItemFragment.kt            |   9 +-
 .../aliaslist/AliasListFragment.kt            |   7 +-
 .../chatlist/ChatListBaseFragment.kt          |  11 +-
 .../chatlist/ChatListCreateFragment.kt        |   2 +-
 .../chatlist/ChatListEditFragment.kt          |   2 +-
 .../highlightlist/HighlightListFragment.kt    |   9 +-
 .../identity/IdentityBaseFragment.kt          |   9 +-
 .../identity/IdentityCreateFragment.kt        |   2 +-
 .../identity/IdentityEditFragment.kt          |   2 +-
 .../ignorelist/IgnoreListFragment.kt          |   7 +-
 .../network/NetworkBaseFragment.kt            |  15 +-
 .../network/NetworkCreateFragment.kt          |   4 +-
 .../network/NetworkEditFragment.kt            |   2 +-
 .../networkconfig/NetworkConfigFragment.kt    |   7 +-
 .../passwordchange/PasswordChangeFragment.kt  |  16 +-
 .../certificate/CertificateInfoFragment.kt    |   6 +-
 .../ui/info/channel/ChannelInfoFragment.kt    |  12 +-
 .../info/channellist/ChannelListFragment.kt   |   8 +-
 .../ui/info/core/CoreInfoFragment.kt          |  27 +-
 .../ui/info/user/UserInfoFragment.kt          |  14 +-
 .../ui/setup/ServiceBoundSlideFragment.kt     |   3 -
 .../ui/setup/core/CoreSetupActivity.kt        |   7 +-
 .../ui/setup/network/NetworkSetupActivity.kt  |   9 +-
 .../setup/network/NetworkSetupNetworkSlide.kt |   9 +-
 .../ui/setup/user/UserSetupActivity.kt        |  11 +-
 .../kuschku/quasseldroid/util/ColorContext.kt |   3 +-
 .../util/ShortcutCreationHelper.kt            |   3 +-
 .../util/service/ServiceBoundActivity.kt      |   4 +-
 .../util/service/ServiceBoundFragment.kt      |   2 +-
 viewmodel/build.gradle.kts                    |   2 +
 .../quasseldroid/viewmodel/ChatViewModel.kt   |  70 +++
 .../quasseldroid/viewmodel/EditorViewModel.kt | 145 -----
 .../viewmodel/QuasselViewModel.kt             | 553 +-----------------
 .../viewmodel/helper/ChatViewModelHelper.kt   | 434 ++++++++++++++
 .../viewmodel/helper/EditorViewModelHelper.kt | 155 +++++
 .../helper/QuasselViewModelHelper.kt          | 110 ++++
 54 files changed, 1100 insertions(+), 894 deletions(-)
 rename app/src/main/java/de/kuschku/quasseldroid/ui/{info => chat}/topic/TopicActivity.kt (92%)
 create mode 100644 viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/ChatViewModel.kt
 create mode 100644 viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/helper/ChatViewModelHelper.kt
 create mode 100644 viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/helper/EditorViewModelHelper.kt
 create mode 100644 viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/helper/QuasselViewModelHelper.kt

diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 96c17e9a8..ad2730061 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -97,7 +97,7 @@
       android:parentActivityName=".ui.chat.ChatActivity"
       android:windowSoftInputMode="adjustResize" />
     <activity
-      android:name="de.kuschku.quasseldroid.ui.info.topic.TopicActivity"
+      android:name="de.kuschku.quasseldroid.ui.chat.topic.TopicActivity"
       android:exported="false"
       android:label="@string/label_topic"
       android:windowSoftInputMode="adjustResize" />
diff --git a/app/src/main/java/de/kuschku/quasseldroid/dagger/ActivityBaseModule.kt b/app/src/main/java/de/kuschku/quasseldroid/dagger/ActivityBaseModule.kt
index 7887f9287..044409099 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/dagger/ActivityBaseModule.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/dagger/ActivityBaseModule.kt
@@ -26,6 +26,7 @@ import androidx.lifecycle.ViewModelProviders
 import dagger.Module
 import dagger.Provides
 import de.kuschku.quasseldroid.ui.setup.accounts.selection.AccountViewModel
+import de.kuschku.quasseldroid.viewmodel.ChatViewModel
 import de.kuschku.quasseldroid.viewmodel.EditorViewModel
 import de.kuschku.quasseldroid.viewmodel.QuasselViewModel
 
@@ -50,12 +51,18 @@ object ActivityBaseModule {
   @ActivityScope
   @Provides
   @JvmStatic
-  fun provideAccountViewModel(viewModelProvider: ViewModelProvider) =
-    viewModelProvider[AccountViewModel::class.java]
+  fun provideChatViewModel(viewModelProvider: ViewModelProvider) =
+    viewModelProvider[ChatViewModel::class.java]
 
   @ActivityScope
   @Provides
   @JvmStatic
   fun provideEditorViewModel(viewModelProvider: ViewModelProvider) =
     viewModelProvider[EditorViewModel::class.java]
+
+  @ActivityScope
+  @Provides
+  @JvmStatic
+  fun provideAccountViewModel(viewModelProvider: ViewModelProvider) =
+    viewModelProvider[AccountViewModel::class.java]
 }
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 845a4fc90..5a0572b9b 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/dagger/ActivityModule.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/dagger/ActivityModule.kt
@@ -83,7 +83,7 @@ import de.kuschku.quasseldroid.ui.info.channellist.ChannelListActivity
 import de.kuschku.quasseldroid.ui.info.channellist.ChannelListFragmentProvider
 import de.kuschku.quasseldroid.ui.info.core.CoreInfoActivity
 import de.kuschku.quasseldroid.ui.info.core.CoreInfoFragmentProvider
-import de.kuschku.quasseldroid.ui.info.topic.TopicActivity
+import de.kuschku.quasseldroid.ui.chat.topic.TopicActivity
 import de.kuschku.quasseldroid.ui.info.user.UserInfoActivity
 import de.kuschku.quasseldroid.ui.info.user.UserInfoFragmentProvider
 import de.kuschku.quasseldroid.ui.setup.accounts.edit.AccountEditActivity
diff --git a/app/src/main/java/de/kuschku/quasseldroid/service/BacklogRequester.kt b/app/src/main/java/de/kuschku/quasseldroid/service/BacklogRequester.kt
index 41cfe8e37..0302c6cfd 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/service/BacklogRequester.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/service/BacklogRequester.kt
@@ -23,6 +23,7 @@ import de.kuschku.libquassel.protocol.BufferId
 import de.kuschku.libquassel.protocol.Message
 import de.kuschku.libquassel.protocol.MsgId
 import de.kuschku.libquassel.session.ISession
+import de.kuschku.libquassel.util.Optional
 import de.kuschku.libquassel.util.compatibility.LoggingHandler.Companion.log
 import de.kuschku.libquassel.util.compatibility.LoggingHandler.LogLevel.DEBUG
 import de.kuschku.libquassel.util.helpers.value
@@ -31,10 +32,10 @@ import de.kuschku.quasseldroid.persistence.dao.get
 import de.kuschku.quasseldroid.persistence.db.AccountDatabase
 import de.kuschku.quasseldroid.persistence.db.QuasselDatabase
 import de.kuschku.quasseldroid.persistence.util.QuasselBacklogStorage
-import de.kuschku.quasseldroid.viewmodel.QuasselViewModel
+import io.reactivex.Observable
 
 class BacklogRequester(
-  private val viewModel: QuasselViewModel,
+  private val session: Observable<Optional<ISession>>,
   private val database: QuasselDatabase,
   private val accountDatabase: AccountDatabase
 ) {
@@ -46,7 +47,7 @@ class BacklogRequester(
         "BacklogRequester",
         "requested(buffer: $buffer, amount: $amount, pageSize: $pageSize, lastMessageId: $lastMessageId, untilAllVisible: $untilAllVisible)")
     var missing = amount
-    viewModel.session.value?.orNull()?.let { session: ISession ->
+    session.value?.orNull()?.let { session: ISession ->
       session.backlogManager.let {
         val filtered = database.filtered().get(accountId,
                                                buffer,
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 13105edf5..9f35585b1 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/service/QuasselNotificationBackend.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/service/QuasselNotificationBackend.kt
@@ -56,6 +56,7 @@ import de.kuschku.quasseldroid.util.helper.styledAttributes
 import de.kuschku.quasseldroid.util.irc.format.ContentFormatter
 import de.kuschku.quasseldroid.util.ui.drawable.TextDrawable
 import de.kuschku.quasseldroid.viewmodel.EditorViewModel
+import de.kuschku.quasseldroid.viewmodel.helper.EditorViewModelHelper.Companion.IGNORED_CHARS
 import org.threeten.bp.Instant
 import java.util.concurrent.Executors
 import java.util.concurrent.TimeUnit
@@ -299,7 +300,7 @@ class QuasselNotificationBackend @Inject constructor(
       fun obtainAvatar(nickName: String, ident: String, realName: String, avatarUrl: String,
                        self: Boolean): Drawable {
         val senderColorIndex = SenderColorUtil.senderColor(nickName)
-        val rawInitial = nickName.trimStart(*EditorViewModel.IGNORED_CHARS)
+        val rawInitial = nickName.trimStart(*IGNORED_CHARS)
                            .firstOrNull() ?: nickName.firstOrNull()
         val initial = rawInitial?.toUpperCase().toString()
         val senderColor = when (messageSettings.colorizeNicknames) {
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 05d6bee4e..72f906dc2 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
@@ -98,7 +98,9 @@ import de.kuschku.quasseldroid.util.ui.drawable.DrawerToggleActivityDrawable
 import de.kuschku.quasseldroid.util.ui.drawable.NickCountDrawable
 import de.kuschku.quasseldroid.util.ui.view.MaterialContentLoadingProgressBar
 import de.kuschku.quasseldroid.util.ui.view.WarningBarView
+import de.kuschku.quasseldroid.viewmodel.ChatViewModel
 import de.kuschku.quasseldroid.viewmodel.data.BufferData
+import de.kuschku.quasseldroid.viewmodel.helper.ChatViewModelHelper
 import io.reactivex.BackpressureStrategy
 import org.threeten.bp.Instant
 import org.threeten.bp.ZoneId
@@ -126,6 +128,12 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
   @BindView(R.id.autocomplete_list)
   lateinit var autoCompleteList: RecyclerView
 
+  @Inject
+  lateinit var modelHelper: ChatViewModelHelper
+
+  @Inject
+  lateinit var chatViewModel: ChatViewModel
+
   @Inject
   lateinit var database: QuasselDatabase
 
@@ -171,8 +179,8 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
           }
         }
         intent.hasExtra(KEY_BUFFER_ID)                                  -> {
-          viewModel.buffer.onNext(BufferId(intent.getIntExtra(KEY_BUFFER_ID, -1)))
-          viewModel.bufferOpened.onNext(Unit)
+          chatViewModel.buffer.onNext(BufferId(intent.getIntExtra(KEY_BUFFER_ID, -1)))
+          chatViewModel.bufferOpened.onNext(Unit)
           if (intent.hasExtra(KEY_ACCOUNT_ID)) {
             val accountId = intent.getLongExtra(ChatActivity.KEY_ACCOUNT_ID, -1)
             if (accountId != this.accountId) {
@@ -196,7 +204,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
           val networkId = NetworkId(intent.getIntExtra(KEY_NETWORK_ID, -1))
           val channel = intent.getStringExtra(KEY_CHANNEL)
 
-          viewModel.session.filter(Optional<ISession>::isPresent).firstElement().subscribe {
+          modelHelper.session.filter(Optional<ISession>::isPresent).firstElement().subscribe {
             it.orNull()?.also { session ->
               val info = session.bufferSyncer.find(
                 bufferName = channel,
@@ -207,7 +215,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
               if (info != null) {
                 ChatActivity.launch(this, bufferId = info.bufferId)
               } else {
-                viewModel.allBuffers.map {
+                modelHelper.allBuffers.map {
                   listOfNotNull(it.find {
                     it.networkId == networkId &&
                     it.bufferName == channel &&
@@ -268,7 +276,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
 
     setSupportActionBar(toolbar)
 
-    viewModel.bufferOpened.toLiveData().observe(this, Observer {
+    chatViewModel.bufferOpened.toLiveData().observe(this, Observer {
       actionMode?.finish()
       if (drawerLayout.isDrawerOpen(GravityCompat.START)) {
         drawerLayout.closeDrawer(GravityCompat.START, true)
@@ -303,7 +311,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
     })
 
     val maxBufferActivity = combineLatest(
-      viewModel.bufferList,
+      modelHelper.bufferList,
       database.filtered().listenRx(accountId).toObservable().map {
         it.associateBy(Filtered::bufferId, Filtered::filtered)
       },
@@ -397,7 +405,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
     }
     // If we connect to a new network without statusbuffer, the bufferid may be -networkId.
     // In that case, once we’re connected (and a status buffer exists), we want to switch to it.
-    combineLatest(viewModel.allBuffers, viewModel.buffer).map { (buffers, current) ->
+    combineLatest(modelHelper.allBuffers, chatViewModel.buffer).map { (buffers, current) ->
       if (current.isValidId()) Optional.empty()
       else Optional.ofNullable(buffers.firstOrNull {
         it.networkId == NetworkId(-current.id) && it.type.hasFlag(Buffer_Type.StatusBuffer)
@@ -409,7 +417,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
     })
 
     // User-actionable errors that require immediate action, and should show up as dialog
-    viewModel.errors.toLiveData(BackpressureStrategy.BUFFER).observe(this, Observer { error ->
+    modelHelper.errors.toLiveData(BackpressureStrategy.BUFFER).observe(this, Observer { error ->
       error?.let {
         when (it) {
           is Error.HandshakeError -> it.message.let {
@@ -666,7 +674,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
     })
 
     // Connection errors that should show up as toast
-    viewModel.connectionErrors.toLiveData().observe(this, Observer { error ->
+    modelHelper.connectionErrors.toLiveData().observe(this, Observer { error ->
       error?.let {
         val cause = it.cause
         when {
@@ -712,19 +720,19 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
     })
 
     // After initial connect, open the drawer
-    viewModel.connectionProgress
+    modelHelper.connectionProgress
       .filter { (it, _, _) -> it == ConnectionState.CONNECTED }
       .firstElement()
       .toLiveData()
       .observe(this, Observer {
         if (connectedAccount != accountId) {
           if (resources.getBoolean(R.bool.buffer_drawer_exists) &&
-              viewModel.buffer.value == BufferId.MAX_VALUE &&
+              chatViewModel.buffer.value == BufferId.MAX_VALUE &&
               !restoredDrawerState) {
             drawerLayout.openDrawer(GravityCompat.START)
           }
           connectedAccount = accountId
-          viewModel.session.value?.orNull()?.let { session ->
+          modelHelper.session.value?.orNull()?.let { session ->
             if (session.identities.isEmpty()) {
               UserSetupActivity.launch(this)
             }
@@ -754,7 +762,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
       })
 
     connectionStatusDisplay.setOnClickListener {
-      viewModel.sessionManager.value?.orNull()?.apply {
+      modelHelper.sessionManager.value?.orNull()?.apply {
         ifDisconnected {
           autoReconnect()
         }
@@ -762,7 +770,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
     }
 
     // Show Connection Progress Bar
-    viewModel.connectionProgress.toLiveData().observe(this, Observer {
+    modelHelper.connectionProgress.toLiveData().observe(this, Observer {
       val (state, progress, max) = it ?: Triple(ConnectionState.DISCONNECTED, 0, 0)
       when (state) {
         ConnectionState.DISCONNECTED,
@@ -805,7 +813,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
     })
 
     // Only show nick list when we’re in a channel buffer
-    viewModel.bufferDataThrottled.toLiveData().observe(this, Observer {
+    modelHelper.bufferDataThrottled.toLiveData().observe(this, Observer {
       bufferData = it
       if (bufferData?.info?.type?.hasFlag(Buffer_Type.ChannelBuffer) == true) {
         drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.END)
@@ -881,8 +889,9 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
 
   override fun onSaveInstanceState(outState: Bundle) {
     super.onSaveInstanceState(outState)
-    outState.putInt(KEY_OPEN_BUFFER, viewModel.buffer.value.id)
-    outState.putInt(KEY_OPEN_BUFFERVIEWCONFIG, viewModel.bufferViewConfigId.value ?: -1)
+    // TODO: store entire chatviewmodel
+    outState.putInt(KEY_OPEN_BUFFER, chatViewModel.buffer.value.id)
+    outState.putInt(KEY_OPEN_BUFFERVIEWCONFIG, chatViewModel.bufferViewConfigId.value ?: -1)
     outState.putLong(KEY_CONNECTED_ACCOUNT, connectedAccount)
     outState.putBoolean(KEY_OPEN_DRAWER_START, drawerLayout.isDrawerOpen(GravityCompat.START))
     outState.putBoolean(KEY_OPEN_DRAWER_END, drawerLayout.isDrawerOpen(GravityCompat.END))
@@ -890,8 +899,9 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
 
   override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
     super.onRestoreInstanceState(savedInstanceState)
-    viewModel.buffer.onNext(BufferId(savedInstanceState?.getInt(KEY_OPEN_BUFFER, -1) ?: -1))
-    viewModel.bufferViewConfigId.onNext(savedInstanceState?.getInt(KEY_OPEN_BUFFERVIEWCONFIG, -1)
+    // TODO: restore entire chatviewmodel
+    chatViewModel.buffer.onNext(BufferId(savedInstanceState?.getInt(KEY_OPEN_BUFFER, -1) ?: -1))
+    chatViewModel.bufferViewConfigId.onNext(savedInstanceState?.getInt(KEY_OPEN_BUFFERVIEWCONFIG, -1)
                                         ?: -1)
     connectedAccount = savedInstanceState?.getLong(KEY_CONNECTED_ACCOUNT, -1L) ?: -1L
 
@@ -942,7 +952,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
     }
     R.id.action_filter_messages -> {
       runInBackground {
-        viewModel.buffer { buffer ->
+        chatViewModel.buffer { buffer ->
           val filteredRaw = database.filtered().get(
             accountId,
             buffer,
@@ -1070,7 +1080,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
     connectedAccount = -1L
     restoredDrawerState = false
     ChatActivity.launch(this, bufferId = BufferId.MAX_VALUE)
-    viewModel.resetAccount()
+    chatViewModel.resetAccount()
   }
 
   override fun onSelectAccount() {
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 5ba0e602c..55a290928 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
@@ -44,6 +44,7 @@ import de.kuschku.quasseldroid.util.helper.*
 import de.kuschku.quasseldroid.util.irc.format.IrcFormatDeserializer
 import de.kuschku.quasseldroid.util.service.ServiceBoundFragment
 import de.kuschku.quasseldroid.util.ui.SpanFormatter
+import de.kuschku.quasseldroid.viewmodel.helper.EditorViewModelHelper
 import javax.inject.Inject
 
 class ToolbarFragment : ServiceBoundFragment() {
@@ -59,6 +60,9 @@ class ToolbarFragment : ServiceBoundFragment() {
   @BindView(R.id.toolbar_action_area)
   lateinit var actionArea: View
 
+  @Inject
+  lateinit var modelHelper: EditorViewModelHelper
+
   @Inject
   lateinit var ircFormatDeserializer: IrcFormatDeserializer
 
@@ -100,7 +104,7 @@ class ToolbarFragment : ServiceBoundFragment() {
 
     val colorContext = ColorContext(requireContext(), messageSettings)
 
-    combineLatest(viewModel.bufferDataThrottled, viewModel.lag).map {
+    combineLatest(modelHelper.bufferDataThrottled, modelHelper.lag).map {
       val avatarInfo = it.first?.ircUser?.let { user ->
         val avatarUrls = AvatarHelper.avatar(messageSettings, user, avatarSize)
 
@@ -156,7 +160,7 @@ class ToolbarFragment : ServiceBoundFragment() {
       })
 
     actionArea.setOnClickListener {
-      val bufferData = viewModel.bufferData.value
+      val bufferData = modelHelper.bufferData.value
       bufferData?.info?.let { info ->
         when (info.type.toInt()) {
           BufferInfo.Type.QueryBuffer.toInt()   -> {
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 3ef7d379c..4de526216 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
@@ -42,7 +42,9 @@ import de.kuschku.quasseldroid.util.helper.combineLatest
 import de.kuschku.quasseldroid.util.helper.setDependent
 import de.kuschku.quasseldroid.util.helper.toLiveData
 import de.kuschku.quasseldroid.util.ui.settings.fragment.ServiceBoundSettingsFragment
+import de.kuschku.quasseldroid.viewmodel.helper.QuasselViewModelHelper
 import io.reactivex.Observable
+import javax.inject.Inject
 
 class ChannelCreateFragment : ServiceBoundSettingsFragment() {
   @BindView(R.id.network)
@@ -69,6 +71,9 @@ class ChannelCreateFragment : ServiceBoundSettingsFragment() {
   @BindView(R.id.save)
   lateinit var save: Button
 
+  @Inject
+  lateinit var modelHelper: QuasselViewModelHelper
+
   override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                             savedInstanceState: Bundle?): View? {
     val view = inflater.inflate(R.layout.add_create, container, false)
@@ -77,7 +82,7 @@ class ChannelCreateFragment : ServiceBoundSettingsFragment() {
     val networkAdapter = NetworkAdapter()
     network.adapter = networkAdapter
 
-    viewModel.networks.switchMap {
+    modelHelper.networks.switchMap {
       combineLatest(it.values.map(Network::liveNetworkInfo)).map {
         it.map {
           NetworkItem(
@@ -106,13 +111,13 @@ class ChannelCreateFragment : ServiceBoundSettingsFragment() {
       val isPasswordProtected = passwordProtected.isChecked
       val channelPassword = password.text.toString().trim()
 
-      viewModel.bufferSyncer.value?.orNull()?.let { bufferSyncer ->
+      modelHelper.bufferSyncer.value?.orNull()?.let { bufferSyncer ->
         val existingBuffer = bufferSyncer.find(
           networkId = networkId,
           type = Buffer_Type.of(Buffer_Type.ChannelBuffer),
           bufferName = channelName
         )
-        val existingChannel = viewModel.networks.value?.get(networkId)?.ircChannel(channelName)
+        val existingChannel = modelHelper.networks.value?.get(networkId)?.ircChannel(channelName)
           .nullIf { it == IrcChannel.NULL }
         if (existingBuffer != null) {
           if (existingChannel == null) {
@@ -120,7 +125,7 @@ class ChannelCreateFragment : ServiceBoundSettingsFragment() {
               networkId = networkId,
               type = Buffer_Type.of(Buffer_Type.StatusBuffer)
             )?.let { statusBuffer ->
-              viewModel.session.value?.orNull()?.rpcHandler?.apply {
+              modelHelper.session.value?.orNull()?.rpcHandler?.apply {
                 sendInput(statusBuffer, "/join $channelName")
               }
             }
@@ -137,9 +142,9 @@ class ChannelCreateFragment : ServiceBoundSettingsFragment() {
             networkId = networkId,
             type = Buffer_Type.of(Buffer_Type.StatusBuffer)
           )?.let { statusBuffer ->
-            viewModel.session.value?.orNull()?.rpcHandler?.apply {
+            modelHelper.session.value?.orNull()?.rpcHandler?.apply {
               sendInput(statusBuffer, "/join $channelName")
-              viewModel.networks.switchMap {
+              modelHelper.networks.switchMap {
                 it[networkId]?.liveIrcChannel(channelName)
                   ?: Observable.empty()
               }.subscribe {
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 6b917836d..7d48ca42d 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
@@ -75,6 +75,7 @@ import de.kuschku.quasseldroid.viewmodel.data.BufferHiddenState
 import de.kuschku.quasseldroid.viewmodel.data.BufferListItem
 import de.kuschku.quasseldroid.viewmodel.data.BufferState
 import de.kuschku.quasseldroid.viewmodel.data.BufferStatus
+import de.kuschku.quasseldroid.viewmodel.helper.ChatViewModelHelper
 import javax.inject.Inject
 
 class BufferViewConfigFragment : ServiceBoundFragment() {
@@ -117,16 +118,19 @@ class BufferViewConfigFragment : ServiceBoundFragment() {
   @Inject
   lateinit var ircFormatDeserializer: IrcFormatDeserializer
 
+  @Inject
+  lateinit var modelHelper: ChatViewModelHelper
+
   private var actionMode: ActionMode? = null
 
   private val actionModeCallback = object : ActionMode.Callback {
     override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
-      val selected = viewModel.selectedBuffer.value
+      val selected = modelHelper.selectedBuffer.value
       val info = selected?.info
-      val session = viewModel.session.value?.orNull()
+      val session = modelHelper.session.value?.orNull()
       val bufferSyncer = session?.bufferSyncer
       val network = session?.networks?.get(selected?.info?.networkId)
-      val bufferViewConfig = viewModel.bufferViewConfig.value
+      val bufferViewConfig = modelHelper.bufferViewConfig.value
 
       return if (info != null && session != null) {
         when (item?.itemId) {
@@ -258,7 +262,7 @@ class BufferViewConfigFragment : ServiceBoundFragment() {
     ButterKnife.bind(this, view)
 
     val adapter = BufferViewConfigAdapter()
-    viewModel.bufferViewConfigs.switchMap {
+    modelHelper.bufferViewConfigs.switchMap {
       combineLatest(it.map(BufferViewConfig::liveUpdates))
     }.toLiveData().observe(this, Observer {
       if (it != null) {
@@ -269,30 +273,30 @@ class BufferViewConfigFragment : ServiceBoundFragment() {
     var hasSetBufferViewConfigId = false
     adapter.setOnUpdateFinishedListener {
       if (!hasSetBufferViewConfigId) {
-        chatListSpinner.setSelection(adapter.indexOf(viewModel.bufferViewConfigId.value).nullIf { it == -1 }
-          ?: 0)
-        viewModel.bufferViewConfigId.onNext(chatListSpinner.selectedItemId.toInt())
+        chatListSpinner.setSelection(adapter.indexOf(modelHelper.chat.bufferViewConfigId.value).nullIf { it == -1 }
+                                     ?: 0)
+        modelHelper.chat.bufferViewConfigId.onNext(chatListSpinner.selectedItemId.toInt())
         hasSetBufferViewConfigId = true
       }
     }
     chatListSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
       override fun onNothingSelected(adapter: AdapterView<*>?) {
         if (hasSetBufferViewConfigId)
-          viewModel.bufferViewConfigId.onNext(-1)
+          modelHelper.chat.bufferViewConfigId.onNext(-1)
       }
 
       override fun onItemSelected(adapter: AdapterView<*>?, element: View?, position: Int,
                                   id: Long) {
         if (hasSetBufferViewConfigId)
-          viewModel.bufferViewConfigId.onNext(id.toInt())
+          modelHelper.chat.bufferViewConfigId.onNext(id.toInt())
       }
     }
     chatListSpinner.adapter = adapter
 
     listAdapter = BufferListAdapter(
       messageSettings,
-      viewModel.selectedBufferId,
-      viewModel.expandedNetworks
+      modelHelper.chat.selectedBufferId,
+      modelHelper.chat.expandedNetworks
     )
 
     val avatarSize = resources.getDimensionPixelSize(R.dimen.avatar_size_buffer)
@@ -318,16 +322,17 @@ class BufferViewConfigFragment : ServiceBoundFragment() {
       }
     }
 
-    viewModel.negotiatedFeatures.toLiveData().observe(this, Observer { (connected, features) ->
+    modelHelper.negotiatedFeatures.toLiveData().observe(this, Observer { (connected, features) ->
       featureContextBufferActivitySync.setMode(
         if (!connected || features.hasFeature(ExtendedFeature.BufferActivitySync)) WarningBarView.MODE_NONE
         else WarningBarView.MODE_ICON
       )
     })
 
-    combineLatest(viewModel.bufferList,
-      viewModel.expandedNetworks,
-      viewModel.selectedBuffer,
+    combineLatest(
+      modelHelper.bufferList,
+      modelHelper.chat.expandedNetworks,
+      modelHelper.selectedBuffer,
       database.filtered().listenRx(accountId).toObservable(),
       accountDatabase.accounts().listenDefaultFiltered(accountId, 0).toObservable()
     ).toLiveData().observe(this, Observer { it ->
@@ -405,7 +410,7 @@ class BufferViewConfigFragment : ServiceBoundFragment() {
     listAdapter.setOnLongClickListener(this@BufferViewConfigFragment::longClickListener)
     chatList.adapter = listAdapter
 
-    viewModel.selectedBuffer.toLiveData().observe(this, Observer { buffer ->
+    modelHelper.selectedBuffer.toLiveData().observe(this, Observer { buffer ->
       if (buffer != null) {
         val menu = actionMode?.menu
         if (menu != null) {
@@ -480,17 +485,17 @@ class BufferViewConfigFragment : ServiceBoundFragment() {
     })
 
     chatListToolbar.inflateMenu(R.menu.context_bufferlist)
-    chatListToolbar.menu.findItem(R.id.action_search).isChecked = viewModel.bufferSearchTemporarilyVisible.value
+    chatListToolbar.menu.findItem(R.id.action_search).isChecked = modelHelper.chat.bufferSearchTemporarilyVisible.value
     chatListToolbar.setOnMenuItemClickListener { item ->
       when (item.itemId) {
         R.id.action_search -> {
           item.isChecked = !item.isChecked
-          viewModel.bufferSearchTemporarilyVisible.onNext(item.isChecked)
+          modelHelper.chat.bufferSearchTemporarilyVisible.onNext(item.isChecked)
           true
         }
         R.id.action_show_hidden -> {
           item.isChecked = !item.isChecked
-          viewModel.showHidden.onNext(item.isChecked)
+          modelHelper.chat.showHidden.onNext(item.isChecked)
           true
         }
         else -> false
@@ -502,17 +507,17 @@ class BufferViewConfigFragment : ServiceBoundFragment() {
     chatList.itemAnimator = DefaultItemAnimator()
     chatList.setItemViewCacheSize(10)
 
-    viewModel.stateReset.toLiveData().observe(this, Observer {
+    modelHelper.chat.stateReset.toLiveData().observe(this, Observer {
       listAdapter.submitList(emptyList())
       hasSetBufferViewConfigId = false
     })
 
-    val bufferSearchPermanentlyVisible = viewModel.bufferViewConfig
+    val bufferSearchPermanentlyVisible = modelHelper.bufferViewConfig
       .mapMap(BufferViewConfig::showSearch)
       .mapOrElse(false)
 
-    combineLatest(viewModel.bufferSearchTemporarilyVisible.distinctUntilChanged(),
-      bufferSearchPermanentlyVisible)
+    combineLatest(modelHelper.chat.bufferSearchTemporarilyVisible.distinctUntilChanged(),
+                  bufferSearchPermanentlyVisible)
       .toLiveData().observe(this, Observer { (temporarily, permanently) ->
         val visible = temporarily || permanently
 
@@ -527,7 +532,7 @@ class BufferViewConfigFragment : ServiceBoundFragment() {
 
     bufferSearch.addTextChangedListener(object : TextWatcher {
       override fun afterTextChanged(s: Editable) {
-        viewModel.bufferSearch.onNext(s.toString())
+        modelHelper.chat.bufferSearch.onNext(s.toString())
       }
 
       override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit
@@ -627,7 +632,7 @@ class BufferViewConfigFragment : ServiceBoundFragment() {
       longClickListener(bufferId)
     } else {
       context?.let {
-        viewModel.bufferSearchTemporarilyVisible.onNext(false)
+        modelHelper.chat.bufferSearchTemporarilyVisible.onNext(false)
         ChatActivity.launch(it, 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 ac177c683..1d139eadf 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
@@ -45,16 +45,17 @@ import de.kuschku.quasseldroid.util.helper.styledAttributes
 import de.kuschku.quasseldroid.util.helper.toLiveData
 import de.kuschku.quasseldroid.util.irc.format.IrcFormatDeserializer
 import de.kuschku.quasseldroid.viewmodel.EditorViewModel
-import de.kuschku.quasseldroid.viewmodel.EditorViewModel.Companion.IGNORED_CHARS
 import de.kuschku.quasseldroid.viewmodel.data.AutoCompleteItem
 import de.kuschku.quasseldroid.viewmodel.data.BufferStatus
+import de.kuschku.quasseldroid.viewmodel.helper.EditorViewModelHelper
+import de.kuschku.quasseldroid.viewmodel.helper.EditorViewModelHelper.Companion.IGNORED_CHARS
 
 class AutoCompleteHelper(
   activity: FragmentActivity,
   private val autoCompleteSettings: AutoCompleteSettings,
   private val messageSettings: MessageSettings,
   private val ircFormatDeserializer: IrcFormatDeserializer,
-  private val viewModel: EditorViewModel
+  private val helper: EditorViewModelHelper
 ) {
   private var autocompleteListener: ((AutoCompletionState) -> Unit)? = null
   private var dataListeners: List<((List<AutoCompleteItem>) -> Unit)> = emptyList()
@@ -85,7 +86,7 @@ class AutoCompleteHelper(
   private val colorContext = ColorContext(activity, messageSettings)
 
   init {
-    viewModel.autoCompleteData.toLiveData().observe(activity, Observer {
+    helper.autoCompleteData.toLiveData().observe(activity, Observer {
       val query = it?.first ?: ""
       val shouldShowResults =
         (autoCompleteSettings.auto && query.length >= 3) ||
@@ -260,10 +261,10 @@ class AutoCompleteHelper(
   }
 
   fun autoComplete(reverse: Boolean = false) {
-    viewModel.lastWord.switchMap { it }.value?.let { originalWord ->
+    helper.editor.lastWord.switchMap { it }.value?.let { originalWord ->
       val previous = autoCompletionState
       if (!originalWord.second.isEmpty()) {
-        val autoCompletedWords = viewModel.rawAutoCompleteData.value?.let { (sessionOptional, id, lastWord) ->
+        val autoCompletedWords = helper.rawAutoCompleteData.value?.let { (sessionOptional, id, lastWord) ->
           fullAutoComplete(sessionOptional, id, lastWord)
         }?.filter {
           it is AutoCompleteItem.AliasItem && autoCompleteSettings.aliases ||
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 e5a2925b9..90052c05e 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
@@ -42,7 +42,9 @@ import de.kuschku.quasseldroid.util.helper.*
 import de.kuschku.quasseldroid.util.irc.format.IrcFormatDeserializer
 import de.kuschku.quasseldroid.util.irc.format.IrcFormatSerializer
 import de.kuschku.quasseldroid.util.service.ServiceBoundFragment
+import de.kuschku.quasseldroid.viewmodel.ChatViewModel
 import de.kuschku.quasseldroid.viewmodel.EditorViewModel
+import de.kuschku.quasseldroid.viewmodel.helper.EditorViewModelHelper
 import javax.inject.Inject
 
 class ChatlineFragment : ServiceBoundFragment() {
@@ -91,15 +93,15 @@ class ChatlineFragment : ServiceBoundFragment() {
   @Inject
   lateinit var autoCompleteAdapter: AutoCompleteAdapter
 
-  @Inject
-  lateinit var editorViewModel: EditorViewModel
-
   lateinit var editorHelper: EditorHelper
 
   lateinit var autoCompleteHelper: AutoCompleteHelper
 
   lateinit var historyBottomSheet: BottomSheetBehavior<View>
 
+  @Inject
+  lateinit var modelHelper: EditorViewModelHelper
+
   val panelSlideListener = object : BottomSheetBehavior.BottomSheetCallback() {
     override fun onSlide(bottomSheet: View, slideOffset: Float) = Unit
 
@@ -113,14 +115,12 @@ class ChatlineFragment : ServiceBoundFragment() {
     val view = inflater.inflate(R.layout.chat_chatline, container, false)
     ButterKnife.bind(this, view)
 
-    editorViewModel.quasselViewModel.onNext(viewModel)
-
     autoCompleteHelper = AutoCompleteHelper(
       requireActivity(),
       autoCompleteSettings,
       messageSettings,
       ircFormatDeserializer,
-      editorViewModel
+      modelHelper
     )
 
     editorHelper = EditorHelper(
@@ -132,7 +132,7 @@ class ChatlineFragment : ServiceBoundFragment() {
       appearanceSettings
     )
 
-    editorViewModel.lastWord.onNext(editorHelper.lastWord)
+    modelHelper.editor.lastWord.onNext(editorHelper.lastWord)
 
     val autoCompleteBottomSheet = BottomSheetBehavior.from(autoCompleteList)
     if (autoCompleteSettings.prefix || autoCompleteSettings.auto) {
@@ -162,7 +162,7 @@ class ChatlineFragment : ServiceBoundFragment() {
       historyBottomSheet.state = BottomSheetBehavior.STATE_HIDDEN
     }
     messageHistory.adapter = messageHistoryAdapter
-    viewModel.recentlySentMessages.toLiveData()
+    modelHelper.chat.recentlySentMessages.toLiveData()
       .observe(this, Observer(messageHistoryAdapter::submitList))
     messageHistoryAdapter.setOnUpdateFinishedListener {
       messageHistory.scrollToPosition(0)
@@ -181,11 +181,11 @@ class ChatlineFragment : ServiceBoundFragment() {
         }
 
         for ((stripped, _) in lines) {
-          viewModel.addRecentlySentMessage(stripped)
+          modelHelper.chat.addRecentlySentMessage(stripped)
         }
-        viewModel.session { sessionOptional ->
+        modelHelper.session { sessionOptional ->
           val session = sessionOptional.orNull()
-          viewModel.buffer { bufferId ->
+          modelHelper.chat.buffer { bufferId ->
             session?.bufferSyncer?.bufferInfo(bufferId)?.also { bufferInfo ->
               val output = mutableListOf<IAliasManager.Command>()
               for ((_, formatted) in lines) {
@@ -228,7 +228,7 @@ class ChatlineFragment : ServiceBoundFragment() {
 
   fun replaceText(text: CharSequence) {
     if (chatline.safeText.isNotEmpty()) {
-      chatline.safeText.lineSequence().forEach(viewModel::addRecentlySentMessage)
+      chatline.safeText.lineSequence().forEach(modelHelper.chat::addRecentlySentMessage)
     }
     editorHelper.replaceText(text)
   }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageListFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageListFragment.kt
index ed05bd91a..3a5a2fedc 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
@@ -72,7 +72,9 @@ import de.kuschku.quasseldroid.util.helper.*
 import de.kuschku.quasseldroid.util.service.ServiceBoundFragment
 import de.kuschku.quasseldroid.util.ui.LinkLongClickMenuHelper
 import de.kuschku.quasseldroid.util.ui.SpanFormatter
+import de.kuschku.quasseldroid.viewmodel.ChatViewModel
 import de.kuschku.quasseldroid.viewmodel.data.Avatar
+import de.kuschku.quasseldroid.viewmodel.helper.ChatViewModelHelper
 import org.threeten.bp.ZoneId
 import org.threeten.bp.ZonedDateTime
 import org.threeten.bp.temporal.ChronoUnit
@@ -110,6 +112,12 @@ class MessageListFragment : ServiceBoundFragment() {
   @Inject
   lateinit var adapter: MessageAdapter
 
+  @Inject
+  lateinit var chatViewModel: ChatViewModel
+
+  @Inject
+  lateinit var modelHelper: ChatViewModelHelper
+
   private lateinit var linearLayoutManager: LinearLayoutManager
 
   private lateinit var backlogRequester: BacklogRequester
@@ -123,9 +131,9 @@ class MessageListFragment : ServiceBoundFragment() {
   private val actionModeCallback = object : ActionMode.Callback {
     override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?) = when (item?.itemId) {
       R.id.action_user_info -> {
-        viewModel.selectedMessages.value.values.firstOrNull()?.let { msg ->
-          viewModel.session.value?.orNull()?.bufferSyncer?.let { bufferSyncer ->
-            viewModel.bufferData.value?.info?.let(BufferInfo::networkId)?.let { networkId ->
+        modelHelper.chat.selectedMessages.value.values.firstOrNull()?.let { msg ->
+          modelHelper.session.value?.orNull()?.bufferSyncer?.let { bufferSyncer ->
+            modelHelper.bufferData.value?.info?.let(BufferInfo::networkId)?.let { networkId ->
               UserInfoActivity.launch(
                 requireContext(),
                 openBuffer = false,
@@ -145,7 +153,7 @@ class MessageListFragment : ServiceBoundFragment() {
       }
       R.id.action_copy      -> {
         val builder = SpannableStringBuilder()
-        viewModel.selectedMessages.value.values.asSequence().sortedBy {
+        modelHelper.chat.selectedMessages.value.values.asSequence().sortedBy {
           it.original.messageId
         }.map {
           if (it.name != null && it.content != null) {
@@ -174,7 +182,7 @@ class MessageListFragment : ServiceBoundFragment() {
       }
       R.id.action_share     -> {
         val builder = SpannableStringBuilder()
-        viewModel.selectedMessages.value.values.asSequence().sortedBy {
+        modelHelper.chat.selectedMessages.value.values.asSequence().sortedBy {
           it.original.messageId
         }.map {
           if (it.name != null && it.content != null) {
@@ -223,7 +231,7 @@ class MessageListFragment : ServiceBoundFragment() {
 
     override fun onDestroyActionMode(mode: ActionMode?) {
       actionMode = null
-      viewModel.selectedMessages.onNext(emptyMap())
+      modelHelper.chat.selectedMessages.onNext(emptyMap())
     }
   }
 
@@ -244,18 +252,18 @@ class MessageListFragment : ServiceBoundFragment() {
     linearLayoutManager = LinearLayoutManager(context)
     linearLayoutManager.reverseLayout = true
 
-    backlogRequester = BacklogRequester(viewModel, database, accountDatabase)
+    backlogRequester = BacklogRequester(modelHelper.session, database, accountDatabase)
 
     adapter.setOnClickListener { msg ->
       if (actionMode != null) {
-        when (viewModel.selectedMessagesToggle(msg.original.messageId, msg)) {
+        when (modelHelper.chat.selectedMessagesToggle(msg.original.messageId, msg)) {
           0    -> actionMode?.finish()
           1    -> actionMode?.menu?.findItem(R.id.action_user_info)?.isVisible = true
           else -> actionMode?.menu?.findItem(R.id.action_user_info)?.isVisible = false
         }
       } else if (msg.hasSpoilers) {
-        val value = viewModel.expandedMessages.value
-        viewModel.expandedMessages.onNext(
+        val value = modelHelper.chat.expandedMessages.value
+        modelHelper.chat.expandedMessages.onNext(
           if (value.contains(msg.original.messageId)) value - msg.original.messageId
           else value + msg.original.messageId
         )
@@ -265,7 +273,7 @@ class MessageListFragment : ServiceBoundFragment() {
       if (actionMode == null) {
         activity?.startActionMode(actionModeCallback)
       }
-      when (viewModel.selectedMessagesToggle(msg.original.messageId, msg)) {
+      when (modelHelper.chat.selectedMessagesToggle(msg.original.messageId, msg)) {
         0    -> actionMode?.finish()
         1    -> actionMode?.menu?.findItem(R.id.action_user_info)?.isVisible = true
         else -> actionMode?.menu?.findItem(R.id.action_user_info)?.isVisible = false
@@ -280,8 +288,8 @@ class MessageListFragment : ServiceBoundFragment() {
         )
       }
     adapter.setOnSenderIconClickListener { msg ->
-      viewModel.session.value?.orNull()?.bufferSyncer?.let { bufferSyncer ->
-        viewModel.bufferData.value?.info?.let(BufferInfo::networkId)?.let { networkId ->
+      modelHelper.session.value?.orNull()?.bufferSyncer?.let { bufferSyncer ->
+        modelHelper.bufferData.value?.info?.let(BufferInfo::networkId)?.let { networkId ->
           UserInfoActivity.launch(
             requireContext(),
             openBuffer = false,
@@ -361,10 +369,10 @@ class MessageListFragment : ServiceBoundFragment() {
       }
     }
 
-    val data = combineLatest(viewModel.buffer,
-                             viewModel.selectedMessages,
-                             viewModel.expandedMessages,
-                             viewModel.markerLine)
+    val data = combineLatest(modelHelper.chat.buffer,
+                             modelHelper.chat.selectedMessages,
+                             modelHelper.chat.expandedMessages,
+                             modelHelper.markerLine)
       .toLiveData().switchMapNotNull { (buffer, selected, expanded, markerLine) ->
         accountDatabase.accounts().listen(accountId).switchMap {
           database.filtered().listen(accountId,
@@ -387,19 +395,19 @@ class MessageListFragment : ServiceBoundFragment() {
         }
       }
 
-    val lastMessageId = viewModel.buffer.toLiveData().switchMapNotNull {
+    val lastMessageId = modelHelper.chat.buffer.toLiveData().switchMapNotNull {
       database.message().lastMsgId(it)
     }
 
-    viewModel.buffer.toLiveData().observe(this, Observer { bufferId ->
+    modelHelper.chat.buffer.toLiveData().observe(this, Observer { bufferId ->
       swipeRefreshLayout.isEnabled = (bufferId != null || bufferId?.isValidId() == true)
     })
 
-    viewModel.sessionManager.mapSwitchMap(SessionManager::state).distinctUntilChanged().toLiveData().observe(
+    modelHelper.sessionManager.mapSwitchMap(SessionManager::state).distinctUntilChanged().toLiveData().observe(
       this, Observer {
       if (it?.orNull() == ConnectionState.CONNECTED) {
         runInBackgroundDelayed(16) {
-          viewModel.buffer { bufferId ->
+          modelHelper.chat.buffer { bufferId ->
             val filtered = database.filtered().get(accountId,
                                                    bufferId,
                                                    accountDatabase.accounts().findById(accountId)?.defaultFiltered
@@ -416,7 +424,7 @@ class MessageListFragment : ServiceBoundFragment() {
       }
     })
 
-    viewModel.session.toLiveData().zip(lastMessageId).observe(
+    modelHelper.session.toLiveData().zip(lastMessageId).observe(
       this, Observer {
       runInBackground {
         val session = it?.first?.orNull()
@@ -496,11 +504,11 @@ class MessageListFragment : ServiceBoundFragment() {
           list?.let(adapter::submitList)
         }
 
-        val buffer = viewModel.buffer.value
+        val buffer = modelHelper.chat.buffer.value
                      ?: BufferId(-1)
         if (buffer != lastBuffer) {
           adapter.clearCache()
-          viewModel.session.value?.orNull()?.bufferSyncer?.let { bufferSyncer ->
+          modelHelper.session.value?.orNull()?.bufferSyncer?.let { bufferSyncer ->
             onBufferChange(lastBuffer, buffer, firstVisibleMessageId, bufferSyncer)
           }
           lastBuffer = buffer
@@ -549,7 +557,7 @@ class MessageListFragment : ServiceBoundFragment() {
     val previous = lastBuffer
     val firstVisibleItemPosition = linearLayoutManager.findFirstVisibleItemPosition()
     val messageId = adapter[firstVisibleItemPosition]?.content?.messageId
-    val bufferSyncer = viewModel.session.value?.orNull()?.bufferSyncer
+    val bufferSyncer = modelHelper.session.value?.orNull()?.bufferSyncer
     if (previous != null && messageId != null) {
       bufferSyncer?.requestSetMarkerLine(previous, messageId)
     }
@@ -559,7 +567,7 @@ class MessageListFragment : ServiceBoundFragment() {
   private fun loadMore(initial: Boolean = false, lastMessageId: MsgId? = null) {
     // This can be called *after* we’re already detached from the activity
     activity?.runOnUiThread {
-      viewModel.buffer { bufferId ->
+      modelHelper.chat.buffer { bufferId ->
         if (bufferId.isValidId() && bufferId != BufferId.MAX_VALUE) {
           if (initial) swipeRefreshLayout.isRefreshing = true
           runInBackground {
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 03b4767ea..81f57b07d 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
@@ -48,6 +48,7 @@ import de.kuschku.quasseldroid.util.irc.format.IrcFormatDeserializer
 import de.kuschku.quasseldroid.util.ui.SpanFormatter
 import de.kuschku.quasseldroid.viewmodel.EditorViewModel
 import de.kuschku.quasseldroid.viewmodel.data.FormattedMessage
+import de.kuschku.quasseldroid.viewmodel.helper.EditorViewModelHelper.Companion.IGNORED_CHARS
 import org.threeten.bp.ZoneId
 import org.threeten.bp.format.DateTimeFormatter
 import org.threeten.bp.format.FormatStyle
@@ -263,7 +264,7 @@ class QuasselMessageRenderer @Inject constructor(
                                                                     message.content.networkId)
         val nickName = HostmaskHelper.nick(message.content.sender)
         val senderColorIndex = SenderColorUtil.senderColor(nickName)
-        val rawInitial = nickName.trimStart(*EditorViewModel.IGNORED_CHARS)
+        val rawInitial = nickName.trimStart(*IGNORED_CHARS)
                            .firstOrNull() ?: nickName.firstOrNull()
         val initial = rawInitial?.toUpperCase().toString()
         val useSelfColor = when (messageSettings.colorizeNicknames) {
@@ -298,7 +299,7 @@ class QuasselMessageRenderer @Inject constructor(
       Message_Type.Action       -> {
         val nickName = HostmaskHelper.nick(message.content.sender)
         val senderColorIndex = SenderColorUtil.senderColor(nickName)
-        val rawInitial = nickName.trimStart(*EditorViewModel.IGNORED_CHARS)
+        val rawInitial = nickName.trimStart(*IGNORED_CHARS)
                            .firstOrNull() ?: nickName.firstOrNull()
         val initial = rawInitial?.toUpperCase().toString()
         val useSelfColor = when (messageSettings.colorizeNicknames) {
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 082863605..997f840ab 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
@@ -54,8 +54,9 @@ import de.kuschku.quasseldroid.util.helper.styledAttributes
 import de.kuschku.quasseldroid.util.helper.toLiveData
 import de.kuschku.quasseldroid.util.irc.format.IrcFormatDeserializer
 import de.kuschku.quasseldroid.util.service.ServiceBoundFragment
-import de.kuschku.quasseldroid.viewmodel.EditorViewModel.Companion.IGNORED_CHARS
 import de.kuschku.quasseldroid.viewmodel.data.Avatar
+import de.kuschku.quasseldroid.viewmodel.helper.ChatViewModelHelper
+import de.kuschku.quasseldroid.viewmodel.helper.EditorViewModelHelper.Companion.IGNORED_CHARS
 import javax.inject.Inject
 
 class NickListFragment : ServiceBoundFragment() {
@@ -71,6 +72,9 @@ class NickListFragment : ServiceBoundFragment() {
   @Inject
   lateinit var ircFormatDeserializer: IrcFormatDeserializer
 
+  @Inject
+  lateinit var modelHelper: ChatViewModelHelper
+
   override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                             savedInstanceState: Bundle?): View? {
     val view = inflater.inflate(R.layout.chat_nicklist, container, false)
@@ -101,7 +105,7 @@ class NickListFragment : ServiceBoundFragment() {
     val colorContext = ColorContext(requireContext(), messageSettings)
 
     val avatarSize = resources.getDimensionPixelSize(R.dimen.avatar_size)
-    viewModel.nickDataThrottled.toLiveData().observe(this, Observer {
+    modelHelper.nickDataThrottled.toLiveData().observe(this, Observer {
       runInBackground {
         it?.asSequence()?.map {
           val nickName = it.nick
@@ -188,8 +192,8 @@ class NickListFragment : ServiceBoundFragment() {
   }
 
   private val clickListener: ((String) -> Unit)? = { nick ->
-    viewModel.session.value?.orNull()?.bufferSyncer?.let { bufferSyncer ->
-      viewModel.bufferData.value?.info?.let(BufferInfo::networkId)?.let { networkId ->
+    modelHelper.session.value?.orNull()?.bufferSyncer?.let { bufferSyncer ->
+      modelHelper.bufferData.value?.info?.let(BufferInfo::networkId)?.let { networkId ->
         UserInfoActivity.launch(
           requireContext(),
           openBuffer = false,
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/info/topic/TopicActivity.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/topic/TopicActivity.kt
similarity index 92%
rename from app/src/main/java/de/kuschku/quasseldroid/ui/info/topic/TopicActivity.kt
rename to app/src/main/java/de/kuschku/quasseldroid/ui/chat/topic/TopicActivity.kt
index 468a6de9d..cc1a45d3f 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/info/topic/TopicActivity.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/topic/TopicActivity.kt
@@ -17,11 +17,10 @@
  * with this program. If not, see <http://www.gnu.org/licenses/>.
  */
 
-package de.kuschku.quasseldroid.ui.info.topic
+package de.kuschku.quasseldroid.ui.chat.topic
 
 import android.content.Context
 import android.content.Intent
-import de.kuschku.quasseldroid.ui.chat.topic.TopicFragment
 import de.kuschku.quasseldroid.util.ui.settings.ServiceBoundSettingsActivity
 
 class TopicActivity : ServiceBoundSettingsActivity(TopicFragment()) {
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 e6b056c1d..6180fea5a 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
@@ -43,6 +43,7 @@ import de.kuschku.quasseldroid.util.irc.format.IrcFormatSerializer
 import de.kuschku.quasseldroid.util.ui.settings.fragment.Savable
 import de.kuschku.quasseldroid.util.ui.settings.fragment.ServiceBoundSettingsFragment
 import de.kuschku.quasseldroid.viewmodel.EditorViewModel
+import de.kuschku.quasseldroid.viewmodel.helper.EditorViewModelHelper
 import javax.inject.Inject
 
 class TopicFragment : ServiceBoundSettingsFragment(), Savable {
@@ -74,7 +75,7 @@ class TopicFragment : ServiceBoundSettingsFragment(), Savable {
   lateinit var autoCompleteAdapter: AutoCompleteAdapter
 
   @Inject
-  lateinit var editorViewModel: EditorViewModel
+  lateinit var modelHelper: EditorViewModelHelper
 
   private lateinit var editorHelper: EditorHelper
 
@@ -83,14 +84,12 @@ class TopicFragment : ServiceBoundSettingsFragment(), Savable {
     val view = inflater.inflate(R.layout.info_topic, container, false)
     ButterKnife.bind(this, view)
 
-    editorViewModel.quasselViewModel.onNext(viewModel)
-
     val autoCompleteHelper = AutoCompleteHelper(
       requireActivity(),
       autoCompleteSettings,
       messageSettings,
       formatDeserializer,
-      editorViewModel
+      modelHelper
     )
 
     editorHelper = EditorHelper(
@@ -102,7 +101,7 @@ class TopicFragment : ServiceBoundSettingsFragment(), Savable {
       appearanceSettings
     )
 
-    editorViewModel.lastWord.onNext(editorHelper.lastWord)
+    modelHelper.editor.lastWord.onNext(editorHelper.lastWord)
 
     if (autoCompleteSettings.prefix || autoCompleteSettings.auto) {
       val autoCompleteBottomSheet = BottomSheetBehavior.from(autoCompleteList)
@@ -119,8 +118,8 @@ class TopicFragment : ServiceBoundSettingsFragment(), Savable {
     }
 
     val bufferId = BufferId(arguments?.getInt("buffer", -1) ?: -1)
-    viewModel.buffer.onNext(bufferId)
-    viewModel.bufferData.filter {
+    modelHelper.chat.buffer.onNext(bufferId)
+    modelHelper.bufferData.filter {
       it.info != null
     }.firstElement().toLiveData().observe(this, Observer {
       chatline.setText(formatDeserializer.formatString(it?.description, true))
@@ -130,9 +129,9 @@ class TopicFragment : ServiceBoundSettingsFragment(), Savable {
   }
 
   override fun onSave(): Boolean {
-    viewModel.session { sessionOptional ->
+    modelHelper.session { sessionOptional ->
       val session = sessionOptional.orNull()
-      viewModel.buffer { bufferId ->
+      modelHelper.chat.buffer { bufferId ->
         session?.bufferSyncer?.bufferInfo(bufferId)?.also { bufferInfo ->
           val topic = formatSerializer.toEscapeCodes(chatline.safeText)
           session.rpcHandler.sendInput(bufferInfo, "/topic $topic")
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 0ec82a4a4..c4d418209 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
@@ -23,7 +23,6 @@ import androidx.fragment.app.FragmentActivity
 import dagger.Binds
 import dagger.Module
 import dagger.android.ContributesAndroidInjector
-import de.kuschku.quasseldroid.ui.info.topic.TopicActivity
 
 @Module
 abstract class TopicFragmentProvider {
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 d1c2c46ab..b426457df 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
@@ -57,7 +57,9 @@ import de.kuschku.quasseldroid.util.missingfeatures.MissingFeaturesDialog
 import de.kuschku.quasseldroid.util.missingfeatures.RequiredFeatures
 import de.kuschku.quasseldroid.util.service.ServiceBoundFragment
 import de.kuschku.quasseldroid.util.ui.view.BannerView
+import de.kuschku.quasseldroid.viewmodel.helper.EditorViewModelHelper
 import io.reactivex.Observable
+import javax.inject.Inject
 
 class CoreSettingsFragment : ServiceBoundFragment() {
   @BindView(R.id.feature_context_missing)
@@ -99,6 +101,9 @@ class CoreSettingsFragment : ServiceBoundFragment() {
   @BindView(R.id.networkconfig)
   lateinit var networkconfig: View
 
+  @Inject
+  lateinit var modelHelper: EditorViewModelHelper
+
   override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                             savedInstanceState: Bundle?): View? {
     val view = inflater.inflate(R.layout.settings_list, container, false)
@@ -131,7 +136,7 @@ class CoreSettingsFragment : ServiceBoundFragment() {
     networks.addItemDecoration(itemDecoration)
     ViewCompat.setNestedScrollingEnabled(networks, false)
 
-    viewModel.networks.switchMap {
+    modelHelper.networks.switchMap {
       if (it.isEmpty()) {
         Observable.just(emptyList())
       } else {
@@ -150,7 +155,7 @@ class CoreSettingsFragment : ServiceBoundFragment() {
     identities.addItemDecoration(itemDecoration)
     ViewCompat.setNestedScrollingEnabled(identities, false)
 
-    viewModel.identities.switchMap {
+    modelHelper.identities.switchMap {
       if (it.isEmpty()) {
         Observable.just(emptyList())
       } else {
@@ -169,7 +174,7 @@ class CoreSettingsFragment : ServiceBoundFragment() {
     chatlists.addItemDecoration(itemDecoration)
     ViewCompat.setNestedScrollingEnabled(chatlists, false)
 
-    viewModel.bufferViewConfigMap.switchMap {
+    modelHelper.bufferViewConfigMap.switchMap {
       combineLatest(it.values.map(BufferViewConfig::liveUpdates)).map {
         it.map {
           SettingsItem(it.bufferViewId(), it.bufferViewName())
@@ -180,7 +185,7 @@ class CoreSettingsFragment : ServiceBoundFragment() {
     })
 
     var missingFeatureList: List<MissingFeature> = emptyList()
-    viewModel.negotiatedFeatures.toLiveData().observe(this, Observer { (connected, features) ->
+    modelHelper.negotiatedFeatures.toLiveData().observe(this, Observer { (connected, features) ->
       missingFeatureList = RequiredFeatures.features.filter {
         it.feature !in features.enabledFeatures
       }
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 1dfa2e2f9..bb55abfda 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
@@ -44,6 +44,7 @@ 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
 import de.kuschku.quasseldroid.viewmodel.EditorViewModel
+import de.kuschku.quasseldroid.viewmodel.helper.EditorViewModelHelper
 import javax.inject.Inject
 
 class AliasItemFragment : ServiceBoundSettingsFragment(), Savable, Changeable {
@@ -78,7 +79,7 @@ class AliasItemFragment : ServiceBoundSettingsFragment(), Savable, Changeable {
   lateinit var autoCompleteAdapter: AutoCompleteAdapter
 
   @Inject
-  lateinit var editorViewModel: EditorViewModel
+  lateinit var modelHelper: EditorViewModelHelper
 
   private lateinit var editorHelper: EditorHelper
 
@@ -94,14 +95,12 @@ class AliasItemFragment : ServiceBoundSettingsFragment(), Savable, Changeable {
       rule = it
     }
 
-    editorViewModel.quasselViewModel.onNext(viewModel)
-
     val autoCompleteHelper = AutoCompleteHelper(
       requireActivity(),
       autoCompleteSettings,
       messageSettings,
       formatDeserializer,
-      editorViewModel
+      modelHelper
     )
 
     editorHelper = EditorHelper(
@@ -113,7 +112,7 @@ class AliasItemFragment : ServiceBoundSettingsFragment(), Savable, Changeable {
       appearanceSettings
     )
 
-    editorViewModel.lastWord.onNext(editorHelper.lastWord)
+    modelHelper.editor.lastWord.onNext(editorHelper.lastWord)
 
     if (autoCompleteSettings.prefix || autoCompleteSettings.auto) {
       val autoCompleteBottomSheet = BottomSheetBehavior.from(autoCompleteList)
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 433e9f017..1ea45ff8d 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
@@ -39,6 +39,8 @@ import de.kuschku.quasseldroid.util.helper.toLiveData
 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
+import de.kuschku.quasseldroid.viewmodel.helper.EditorViewModelHelper
+import de.kuschku.quasseldroid.viewmodel.helper.QuasselViewModelHelper
 import javax.inject.Inject
 
 class AliasListFragment : ServiceBoundSettingsFragment(), Savable, Changeable {
@@ -51,6 +53,9 @@ class AliasListFragment : ServiceBoundSettingsFragment(), Savable, Changeable {
   @Inject
   lateinit var adapter: AliasListAdapter
 
+  @Inject
+  lateinit var modelHelper: QuasselViewModelHelper
+
   private var aliasManager: Pair<AliasManager, AliasManager>? = null
 
   private lateinit var helper: ItemTouchHelper
@@ -76,7 +81,7 @@ class AliasListFragment : ServiceBoundSettingsFragment(), Savable, Changeable {
       startActivityForResult(AliasItemActivity.intent(requireContext()), REQUEST_CREATE_ITEM)
     }
 
-    viewModel.aliasManager
+    modelHelper.aliasManager
       .filter(Optional<AliasManager>::isPresent)
       .map(Optional<AliasManager>::get)
       .toLiveData().observe(this, Observer {
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 d624d7078..466153b14 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
@@ -48,6 +48,8 @@ import de.kuschku.quasseldroid.util.helper.toLiveData
 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
+import de.kuschku.quasseldroid.viewmodel.helper.EditorViewModelHelper
+import javax.inject.Inject
 
 abstract class ChatListBaseFragment(private val initDefault: Boolean) :
   ServiceBoundSettingsFragment(), Savable, Changeable {
@@ -84,6 +86,9 @@ abstract class ChatListBaseFragment(private val initDefault: Boolean) :
   @BindView(R.id.hide_inactive_networks)
   lateinit var hideInactiveNetworks: SwitchCompat
 
+  @Inject
+  lateinit var modelHelper: EditorViewModelHelper
+
   protected var chatlist: Pair<BufferViewConfig?, BufferViewConfig>? = null
 
   override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
@@ -116,7 +121,7 @@ abstract class ChatListBaseFragment(private val initDefault: Boolean) :
     val networkAdapter = NetworkAdapter(R.string.settings_chatlist_network_all)
     networkId.adapter = networkAdapter
 
-    viewModel.networks.switchMap {
+    modelHelper.networks.switchMap {
       combineLatest(it.values.map(Network::liveNetworkInfo)).map {
         it.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, INetwork.NetworkInfo::networkName))
       }
@@ -133,7 +138,7 @@ abstract class ChatListBaseFragment(private val initDefault: Boolean) :
     })
 
     if (initDefault) {
-      viewModel.session
+      modelHelper.session
         .filter(Optional<ISession>::isPresent)
         .map(Optional<ISession>::get)
         .firstElement()
@@ -145,7 +150,7 @@ abstract class ChatListBaseFragment(private val initDefault: Boolean) :
           }
         })
     } else {
-      viewModel.bufferViewConfigMap.map { Optional.ofNullable(it[chatlistId]) }
+      modelHelper.bufferViewConfigMap.map { Optional.ofNullable(it[chatlistId]) }
         .filter(Optional<BufferViewConfig>::isPresent)
         .map(Optional<BufferViewConfig>::get)
         .firstElement()
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/chatlist/ChatListCreateFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/chatlist/ChatListCreateFragment.kt
index 42204dd07..57a87610c 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/chatlist/ChatListCreateFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/chatlist/ChatListCreateFragment.kt
@@ -24,7 +24,7 @@ import de.kuschku.libquassel.util.helpers.value
 class ChatListCreateFragment : ChatListBaseFragment(true) {
   override fun onSave() = chatlist?.let { (_, data) ->
     applyChanges(data, null)
-    viewModel.bufferViewManager.value?.orNull()?.requestCreateBufferView(data.toVariantMap())
+    modelHelper.bufferViewManager.value?.orNull()?.requestCreateBufferView(data.toVariantMap())
     true
   } ?: false
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/chatlist/ChatListEditFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/chatlist/ChatListEditFragment.kt
index 2c7cc4067..b94bc5eac 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/chatlist/ChatListEditFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/chatlist/ChatListEditFragment.kt
@@ -32,7 +32,7 @@ class ChatListEditFragment : ChatListBaseFragment(false), Deletable {
   override fun onDelete() {
     chatlist?.let { (it, _) ->
       it?.let {
-        viewModel.bufferViewManager.value?.orNull()?.requestDeleteBufferView(it.bufferViewId())
+        modelHelper.bufferViewManager.value?.orNull()?.requestDeleteBufferView(it.bufferViewId())
       }
     }
   }
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 eb777d056..a07992970 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
@@ -46,6 +46,8 @@ 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
 import de.kuschku.quasseldroid.util.ui.view.WarningBarView
+import de.kuschku.quasseldroid.viewmodel.helper.EditorViewModelHelper
+import javax.inject.Inject
 
 class HighlightListFragment : ServiceBoundSettingsFragment(), Savable, Changeable {
   @BindView(R.id.feature_context_coresidehighlights)
@@ -69,6 +71,9 @@ class HighlightListFragment : ServiceBoundSettingsFragment(), Savable, Changeabl
   @BindView(R.id.new_highlight_ignore_rule)
   lateinit var newHighlightIgnoreRule: Button
 
+  @Inject
+  lateinit var modelHelper: EditorViewModelHelper
+
   private var ruleManager: Pair<HighlightRuleManager, HighlightRuleManager>? = null
 
   private lateinit var rulesHelper: ItemTouchHelper
@@ -128,7 +133,7 @@ class HighlightListFragment : ServiceBoundSettingsFragment(), Savable, Changeabl
     ))
     highlightNickType.adapter = highlightNickTypeAdapter
 
-    viewModel.highlightRuleManager
+    modelHelper.highlightRuleManager
       .filter(Optional<HighlightRuleManager>::isPresent)
       .map(Optional<HighlightRuleManager>::get)
       .toLiveData().observe(this, Observer {
@@ -146,7 +151,7 @@ class HighlightListFragment : ServiceBoundSettingsFragment(), Savable, Changeabl
         }
       })
 
-    viewModel.negotiatedFeatures.toLiveData().observe(this, Observer { (connected, features) ->
+    modelHelper.negotiatedFeatures.toLiveData().observe(this, Observer { (connected, features) ->
       featureContextCoreSideHighlights.setMode(
         if (!connected || features.hasFeature(ExtendedFeature.CoreSideHighlights)) WarningBarView.MODE_NONE
         else WarningBarView.MODE_ICON
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 1b2154fe2..b0bd1f629 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
@@ -45,6 +45,8 @@ import de.kuschku.quasseldroid.util.helper.toLiveData
 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
+import de.kuschku.quasseldroid.viewmodel.helper.EditorViewModelHelper
+import javax.inject.Inject
 
 abstract class IdentityBaseFragment(private val initDefault: Boolean) :
   ServiceBoundSettingsFragment(), Savable, Changeable {
@@ -85,6 +87,9 @@ abstract class IdentityBaseFragment(private val initDefault: Boolean) :
   @BindView(R.id.detach_away_reason)
   lateinit var detachAwayReason: EditText
 
+  @Inject
+  lateinit var modelHelper: EditorViewModelHelper
+
   protected var identity: Pair<Identity?, Identity>? = null
 
   private lateinit var adapter: IdentityNicksAdapter
@@ -122,7 +127,7 @@ abstract class IdentityBaseFragment(private val initDefault: Boolean) :
     }
 
     if (initDefault) {
-      viewModel.session
+      modelHelper.session
         .filter(Optional<ISession>::isPresent)
         .map(Optional<ISession>::get)
         .firstElement()
@@ -132,7 +137,7 @@ abstract class IdentityBaseFragment(private val initDefault: Boolean) :
           }
         })
     } else {
-      viewModel.identities.map { Optional.ofNullable(it[identityId]) }
+      modelHelper.identities.map { Optional.ofNullable(it[identityId]) }
         .filter(Optional<Identity>::isPresent)
         .map(Optional<Identity>::get)
         .firstElement()
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/identity/IdentityCreateFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/identity/IdentityCreateFragment.kt
index d3797640e..55679aae3 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/identity/IdentityCreateFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/identity/IdentityCreateFragment.kt
@@ -22,7 +22,7 @@ package de.kuschku.quasseldroid.ui.coresettings.identity
 import de.kuschku.libquassel.util.helpers.value
 
 class IdentityCreateFragment : IdentityBaseFragment(true) {
-  override fun onSave() = viewModel.session.value?.orNull()?.let { session ->
+  override fun onSave() = modelHelper.session.value?.orNull()?.let { session ->
     identity?.let { (_, data) ->
       applyChanges(data)
       session.rpcHandler.createIdentity(data, mapOf())
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/identity/IdentityEditFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/identity/IdentityEditFragment.kt
index 88d913fc8..4523eb3b4 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/identity/IdentityEditFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/identity/IdentityEditFragment.kt
@@ -32,7 +32,7 @@ class IdentityEditFragment : IdentityBaseFragment(false), Deletable {
   override fun onDelete() {
     identity?.let { (it, _) ->
       it?.let {
-        viewModel.session.value?.orNull()?.rpcHandler?.removeIdentity(it.id())
+        modelHelper.session.value?.orNull()?.rpcHandler?.removeIdentity(it.id())
       }
     }
   }
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 5be49568d..8799beb7e 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
@@ -42,6 +42,8 @@ import de.kuschku.quasseldroid.util.helper.toLiveData
 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
+import de.kuschku.quasseldroid.viewmodel.helper.EditorViewModelHelper
+import javax.inject.Inject
 
 class IgnoreListFragment : ServiceBoundSettingsFragment(), Savable,
                            Changeable {
@@ -51,6 +53,9 @@ class IgnoreListFragment : ServiceBoundSettingsFragment(), Savable,
   @BindView(R.id.add)
   lateinit var add: FloatingActionButton
 
+  @Inject
+  lateinit var modelHelper: EditorViewModelHelper
+
   private var ignoreListManager: Pair<IgnoreListManager, IgnoreListManager>? = null
 
   private lateinit var helper: ItemTouchHelper
@@ -82,7 +87,7 @@ class IgnoreListFragment : ServiceBoundSettingsFragment(), Savable,
                              REQUEST_CREATE_RULE)
     }
 
-    viewModel.ignoreListManager
+    modelHelper.ignoreListManager
       .filter(Optional<IgnoreListManager>::isPresent)
       .map(Optional<IgnoreListManager>::get)
       .toLiveData().observe(this, Observer {
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 87b20b9d0..3bbf7e6a6 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
@@ -56,6 +56,8 @@ 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
 import de.kuschku.quasseldroid.util.ui.view.InlineSnackBar
+import de.kuschku.quasseldroid.viewmodel.helper.EditorViewModelHelper
+import javax.inject.Inject
 import kotlin.math.roundToInt
 
 abstract class NetworkBaseFragment(private val initDefault: Boolean) :
@@ -135,6 +137,9 @@ abstract class NetworkBaseFragment(private val initDefault: Boolean) :
   @BindView(R.id.customratelimits_delay)
   lateinit var customratelimitsDelay: EditText
 
+  @Inject
+  lateinit var modelHelper: EditorViewModelHelper
+
   protected var network: Pair<Network?, Network>? = null
 
   private lateinit var adapter: NetworkServerAdapter
@@ -166,7 +171,7 @@ abstract class NetworkBaseFragment(private val initDefault: Boolean) :
     val identityAdapter = IdentityAdapter()
     identity.adapter = identityAdapter
 
-    viewModel.identities.switchMap {
+    modelHelper.identities.switchMap {
       combineLatest(it.values.map(Identity::liveUpdates)).map {
         it.sortedBy(Identity::identityName)
       }
@@ -183,7 +188,7 @@ abstract class NetworkBaseFragment(private val initDefault: Boolean) :
     })
 
     if (initDefault) {
-      viewModel.session
+      modelHelper.session
         .filter(Optional<ISession>::isPresent)
         .map(Optional<ISession>::get)
         .firstElement()
@@ -193,7 +198,7 @@ abstract class NetworkBaseFragment(private val initDefault: Boolean) :
           }
         })
     } else {
-      viewModel.networks.map { Optional.ofNullable(it[networkId]) }
+      modelHelper.networks.map { Optional.ofNullable(it[networkId]) }
         .filter(Optional<Network>::isPresent)
         .map(Optional<Network>::get)
         .firstElement()
@@ -203,7 +208,7 @@ abstract class NetworkBaseFragment(private val initDefault: Boolean) :
             update(it, identityAdapter)
           }
         })
-      viewModel.networks.map { Optional.ofNullable(it[networkId]) }
+      modelHelper.networks.map { Optional.ofNullable(it[networkId]) }
         .filter(Optional<Network>::isPresent)
         .map(Optional<Network>::get)
         .switchMap(Network::liveCaps)
@@ -215,7 +220,7 @@ abstract class NetworkBaseFragment(private val initDefault: Boolean) :
 
 
     autoidentifyWarning.setOnClickListener {
-      val identity = viewModel.identities.value?.get(IdentityId(identity.selectedItemId.toInt()))
+      val identity = modelHelper.identities.value?.get(IdentityId(identity.selectedItemId.toInt()))
       if (identity != null) {
         saslEnabled.isChecked = true
         saslAccount.setText(identity.nicks().firstOrNull())
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/network/NetworkCreateFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/network/NetworkCreateFragment.kt
index 7a5cde498..30a627384 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/network/NetworkCreateFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/network/NetworkCreateFragment.kt
@@ -23,10 +23,10 @@ import de.kuschku.libquassel.session.Backend
 import de.kuschku.libquassel.util.helpers.value
 
 class NetworkCreateFragment : NetworkBaseFragment(true) {
-  override fun onSave() = viewModel.session.value?.orNull()?.let { session ->
+  override fun onSave() = modelHelper.session.value?.orNull()?.let { session ->
     network?.let { (_, data) ->
       applyChanges(data)
-      viewModel.backend.value?.ifPresent(Backend::requestConnectNewNetwork)
+      modelHelper.backend.value?.ifPresent(Backend::requestConnectNewNetwork)
       session.rpcHandler.createNetwork(data.networkInfo(), emptyList())
       true
     }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/network/NetworkEditFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/network/NetworkEditFragment.kt
index 6924fe05c..1c76a2a7d 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/network/NetworkEditFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/network/NetworkEditFragment.kt
@@ -32,7 +32,7 @@ class NetworkEditFragment : NetworkBaseFragment(false), Deletable {
   override fun onDelete() {
     network?.let { (it, _) ->
       it?.let {
-        viewModel.session.value?.orNull()?.rpcHandler?.removeNetwork(it.networkId())
+        modelHelper.session.value?.orNull()?.rpcHandler?.removeNetwork(it.networkId())
       }
     }
   }
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 7b1d1d5db..f1aa0e864 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
@@ -36,6 +36,8 @@ import de.kuschku.quasseldroid.util.helper.toLiveData
 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
+import de.kuschku.quasseldroid.viewmodel.helper.EditorViewModelHelper
+import javax.inject.Inject
 
 class NetworkConfigFragment : ServiceBoundSettingsFragment(), Savable,
                               Changeable {
@@ -69,6 +71,9 @@ class NetworkConfigFragment : ServiceBoundSettingsFragment(), Savable,
   @BindView(R.id.standard_ctcp)
   lateinit var standardCtcp: SwitchCompat
 
+  @Inject
+  lateinit var modelHelper: EditorViewModelHelper
+
   private var networkConfig: Pair<NetworkConfig, NetworkConfig>? = null
 
   override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
@@ -76,7 +81,7 @@ class NetworkConfigFragment : ServiceBoundSettingsFragment(), Savable,
     val view = inflater.inflate(R.layout.settings_networkconfig, container, false)
     ButterKnife.bind(this, view)
 
-    viewModel.networkConfig
+    modelHelper.networkConfig
       .filter(Optional<NetworkConfig>::isPresent)
       .map(Optional<NetworkConfig>::get)
       .firstElement()
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 63c62f011..c77211353 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
@@ -43,6 +43,7 @@ import de.kuschku.quasseldroid.persistence.models.Account
 import de.kuschku.quasseldroid.util.TextValidator
 import de.kuschku.quasseldroid.util.helper.toLiveData
 import de.kuschku.quasseldroid.util.service.ServiceBoundFragment
+import de.kuschku.quasseldroid.viewmodel.helper.EditorViewModelHelper
 import me.zhanghai.android.materialprogressbar.MaterialProgressBar
 import javax.inject.Inject
 
@@ -77,6 +78,9 @@ class PasswordChangeFragment : ServiceBoundFragment() {
   @Inject
   lateinit var accountDatabase: AccountDatabase
 
+  @Inject
+  lateinit var modelHelper: EditorViewModelHelper
+
   private var waiting: Account? = null
 
   override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
@@ -88,7 +92,7 @@ class PasswordChangeFragment : ServiceBoundFragment() {
 
     user.setText(account?.user)
 
-    viewModel.session
+    modelHelper.session
       .mapMapNullable(ISession::rpcHandler)
       .mapSwitchMap(RpcHandler::passwordChanged)
       .filter(Optional<Boolean>::isPresent)
@@ -140,10 +144,12 @@ class PasswordChangeFragment : ServiceBoundFragment() {
 
       waiting = account?.copy(pass = pass)
 
-      viewModel.session.value?.orNull()?.rpcHandler?.changePassword(0L,
-                                                                    user.text.toString(),
-                                                                    oldPassword.text.toString(),
-                                                                    pass)
+      modelHelper.session.value?.orNull()?.rpcHandler?.changePassword(
+        0L,
+        user.text.toString(),
+        oldPassword.text.toString(),
+        pass
+      )
     }
 
     return view
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 02179e48b..00cc2ec83 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
@@ -37,10 +37,12 @@ import de.kuschku.quasseldroid.util.helper.sha256Fingerprint
 import de.kuschku.quasseldroid.util.helper.toLiveData
 import de.kuschku.quasseldroid.util.helper.visibleIf
 import de.kuschku.quasseldroid.util.ui.settings.fragment.ServiceBoundSettingsFragment
+import de.kuschku.quasseldroid.viewmodel.helper.EditorViewModelHelper
 import org.threeten.bp.Instant
 import org.threeten.bp.ZoneId
 import org.threeten.bp.format.DateTimeFormatter
 import org.threeten.bp.format.FormatStyle
+import javax.inject.Inject
 
 class CertificateInfoFragment : ServiceBoundSettingsFragment() {
 
@@ -95,6 +97,8 @@ class CertificateInfoFragment : ServiceBoundSettingsFragment() {
   @BindView(R.id.fingerprint_sha1)
   lateinit var fingerprintSha1: TextView
 
+  @Inject
+  lateinit var modelHelper: EditorViewModelHelper
 
   override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                             savedInstanceState: Bundle?): View? {
@@ -103,7 +107,7 @@ class CertificateInfoFragment : ServiceBoundSettingsFragment() {
 
     val dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
 
-    viewModel.peerCertificateChain.toLiveData().observe(this, Observer {
+    modelHelper.peerCertificateChain.toLiveData().observe(this, Observer {
       val leafCertificate = it.firstOrNull()
       if (leafCertificate != null) {
         content.visibility = View.VISIBLE
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 9126ee12c..49b933ff3 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
@@ -37,13 +37,14 @@ import de.kuschku.libquassel.quassel.syncables.IrcChannel
 import de.kuschku.libquassel.util.helpers.value
 import de.kuschku.quasseldroid.R
 import de.kuschku.quasseldroid.settings.MessageSettings
-import de.kuschku.quasseldroid.ui.info.topic.TopicActivity
+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.irc.format.ContentFormatter
 import de.kuschku.quasseldroid.util.service.ServiceBoundFragment
 import de.kuschku.quasseldroid.util.ui.BetterLinkMovementMethod
 import de.kuschku.quasseldroid.util.ui.LinkLongClickMenuHelper
+import de.kuschku.quasseldroid.viewmodel.helper.EditorViewModelHelper
 import javax.inject.Inject
 
 class ChannelInfoFragment : ServiceBoundFragment() {
@@ -75,6 +76,9 @@ class ChannelInfoFragment : ServiceBoundFragment() {
   @Inject
   lateinit var messageSettings: MessageSettings
 
+  @Inject
+  lateinit var modelHelper: EditorViewModelHelper
+
   override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                             savedInstanceState: Bundle?): View? {
     val view = inflater.inflate(R.layout.info_channel, container, false)
@@ -86,7 +90,7 @@ class ChannelInfoFragment : ServiceBoundFragment() {
 
     var currentBufferInfo: BufferInfo?
 
-    combineLatest(viewModel.session, viewModel.networks).map { (sessionOptional, networks) ->
+    combineLatest(modelHelper.session, modelHelper.networks).map { (sessionOptional, networks) ->
       if (openBuffer == true) {
         val session = sessionOptional?.orNull()
         val bufferSyncer = session?.bufferSyncer
@@ -123,7 +127,7 @@ class ChannelInfoFragment : ServiceBoundFragment() {
       }
 
       actionPart.setOnClickListener {
-        viewModel.session.value?.orNull()?.let { session ->
+        modelHelper.session.value?.orNull()?.let { session ->
           session.bufferSyncer.find(
             networkId = channel.network().networkId(),
             type = Buffer_Type.of(Buffer_Type.StatusBuffer)
@@ -135,7 +139,7 @@ class ChannelInfoFragment : ServiceBoundFragment() {
       }
 
       actionWho.setOnClickListener {
-        viewModel.session.value?.orNull()?.let { session ->
+        modelHelper.session.value?.orNull()?.let { session ->
           session.bufferSyncer.find(
             networkId = channel.network().networkId(),
             type = Buffer_Type.of(Buffer_Type.StatusBuffer)
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 7b37af701..e5fbcc0c3 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
@@ -43,6 +43,7 @@ import de.kuschku.quasseldroid.util.helper.toLiveData
 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
+import de.kuschku.quasseldroid.viewmodel.helper.EditorViewModelHelper
 import io.reactivex.subjects.BehaviorSubject
 import javax.inject.Inject
 
@@ -65,6 +66,9 @@ class ChannelListFragment : ServiceBoundSettingsFragment() {
   @Inject
   lateinit var adapter: ChannelListAdapter
 
+  @Inject
+  lateinit var modelHelper: EditorViewModelHelper
+
   private var query: Query? = null
   private var state: State = State()
 
@@ -146,7 +150,7 @@ class ChannelListFragment : ServiceBoundSettingsFragment() {
       })
     })
 
-    viewModel.ircListHelper
+    modelHelper.ircListHelper
       .mapSwitchMap(IrcListHelper::observable)
       .filter(Optional<IrcListHelper.Event>::isPresent)
       .map(Optional<IrcListHelper.Event>::get)
@@ -169,7 +173,7 @@ class ChannelListFragment : ServiceBoundSettingsFragment() {
       })
 
     searchButton.setOnClickListener {
-      viewModel.ircListHelper.value?.orNull()?.let { ircListHelper ->
+      modelHelper.ircListHelper.value?.orNull()?.let { ircListHelper ->
         val query = Query(
           networkId,
           listOf(searchInput.text.toString())
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 b9d1d6c24..94f3185b2 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
@@ -45,10 +45,12 @@ import de.kuschku.quasseldroid.util.missingfeatures.RequiredFeatures
 import de.kuschku.quasseldroid.util.service.ServiceBoundFragment
 import de.kuschku.quasseldroid.util.ui.BetterLinkMovementMethod
 import de.kuschku.quasseldroid.util.ui.LinkLongClickMenuHelper
+import de.kuschku.quasseldroid.viewmodel.helper.EditorViewModelHelper
 import org.threeten.bp.Instant
 import org.threeten.bp.ZoneId
 import org.threeten.bp.format.DateTimeFormatter
 import org.threeten.bp.format.FormatStyle
+import javax.inject.Inject
 
 class CoreInfoFragment : ServiceBoundFragment() {
 
@@ -88,6 +90,9 @@ class CoreInfoFragment : ServiceBoundFragment() {
   @BindView(R.id.clients)
   lateinit var clients: RecyclerView
 
+  @Inject
+  lateinit var modelHelper: EditorViewModelHelper
+
   private val dateTimeFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
   private val dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
 
@@ -105,18 +110,21 @@ class CoreInfoFragment : ServiceBoundFragment() {
     ButterKnife.bind(this, view)
 
     var missingFeatureList: List<MissingFeature> = emptyList()
-    viewModel.coreInfo.toLiveData().observe(this, Observer {
-      it?.orNull().let { data ->
-        version.text = data?.quasselVersion?.let(Html::fromHtml)
+    combineLatest(modelHelper.coreInfo, modelHelper.coreFeatures).toLiveData()
+      .observe(this, Observer {
+        val data = it?.first?.orNull()
+        val connected = it?.second?.first
+                        ?: false
+        val features = it?.second?.second
+                       ?: QuasselFeatures.empty()
+
+        version.text = data?.quasselVersion.let(Html::fromHtml)
         val versionTime = data?.quasselBuildDate?.toLongOrNull()
         val formattedVersionTime = if (versionTime != null)
           dateFormatter.format(Instant.ofEpochSecond(versionTime).atZone(ZoneId.systemDefault()))
         else
           data?.quasselBuildDate?.let(Html::fromHtml)
         versionDate.text = formattedVersionTime
-
-        val (connected, features) = viewModel.coreFeatures.value ?: Pair(false,
-                                                                         QuasselFeatures.empty())
         missingFeatureList = RequiredFeatures.features.filter {
           it.feature !in features.enabledFeatures
         }
@@ -126,7 +134,6 @@ class CoreInfoFragment : ServiceBoundFragment() {
         uptime.text = requireContext().getString(R.string.label_core_online_since,
                                                  startTime.toString())
         uptimeContainer.visibleIf(startTime != null)
-      }
     })
     missingFeatures.setOnClickListener {
       MissingFeaturesDialog.Builder(requireActivity())
@@ -154,7 +161,7 @@ class CoreInfoFragment : ServiceBoundFragment() {
       CertificateInfoActivity.launch(it.context)
     }
 
-    viewModel.sslSession.toLiveData().observe(this, Observer {
+    modelHelper.sslSession.toLiveData().observe(this, Observer {
       val certificateChain = it?.orNull()?.peerCertificateChain?.map(X509Helper::convert).orEmpty()
       val leafCertificate = certificateChain.firstOrNull()
       if (leafCertificate != null) {
@@ -198,13 +205,13 @@ class CoreInfoFragment : ServiceBoundFragment() {
     clients.layoutManager = LinearLayoutManager(requireContext())
     val adapter = ClientAdapter()
     adapter.setDisconnectListener {
-      val sessionOptional = viewModel.session.value
+      val sessionOptional = modelHelper.session.value
       val session = sessionOptional?.orNull()
       val rpcHandler = session?.rpcHandler
       rpcHandler?.requestKickClient(it)
     }
     clients.adapter = adapter
-    viewModel.coreInfoClients.toLiveData().observe(this, Observer {
+    modelHelper.coreInfoClients.toLiveData().observe(this, Observer {
       clientsTitle.visibleIf(it?.isNotEmpty() == true)
       adapter.submitList(it)
     })
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 08c0a7ad0..91a7a4070 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
@@ -70,6 +70,7 @@ import de.kuschku.quasseldroid.viewmodel.data.Avatar
 import de.kuschku.quasseldroid.viewmodel.data.BufferHiddenState
 import de.kuschku.quasseldroid.viewmodel.data.BufferProps
 import de.kuschku.quasseldroid.viewmodel.data.BufferStatus
+import de.kuschku.quasseldroid.viewmodel.helper.EditorViewModelHelper
 import io.reactivex.Observable
 import javax.inject.Inject
 
@@ -140,6 +141,9 @@ class UserInfoFragment : ServiceBoundFragment() {
   @Inject
   lateinit var matrixApi: MatrixApi
 
+  @Inject
+  lateinit var modelHelper: EditorViewModelHelper
+
   private var actualUrl: String? = null
 
   override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
@@ -175,7 +179,7 @@ class UserInfoFragment : ServiceBoundFragment() {
       getColor(0, 0)
     }
 
-    combineLatest(viewModel.session, viewModel.networks).switchMap { (sessionOptional, networks) ->
+    combineLatest(modelHelper.session, modelHelper.networks).switchMap { (sessionOptional, networks) ->
       fun processUser(user: IrcUser, bufferSyncer: BufferSyncer? = null, info: BufferInfo? = null,
                       ignoreItems: List<IgnoreListManager.IgnoreListItem>? = null): Observable<Optional<IrcUserInfo>> {
         actionShortcut.post(::updateShortcutVisibility)
@@ -347,7 +351,7 @@ class UserInfoFragment : ServiceBoundFragment() {
         actionWhois.visibleIf(user.knownToCore)
 
         actionQuery.setOnClickListener { view ->
-          viewModel.session.value?.orNull()?.let { session ->
+          modelHelper.session.value?.orNull()?.let { session ->
             val info = session.bufferSyncer.find(
               bufferName = user.nick,
               networkId = user.networkId,
@@ -357,7 +361,7 @@ class UserInfoFragment : ServiceBoundFragment() {
             if (info != null) {
               ChatActivity.launch(view.context, bufferId = info.bufferId)
             } else {
-              viewModel.allBuffers.map {
+              modelHelper.allBuffers.map {
                 listOfNotNull(it.find {
                   it.networkId == user.networkId && it.bufferName == user.nick
                 })
@@ -410,7 +414,7 @@ class UserInfoFragment : ServiceBoundFragment() {
                   true
                 }
                 it.isCheckable -> {
-                  viewModel.ignoreListManager.value?.orNull()?.requestToggleIgnoreRule(it.title.toString())
+                  modelHelper.ignoreListManager.value?.orNull()?.requestToggleIgnoreRule(it.title.toString())
                   true
                 }
                 else               -> false
@@ -428,7 +432,7 @@ class UserInfoFragment : ServiceBoundFragment() {
         }
 
         actionWhois.setOnClickListener { view ->
-          viewModel.session {
+          modelHelper.session {
             it.orNull()?.let { session ->
               session.bufferSyncer.find(
                 networkId = user.networkId,
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/setup/ServiceBoundSlideFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/setup/ServiceBoundSlideFragment.kt
index 1c13bde36..6c6874e72 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/setup/ServiceBoundSlideFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/setup/ServiceBoundSlideFragment.kt
@@ -30,9 +30,6 @@ import io.reactivex.subjects.BehaviorSubject
 import javax.inject.Inject
 
 abstract class ServiceBoundSlideFragment : SlideFragment() {
-  @Inject
-  lateinit var viewModel: QuasselViewModel
-
   private val connection = BackendServiceConnection()
   protected val backend: BehaviorSubject<Optional<Backend>>
     get() = connection.backend
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/setup/core/CoreSetupActivity.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/setup/core/CoreSetupActivity.kt
index b77644f34..b3a942f9e 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/setup/core/CoreSetupActivity.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/setup/core/CoreSetupActivity.kt
@@ -31,8 +31,13 @@ import de.kuschku.libquassel.quassel.ExtendedFeature
 import de.kuschku.libquassel.util.helpers.value
 import de.kuschku.quasseldroid.persistence.models.Account
 import de.kuschku.quasseldroid.ui.setup.ServiceBoundSetupActivity
+import de.kuschku.quasseldroid.viewmodel.helper.EditorViewModelHelper
+import javax.inject.Inject
 
 class CoreSetupActivity : ServiceBoundSetupActivity() {
+  @Inject
+  lateinit var modelHelper: EditorViewModelHelper
+
   override val initData = Bundle()
 
   override fun onCreate(savedInstanceState: Bundle?) {
@@ -51,7 +56,7 @@ class CoreSetupActivity : ServiceBoundSetupActivity() {
     val authenticatorBackend = data.getSerializable("authenticator") as? CoreSetupBackend
     val authenticatorBackendSetup = data.getSerializable("authenticatorSetup") as? HashMap<String, QVariant_>
 
-    viewModel.sessionManager.value?.orNull()?.setupCore(HandshakeMessage.CoreSetupData(
+    modelHelper.sessionManager.value?.orNull()?.setupCore(HandshakeMessage.CoreSetupData(
       adminUser = user,
       adminPassword = pass,
       backend = storageBackend?.backendId,
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/setup/network/NetworkSetupActivity.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/setup/network/NetworkSetupActivity.kt
index 6fd30cd0a..e1ae92985 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/setup/network/NetworkSetupActivity.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/setup/network/NetworkSetupActivity.kt
@@ -29,8 +29,13 @@ import de.kuschku.libquassel.protocol.NetworkId
 import de.kuschku.libquassel.quassel.syncables.interfaces.INetwork
 import de.kuschku.libquassel.util.helpers.value
 import de.kuschku.quasseldroid.ui.setup.ServiceBoundSetupActivity
+import de.kuschku.quasseldroid.viewmodel.helper.EditorViewModelHelper
+import javax.inject.Inject
 
 class NetworkSetupActivity : ServiceBoundSetupActivity() {
+  @Inject
+  lateinit var modelHelper: EditorViewModelHelper
+
   private lateinit var arguments: Bundle
   override val initData: Bundle
     get() = arguments
@@ -45,8 +50,8 @@ class NetworkSetupActivity : ServiceBoundSetupActivity() {
     val networkId = NetworkId(data.getInt("network_id", -1))
     val identity = IdentityId(data.getInt("identity", -1))
     if (networkId.isValidId() || (network != null && identity.isValidId())) {
-      viewModel.backend?.value?.ifPresent { backend ->
-        val session = viewModel.session.value?.orNull()
+      modelHelper.backend?.value?.ifPresent { backend ->
+        val session = modelHelper.session.value?.orNull()
         session?.apply {
           rpcHandler.apply {
             when {
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 c87f2a25a..19a229770 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
@@ -49,6 +49,8 @@ import de.kuschku.quasseldroid.util.TextValidator
 import de.kuschku.quasseldroid.util.helper.combineLatest
 import de.kuschku.quasseldroid.util.helper.toLiveData
 import de.kuschku.quasseldroid.util.ui.AnimationHelper
+import de.kuschku.quasseldroid.viewmodel.helper.EditorViewModelHelper
+import javax.inject.Inject
 
 class NetworkSetupNetworkSlide : ServiceBoundSlideFragment() {
   @BindView(R.id.identity)
@@ -81,6 +83,9 @@ class NetworkSetupNetworkSlide : ServiceBoundSlideFragment() {
   @BindView(R.id.ssl_enabled)
   lateinit var sslEnabled: SwitchCompat
 
+  @Inject
+  lateinit var modelHelper: EditorViewModelHelper
+
   private val identityAdapter = IdentityAdapter()
   private val networkAdapter = NetworkAdapter(R.string.settings_chatlist_network_create)
 
@@ -182,7 +187,7 @@ class NetworkSetupNetworkSlide : ServiceBoundSlideFragment() {
 
     identity.adapter = identityAdapter
 
-    viewModel.identities.switchMap {
+    modelHelper.identities.switchMap {
       combineLatest(it.values.map(Identity::liveUpdates)).map {
         it.sortedBy(Identity::identityName)
       }
@@ -192,7 +197,7 @@ class NetworkSetupNetworkSlide : ServiceBoundSlideFragment() {
       }
     })
 
-    viewModel.networks.switchMap {
+    modelHelper.networks.switchMap {
       combineLatest(it.values.map(Network::liveNetworkInfo)).map {
         it.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, INetwork.NetworkInfo::networkName))
       }
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 d50a8be89..292f05833 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
@@ -33,9 +33,14 @@ import de.kuschku.quasseldroid.defaults.DefaultNetwork
 import de.kuschku.quasseldroid.defaults.Defaults
 import de.kuschku.quasseldroid.ui.setup.ServiceBoundSetupActivity
 import de.kuschku.quasseldroid.util.helper.toLiveData
+import de.kuschku.quasseldroid.viewmodel.helper.EditorViewModelHelper
 import java.util.*
+import javax.inject.Inject
 
 class UserSetupActivity : ServiceBoundSetupActivity() {
+  @Inject
+  lateinit var modelHelper: EditorViewModelHelper
+
   override val initData
     get() = Bundle().apply {
       putString("nick", getString(R.string.default_identity_nick, Random().nextInt(16)))
@@ -45,15 +50,15 @@ class UserSetupActivity : ServiceBoundSetupActivity() {
   override fun onDone(data: Bundle) {
     val network = data.getSerializable("network") as? DefaultNetwork
     if (network != null) {
-      viewModel.backend?.value?.ifPresent { backend ->
-        viewModel.session.value?.orNull()?.rpcHandler?.apply {
+      modelHelper.backend?.value?.ifPresent { backend ->
+        modelHelper.session.value?.orNull()?.rpcHandler?.apply {
           createIdentity(Defaults.identity(this@UserSetupActivity).apply {
             setIdentityName(this@UserSetupActivity.getString(R.string.default_identity_identity_name))
             setNicks(listOf(data.getString("nick")))
             setRealName(data.getString("realname"))
           }, emptyMap())
 
-          viewModel.identities
+          modelHelper.identities
             .map(Map<IdentityId, Identity>::values)
             .filter(Collection<Identity>::isNotEmpty)
             .map(Collection<Identity>::first)
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/ColorContext.kt b/app/src/main/java/de/kuschku/quasseldroid/util/ColorContext.kt
index 1d575c0a4..098682f52 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/util/ColorContext.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/ColorContext.kt
@@ -28,6 +28,7 @@ import de.kuschku.quasseldroid.settings.MessageSettings
 import de.kuschku.quasseldroid.util.helper.styledAttributes
 import de.kuschku.quasseldroid.util.ui.drawable.TextDrawable
 import de.kuschku.quasseldroid.viewmodel.EditorViewModel
+import de.kuschku.quasseldroid.viewmodel.helper.EditorViewModelHelper.Companion.IGNORED_CHARS
 import javax.inject.Inject
 
 class ColorContext @Inject constructor(
@@ -71,7 +72,7 @@ class ColorContext @Inject constructor(
 
   fun buildTextDrawable(nickName: String, self: Boolean): TextDrawable {
     val senderColorIndex = SenderColorUtil.senderColor(nickName)
-    val rawInitial = nickName.trimStart(*EditorViewModel.IGNORED_CHARS).firstOrNull()
+    val rawInitial = nickName.trimStart(*IGNORED_CHARS).firstOrNull()
                      ?: nickName.firstOrNull()
     val initial = rawInitial?.toUpperCase().toString()
     val senderColor = if (self) selfColor else senderColors[senderColorIndex]
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/ShortcutCreationHelper.kt b/app/src/main/java/de/kuschku/quasseldroid/util/ShortcutCreationHelper.kt
index 810f85bac..742e1d141 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/util/ShortcutCreationHelper.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/ShortcutCreationHelper.kt
@@ -42,6 +42,7 @@ import de.kuschku.quasseldroid.util.avatars.AvatarHelper
 import de.kuschku.quasseldroid.util.helper.loadWithFallbacks
 import de.kuschku.quasseldroid.util.helper.styledAttributes
 import de.kuschku.quasseldroid.viewmodel.EditorViewModel
+import de.kuschku.quasseldroid.viewmodel.helper.EditorViewModelHelper.Companion.IGNORED_CHARS
 
 object ShortcutCreationHelper {
   fun create(context: Context,
@@ -91,7 +92,7 @@ object ShortcutCreationHelper {
     if (info.type.hasFlag(Buffer_Type.QueryBuffer)) {
       val nickName = info.bufferName ?: ""
       val senderColorIndex = SenderColorUtil.senderColor(nickName)
-      val rawInitial = nickName.trimStart(*EditorViewModel.IGNORED_CHARS).firstOrNull()
+      val rawInitial = nickName.trimStart(*IGNORED_CHARS).firstOrNull()
                        ?: nickName.firstOrNull()
       val initial = rawInitial?.toUpperCase().toString()
       val senderColor = senderColors[senderColorIndex]
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/service/ServiceBoundActivity.kt b/app/src/main/java/de/kuschku/quasseldroid/util/service/ServiceBoundActivity.kt
index ed67800fa..ed62a7d1a 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/util/service/ServiceBoundActivity.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/service/ServiceBoundActivity.kt
@@ -83,13 +83,13 @@ abstract class ServiceBoundActivity :
   lateinit var connectionSettings: ConnectionSettings
 
   @Inject
-  lateinit var viewModel: QuasselViewModel
+  lateinit var quasselViewModel: QuasselViewModel
 
   protected var accountId: Long = -1
 
   override fun onCreate(savedInstanceState: Bundle?) {
     super.onCreate(savedInstanceState)
-    viewModel.backendWrapper.onNext(this.backend)
+    quasselViewModel.backendWrapper.onNext(this.backend)
     updateRecentsHeader()
     connection.context = this
     lifecycle.addObserver(connection)
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/service/ServiceBoundFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/util/service/ServiceBoundFragment.kt
index d7d848a7c..75cba1a03 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/util/service/ServiceBoundFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/service/ServiceBoundFragment.kt
@@ -31,7 +31,7 @@ import javax.inject.Inject
 
 abstract class ServiceBoundFragment : DaggerFragment() {
   @Inject
-  lateinit var viewModel: QuasselViewModel
+  lateinit var quasselViewModel: QuasselViewModel
 
   private val connection = BackendServiceConnection()
   protected val backend: BehaviorSubject<Optional<Backend>>
diff --git a/viewmodel/build.gradle.kts b/viewmodel/build.gradle.kts
index 01954e17b..219b61edf 100644
--- a/viewmodel/build.gradle.kts
+++ b/viewmodel/build.gradle.kts
@@ -58,6 +58,8 @@ dependencies {
   implementation("org.threeten", "threetenbp", "1.3.8", classifier = "no-tzdb")
   implementation("org.jetbrains", "annotations", "17.0.0")
 
+  implementation("javax.inject", "javax.inject", "1")
+
   // Quassel
   implementation(project(":persistence"))
   implementation(project(":lib")) {
diff --git a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/ChatViewModel.kt b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/ChatViewModel.kt
new file mode 100644
index 000000000..f48c096f3
--- /dev/null
+++ b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/ChatViewModel.kt
@@ -0,0 +1,70 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2019 Janne Koschinski
+ * Copyright (c) 2019 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.kuschku.quasseldroid.viewmodel
+
+import de.kuschku.libquassel.protocol.BufferId
+import de.kuschku.libquassel.protocol.MsgId
+import de.kuschku.libquassel.protocol.NetworkId
+import de.kuschku.quasseldroid.viewmodel.data.FormattedMessage
+import io.reactivex.subjects.BehaviorSubject
+import io.reactivex.subjects.PublishSubject
+
+open class ChatViewModel : QuasselViewModel() {
+  val stateReset = BehaviorSubject.create<Unit>()
+  val selectedMessages = BehaviorSubject.createDefault(emptyMap<MsgId, FormattedMessage>())
+  val bufferSearch = BehaviorSubject.createDefault("")
+  val expandedMessages = BehaviorSubject.createDefault(emptySet<MsgId>())
+  val buffer = BehaviorSubject.createDefault(BufferId.MAX_VALUE)
+  val bufferOpened = PublishSubject.create<Unit>()
+  val bufferViewConfigId = BehaviorSubject.createDefault(-1)
+  val recentlySentMessages = BehaviorSubject.createDefault(emptyList<CharSequence>())
+  val showHidden = BehaviorSubject.createDefault(false)
+  val bufferSearchTemporarilyVisible = BehaviorSubject.createDefault(false)
+  val expandedNetworks = BehaviorSubject.createDefault(emptyMap<NetworkId, Boolean>())
+  val selectedBufferId = BehaviorSubject.createDefault(BufferId.MAX_VALUE)
+
+  fun resetAccount() {
+    bufferViewConfigId.onNext(-1)
+    selectedMessages.onNext(emptyMap())
+    expandedMessages.onNext(emptySet())
+    recentlySentMessages.onNext(emptyList())
+    stateReset.onNext(Unit)
+  }
+
+  fun selectedMessagesToggle(key: MsgId, value: FormattedMessage): Int {
+    val set = selectedMessages.value.orEmpty()
+    val result = if (set.containsKey(key)) set - key else set + Pair(key, value)
+    selectedMessages.onNext(result)
+    return result.size
+  }
+
+  fun addRecentlySentMessage(message: CharSequence) {
+    recentlySentMessages.onNext(
+      listOf(message) + recentlySentMessages.value
+        .orEmpty()
+        .filter { it != message }
+        .take(MAX_RECENT_MESSAGES - 1)
+    )
+  }
+
+  companion object {
+    const val MAX_RECENT_MESSAGES = 20
+  }
+}
diff --git a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/EditorViewModel.kt b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/EditorViewModel.kt
index 80464f9bc..a340d2d65 100644
--- a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/EditorViewModel.kt
+++ b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/EditorViewModel.kt
@@ -20,154 +20,9 @@
 package de.kuschku.quasseldroid.viewmodel
 
 import androidx.lifecycle.ViewModel
-import de.kuschku.libquassel.protocol.BufferId
-import de.kuschku.libquassel.protocol.Buffer_Type
-import de.kuschku.libquassel.quassel.syncables.AliasManager
-import de.kuschku.libquassel.quassel.syncables.IrcChannel
-import de.kuschku.libquassel.quassel.syncables.IrcUser
-import de.kuschku.libquassel.quassel.syncables.Network
-import de.kuschku.libquassel.session.ISession
-import de.kuschku.libquassel.util.Optional
-import de.kuschku.libquassel.util.flag.hasFlag
-import de.kuschku.libquassel.util.helpers.mapNullable
-import de.kuschku.libquassel.util.helpers.nullIf
-import de.kuschku.quasseldroid.util.helper.combineLatest
-import de.kuschku.quasseldroid.viewmodel.data.AutoCompleteItem
-import de.kuschku.quasseldroid.viewmodel.data.BufferStatus
 import io.reactivex.Observable
 import io.reactivex.subjects.BehaviorSubject
-import java.util.concurrent.TimeUnit
 
 class EditorViewModel : ViewModel() {
-  val quasselViewModel = BehaviorSubject.create<QuasselViewModel>()
-
-  val session = quasselViewModel.switchMap(QuasselViewModel::session)
-  val buffer = quasselViewModel.switchMap(QuasselViewModel::buffer)
-
   val lastWord = BehaviorSubject.create<Observable<Pair<String, IntRange>>>()
-
-  val rawAutoCompleteData: Observable<Triple<Optional<ISession>, BufferId, Pair<String, IntRange>>> =
-    combineLatest(session, buffer, lastWord).switchMap { (sessionOptional, id, lastWordWrapper) ->
-      lastWordWrapper
-        .distinctUntilChanged()
-        .map { lastWord ->
-          Triple(sessionOptional, id, lastWord)
-        }
-    }
-
-  val autoCompleteData: Observable<Pair<String, List<AutoCompleteItem>>> = rawAutoCompleteData
-    .distinctUntilChanged()
-    .debounce(300, TimeUnit.MILLISECONDS)
-    .switchMap { (sessionOptional, id, lastWord) ->
-      val session = sessionOptional.orNull()
-      val bufferSyncer = session?.bufferSyncer
-      val bufferInfo = bufferSyncer?.bufferInfo(id)
-      if (bufferSyncer != null) {
-        session.liveNetworks().switchMap { networks ->
-          bufferSyncer.liveBufferInfos().switchMap { infos ->
-            session.aliasManager.updates().map(AliasManager::aliasList).switchMap { aliases ->
-              val network = networks[bufferInfo?.networkId] ?: Network.NULL
-              val ircChannel = if (bufferInfo?.type?.hasFlag(Buffer_Type.ChannelBuffer) == true) {
-                network.ircChannel(bufferInfo.bufferName) ?: IrcChannel.NULL
-              } else IrcChannel.NULL
-              ircChannel.liveIrcUsers().switchMap { users ->
-                fun processResults(results: List<Observable<out AutoCompleteItem>>) =
-                  combineLatest<AutoCompleteItem>(results)
-                    .map { list ->
-                      val filtered = list.filter {
-                        it.name.trimStart(*IGNORED_CHARS)
-                          .startsWith(
-                            lastWord.first.trimStart(*IGNORED_CHARS),
-                            ignoreCase = true
-                          )
-                      }
-                      Pair(
-                        lastWord.first,
-                        filtered.sorted()
-                      )
-                    }
-
-                fun getAliases() = aliases.map {
-                  Observable.just(AutoCompleteItem.AliasItem(
-                    "/${it.name}",
-                    it.expansion
-                  ))
-                }
-
-                fun getBuffers() = infos.values
-                  .filter {
-                    it.type.toInt() == Buffer_Type.ChannelBuffer.toInt()
-                  }.mapNotNull { info ->
-                    networks[info.networkId]?.let { info to it }
-                  }.map { (info, network) ->
-                    network.liveIrcChannel(
-                      info.bufferName
-                    ).switchMap { channel ->
-                      channel.updates().mapNullable(IrcChannel.NULL) {
-                        AutoCompleteItem.ChannelItem(
-                          info = info,
-                          network = network.networkInfo(),
-                          bufferStatus = when (it) {
-                            null -> BufferStatus.OFFLINE
-                            else -> BufferStatus.ONLINE
-                          },
-                          description = it?.topic() ?: ""
-                        )
-                      }
-                    }
-                  }
-
-                fun getUsers(): Set<IrcUser> = when {
-                  bufferInfo?.type?.hasFlag(Buffer_Type.ChannelBuffer) == true ->
-                    users
-                  bufferInfo?.type?.hasFlag(Buffer_Type.QueryBuffer) == true   ->
-                    network.ircUser(bufferInfo.bufferName).nullIf { it == IrcUser.NULL }?.let {
-                      setOf(it)
-                    } ?: emptySet()
-                  else                                                         ->
-                    emptySet()
-                }
-
-                fun getNicks() = getUsers().map<IrcUser, Observable<AutoCompleteItem.UserItem>> {
-                  it.updates().map { user ->
-                    val userModes = ircChannel.userModes(user)
-                    val prefixModes = network.prefixModes()
-
-                    val lowestMode = userModes.mapNotNull(prefixModes::indexOf).min()
-                                     ?: prefixModes.size
-
-                    AutoCompleteItem.UserItem(
-                      user.nick(),
-                      user.hostMask(),
-                      network.modesToPrefixes(userModes),
-                      lowestMode,
-                      user.realName(),
-                      user.isAway(),
-                      user.network().isMyNick(user.nick()),
-                      network.support("CASEMAPPING")
-                    )
-                  }
-                }
-
-                when (lastWord.first.firstOrNull()) {
-                  '/'  -> processResults(getAliases())
-                  '@'  -> processResults(getNicks())
-                  '#'  -> processResults(getBuffers())
-                  else -> processResults(getAliases() + getNicks() + getBuffers())
-                }
-              }
-            }
-          }
-        }
-      } else {
-        Observable.just(Pair(lastWord.first, emptyList()))
-      }
-    }
-
-  companion object {
-    val IGNORED_CHARS = charArrayOf(
-      '!', '"', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', ':', ';', '<', '=',
-      '>', '?', '@', '[', '\\', ']', '^', '_', '`', '{', '|', '}', '~'
-    )
-  }
 }
diff --git a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/QuasselViewModel.kt b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/QuasselViewModel.kt
index da0eac52f..b18a60589 100644
--- a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/QuasselViewModel.kt
+++ b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/QuasselViewModel.kt
@@ -20,562 +20,11 @@
 package de.kuschku.quasseldroid.viewmodel
 
 import androidx.lifecycle.ViewModel
-import de.kuschku.libquassel.connection.ConnectionState
-import de.kuschku.libquassel.connection.Features
-import de.kuschku.libquassel.protocol.*
-import de.kuschku.libquassel.quassel.BufferInfo
-import de.kuschku.libquassel.quassel.syncables.*
-import de.kuschku.libquassel.quassel.syncables.interfaces.INetwork
 import de.kuschku.libquassel.session.Backend
-import de.kuschku.libquassel.session.ISession
-import de.kuschku.libquassel.session.SessionManager
-import de.kuschku.libquassel.ssl.X509Helper
 import de.kuschku.libquassel.util.Optional
-import de.kuschku.libquassel.util.flag.and
-import de.kuschku.libquassel.util.flag.hasFlag
-import de.kuschku.libquassel.util.helpers.*
-import de.kuschku.libquassel.util.irc.IrcCaseMappers
-import de.kuschku.quasseldroid.util.helper.combineLatest
-import de.kuschku.quasseldroid.viewmodel.data.*
 import io.reactivex.Observable
 import io.reactivex.subjects.BehaviorSubject
-import io.reactivex.subjects.PublishSubject
-import java.util.concurrent.TimeUnit
-import javax.net.ssl.SSLSession
-
-class QuasselViewModel : ViewModel() {
-  fun resetAccount() {
-    bufferViewConfigId.onNext(-1)
-    selectedMessages.onNext(emptyMap())
-    expandedMessages.onNext(emptySet())
-    recentlySentMessages.onNext(emptyList())
-    stateReset.onNext(Unit)
-  }
-
-  val stateReset = BehaviorSubject.create<Unit>()
 
+open class QuasselViewModel : ViewModel() {
   val backendWrapper = BehaviorSubject.createDefault(Observable.empty<Optional<Backend>>())
-
-  val selectedMessages = BehaviorSubject.createDefault(emptyMap<MsgId, FormattedMessage>())
-  fun selectedMessagesToggle(key: MsgId, value: FormattedMessage): Int {
-    val set = selectedMessages.value.orEmpty()
-    val result = if (set.containsKey(key)) set - key else set + Pair(key, value)
-    selectedMessages.onNext(result)
-    return result.size
-  }
-
-  val bufferSearch = BehaviorSubject.createDefault("")
-
-  val expandedMessages = BehaviorSubject.createDefault(emptySet<MsgId>())
-
-  val buffer = BehaviorSubject.createDefault(BufferId.MAX_VALUE)
-  val bufferOpened = PublishSubject.create<Unit>()
-
-  val bufferViewConfigId = BehaviorSubject.createDefault(-1)
-
-  val recentlySentMessages = BehaviorSubject.createDefault(emptyList<CharSequence>())
-  fun addRecentlySentMessage(message: CharSequence) {
-    recentlySentMessages.onNext(
-      listOf(message) + recentlySentMessages.value
-        .orEmpty()
-        .filter { it != message }
-        .take(MAX_RECENT_MESSAGES - 1)
-    )
-  }
-
-  val backend = backendWrapper.switchMap { it }
-  val sessionManager = backend.mapMapNullable(Backend::sessionManager)
-  val session = sessionManager.mapSwitchMap(SessionManager::session)
-  val rpcHandler = session.mapMap(ISession::rpcHandler)
-  val ircListHelper = session.mapMap(ISession::ircListHelper)
-  val features = sessionManager.mapSwitchMap { manager ->
-    manager.state.switchMap { state ->
-      if (state != ConnectionState.CONNECTED) {
-        Observable.just(Pair(false, Features.empty()))
-      } else {
-        manager.session.map {
-          Pair(true, it.features)
-        }
-      }
-    }
-  }.mapOrElse(Pair(false, Features.empty()))
-  val clientFeatures = features.map { (connected, features) ->
-    Pair(connected, features.client)
-  }
-  val negotiatedFeatures = features.map { (connected, features) ->
-    Pair(connected, features.negotiated)
-  }
-  val coreFeatures = features.map { (connected, features) ->
-    Pair(connected, features.core)
-  }
-
-  val connectionProgress = sessionManager.mapSwitchMap(SessionManager::connectionProgress)
-    .mapOrElse(Triple(ConnectionState.DISCONNECTED, 0, 0))
-
-  val bufferViewManager = session.mapMap(ISession::bufferViewManager)
-
-  val bufferViewConfig = bufferViewManager.flatMapSwitchMap { manager ->
-    bufferViewConfigId.map { id ->
-      Optional.ofNullable(manager.bufferViewConfig(id))
-    }.mapSwitchMap(BufferViewConfig::liveUpdates)
-  }
-
-  val bufferSearchTemporarilyVisible = BehaviorSubject.createDefault(false)
-
-  val errors = sessionManager.switchMap {
-    it.orNull()?.error ?: Observable.empty()
-  }
-
-  val connectionErrors = sessionManager.switchMap {
-    it.orNull()?.connectionError ?: Observable.empty()
-  }
-
-  val sslSession = session.flatMapSwitchMap(ISession::sslSession)
-  val peerCertificateChain = sslSession.mapMap(SSLSession::getPeerCertificateChain).mapMap {
-    it.mapNotNull(X509Helper::convert)
-  }.mapOrElse(emptyList())
-  val leafCertificate = peerCertificateChain.map { Optional.ofNullable(it.firstOrNull()) }
-
-  val coreInfo = session.mapMap(ISession::coreInfo).mapSwitchMap(CoreInfo::liveInfo)
-  val coreInfoClients = coreInfo.mapMap(CoreInfo.CoreData::sessionConnectedClientData)
-    .mapOrElse(emptyList())
-
-  val networkConfig = session.mapMap(ISession::networkConfig)
-
-  val ignoreListManager = session.mapMap(ISession::ignoreListManager)
-
-  val highlightRuleManager = session.mapMap(ISession::highlightRuleManager)
-
-  val aliasManager = session.mapMap(ISession::aliasManager)
-
-  val networks = session.switchMap {
-    it.map(ISession::liveNetworks).orElse(Observable.just(emptyMap()))
-  }
-
-  val identities = session.switchMap {
-    it.map(ISession::liveIdentities).orElse(Observable.just(emptyMap()))
-  }
-
-  val bufferSyncer = session.mapMap(ISession::bufferSyncer)
-  val allBuffers = bufferSyncer.mapSwitchMap {
-    it.liveBufferInfos().map(Map<BufferId, BufferInfo>::values)
-  }.mapOrElse(emptyList())
-
-  val network = combineLatest(bufferSyncer, networks, buffer).map { (syncer, networks, buffer) ->
-    Optional.ofNullable(syncer.orNull()?.bufferInfo(buffer)?.let { networks[it.networkId] })
-  }
-
-  /**
-   * An observable of the changes of the markerline, as pairs of `(old, new)`
-   */
-  val markerLine = session.mapSwitchMap { currentSession ->
-    buffer.switchMap { currentBuffer ->
-      // Get a stream of the latest marker line
-      currentSession.bufferSyncer.liveMarkerLine(currentBuffer)
-    }
-  }
-
-  val lag: Observable<Long> = session.mapSwitchMap(ISession::lag).mapOrElse(0)
-
-  val bufferData = combineLatest(session, buffer).switchMap { (sessionOptional, id) ->
-    val session = sessionOptional.orNull()
-    val bufferSyncer = session?.bufferSyncer
-    if (bufferSyncer != null) {
-      session.liveNetworks().switchMap { networks ->
-        bufferSyncer.liveBufferInfos().switchMap {
-          val info = bufferSyncer.bufferInfo(id)
-          val network = networks[info?.networkId]
-          if (info == null || network == null) {
-            Observable.just(BufferData())
-          } else {
-            when (info.type.toInt()) {
-              BufferInfo.Type.QueryBuffer.toInt()   -> {
-                network.liveIrcUser(info.bufferName).switchMap {
-                  it.updates().map { user ->
-                    BufferData(
-                      info = info,
-                      network = network,
-                      description = user.realName(),
-                      ircUser =
-                      if (user == IrcUser.NULL) null
-                      else user
-                    )
-                  }
-                }
-              }
-              BufferInfo.Type.ChannelBuffer.toInt() -> {
-                network.liveIrcChannel(
-                  info.bufferName
-                ).switchMap { channel ->
-                  channel.updates().map {
-                    BufferData(
-                      info = info,
-                      network = network,
-                      description = it.topic(),
-                      userCount = it.userCount()
-                    )
-                  }
-                }
-              }
-              BufferInfo.Type.StatusBuffer.toInt()  -> {
-                network.liveConnectionState().map {
-                  BufferData(
-                    info = info,
-                    network = network
-                  )
-                }
-              }
-              else                                  -> Observable.just(
-                BufferData(
-                  description = "type is unknown: ${info.type.toInt()}"
-                )
-              )
-            }
-          }
-        }
-      }
-    } else {
-      Observable.just(BufferData())
-    }
-  }
-  val bufferDataThrottled = bufferData.distinctUntilChanged().throttleLast(100,
-                                                                           TimeUnit.MILLISECONDS)
-
-  val nickData: Observable<List<IrcUserItem>> = combineLatest(session, buffer)
-    .switchMap { (sessionOptional, buffer) ->
-      val session = sessionOptional.orNull()
-      val bufferSyncer = session?.bufferSyncer
-      val bufferInfo = bufferSyncer?.bufferInfo(buffer)
-      if (bufferInfo?.type?.hasFlag(Buffer_Type.ChannelBuffer) == true) {
-        session.liveNetworks().switchMap { networks ->
-          val network = networks[bufferInfo.networkId]
-          network?.liveIrcChannel(bufferInfo.bufferName)?.switchMapNullable(IrcChannel.NULL) { ircChannel ->
-            ircChannel?.liveIrcUsers()?.switchMap { users ->
-              combineLatest<IrcUserItem>(
-                users.map<IrcUser, Observable<IrcUserItem>?> {
-                  it.updates().map { user ->
-                    val userModes = ircChannel.userModes(user)
-                    val prefixModes = network.prefixModes()
-
-                    val lowestMode = userModes.asSequence().mapNotNull {
-                      prefixModes.indexOf(it)
-                    }.min() ?: prefixModes.size
-
-                    IrcUserItem(
-                      user.nick(),
-                      network.modesToPrefixes(userModes),
-                      lowestMode,
-                      user.realName(),
-                      user.hostMask(),
-                      user.isAway(),
-                      user.network().isMyNick(user.nick()),
-                      network.support("CASEMAPPING")
-                    )
-                  }
-                }
-              )
-            } ?: Observable.just(emptyList())
-          }
-        }
-      } else {
-        Observable.just(emptyList())
-      }
-    }
-  val nickDataThrottled = nickData.distinctUntilChanged().throttleLast(100, TimeUnit.MILLISECONDS)
-
-  val bufferViewConfigs = bufferViewManager.mapSwitchMap { manager ->
-    manager.liveBufferViewConfigs().map { ids ->
-      ids.mapNotNull { id ->
-        manager.bufferViewConfig(id)
-      }.sortedWith(BufferViewConfig.NameComparator)
-    }
-  }.mapOrElse(emptyList())
-
-  val bufferViewConfigMap = bufferViewManager.switchMap {
-    it.map { manager ->
-      manager.liveBufferViewConfigs().map {
-        it.mapNotNull(manager::bufferViewConfig).associateBy(BufferViewConfig::bufferViewId)
-      }
-    }.orElse(Observable.empty())
-  }
-
-  val showHidden = BehaviorSubject.createDefault(false)
-  val expandedNetworks = BehaviorSubject.createDefault(emptyMap<NetworkId, Boolean>())
-  val selectedBufferId = BehaviorSubject.createDefault(BufferId.MAX_VALUE)
-  val selectedBuffer = combineLatest(session, selectedBufferId, bufferViewConfig)
-    .switchMap { (sessionOptional, buffer, bufferViewConfigOptional) ->
-      val session = sessionOptional.orNull()
-      val bufferSyncer = session?.bufferSyncer
-      val bufferViewConfig = bufferViewConfigOptional.orNull()
-      if (bufferSyncer != null && bufferViewConfig != null) {
-        session.liveNetworks().switchMap { networks ->
-          val hiddenState = when {
-            bufferViewConfig.removedBuffers().contains(buffer)            ->
-              BufferHiddenState.HIDDEN_PERMANENT
-            bufferViewConfig.temporarilyRemovedBuffers().contains(buffer) ->
-              BufferHiddenState.HIDDEN_TEMPORARY
-            else                                                          ->
-              BufferHiddenState.VISIBLE
-          }
-
-          val info = if (!buffer.isValidId()) networks[NetworkId(-buffer.id)]?.let {
-            BufferInfo(
-              bufferId = buffer,
-              networkId = it.networkId(),
-              groupId = 0,
-              bufferName = it.networkName(),
-              type = Buffer_Type.of(Buffer_Type.StatusBuffer)
-            )
-          } else bufferSyncer.bufferInfo(buffer)
-          if (info != null) {
-            val network = networks[info.networkId]
-            when (info.type.enabledValues().firstOrNull()) {
-              Buffer_Type.StatusBuffer  -> {
-                network?.liveConnectionState()?.map {
-                  SelectedBufferItem(
-                    info,
-                    connectionState = it,
-                    hiddenState = hiddenState
-                  )
-                } ?: Observable.just(SelectedBufferItem(info, hiddenState = hiddenState))
-              }
-              Buffer_Type.ChannelBuffer -> {
-                network?.liveIrcChannel(info.bufferName)?.mapNullable(IrcChannel.NULL) {
-                  SelectedBufferItem(
-                    info,
-                    joined = it != null,
-                    hiddenState = hiddenState
-                  )
-                } ?: Observable.just(SelectedBufferItem(info, hiddenState = hiddenState))
-              }
-              else                      ->
-                Observable.just(SelectedBufferItem(info, hiddenState = hiddenState))
-            }
-          } else {
-            Observable.just(SelectedBufferItem())
-          }
-        }
-      } else {
-        Observable.just(SelectedBufferItem())
-      }
-    }
-
-  val bufferList: Observable<Pair<BufferViewConfig?, List<BufferProps>>> =
-    combineLatest(session, bufferViewConfig, showHidden, bufferSearch)
-      .switchMap { (sessionOptional, configOptional, showHiddenRaw, bufferSearch) ->
-        val session = sessionOptional.orNull()
-        val bufferSyncer = session?.bufferSyncer
-        val showHidden = showHiddenRaw ?: false
-        val config = configOptional.orNull()
-        if (bufferSyncer != null && config != null) {
-          session.liveNetworks().switchMap { networks ->
-            config.liveUpdates()
-              .switchMap { currentConfig ->
-                combineLatest<Collection<BufferId>>(
-                  listOf(
-                    config.liveBuffers(),
-                    config.liveTemporarilyRemovedBuffers(),
-                    config.liveRemovedBuffers()
-                  )
-                ).switchMap { (ids, temp, perm) ->
-                  fun transformIds(ids: Collection<BufferId>, state: BufferHiddenState) =
-                    ids.asSequence().mapNotNull { id ->
-                      bufferSyncer.bufferInfo(id)
-                    }.filter {
-                      bufferSearch.isBlank() ||
-                      it.type.hasFlag(Buffer_Type.StatusBuffer) ||
-                      it.bufferName?.contains(bufferSearch, ignoreCase = true) == true
-                    }.filter {
-                      !currentConfig.networkId().isValidId() || currentConfig.networkId() == it.networkId
-                    }.filter {
-                      (currentConfig.allowedBufferTypes() and it.type).isNotEmpty() ||
-                      (it.type.hasFlag(Buffer_Type.StatusBuffer) && !currentConfig.networkId().isValidId())
-                    }.mapNotNull {
-                      val network = networks[it.networkId]
-                      if (network == null) {
-                        null
-                      } else {
-                        it to network
-                      }
-                    }.map<Pair<BufferInfo, Network>, Observable<BufferProps>?> { (info, network) ->
-                      bufferSyncer.liveActivity(info.bufferId).switchMap { activity ->
-                        bufferSyncer.liveHighlightCount(info.bufferId).map { highlights ->
-                          activity to highlights
-                        }
-                      }.switchMap { (activity, highlights) ->
-                        val name = info.bufferName?.trim() ?: ""
-                        val search = bufferSearch.trim()
-                        val matchMode = when {
-                          name.equals(search, ignoreCase = true)     ->
-                            BufferProps.BufferMatchMode.EXACT
-                          name.startsWith(search, ignoreCase = true) ->
-                            BufferProps.BufferMatchMode.START
-                          else                                       ->
-                            BufferProps.BufferMatchMode.CONTAINS
-                        }
-                        when (info.type.toInt()) {
-                          BufferInfo.Type.QueryBuffer.toInt()   -> {
-                            network.liveNetworkInfo().switchMap { networkInfo ->
-                              network.liveConnectionState().switchMap { connectionState ->
-                                network.liveIrcUser(info.bufferName).switchMap {
-                                  it.updates().mapNullable(IrcUser.NULL) { user ->
-                                    BufferProps(
-                                      info = info,
-                                      network = networkInfo,
-                                      networkConnectionState = connectionState,
-                                      bufferStatus = when {
-                                        user == null  -> BufferStatus.OFFLINE
-                                        user.isAway() -> BufferStatus.AWAY
-                                        else          -> BufferStatus.ONLINE
-                                      },
-                                      description = user?.realName() ?: "",
-                                      activity = activity,
-                                      highlights = highlights,
-                                      hiddenState = state,
-                                      ircUser = user,
-                                      matchMode = matchMode
-                                    )
-                                  }
-                                }
-                              }
-                            }
-                          }
-                          BufferInfo.Type.ChannelBuffer.toInt() -> {
-                            network.liveNetworkInfo().switchMap { networkInfo ->
-                              network.liveConnectionState().switchMap { connectionState ->
-                                network.liveIrcChannel(info.bufferName).switchMap { channel ->
-                                  channel.updates().mapNullable(IrcChannel.NULL) {
-                                    BufferProps(
-                                      info = info,
-                                      network = networkInfo,
-                                      networkConnectionState = connectionState,
-                                      bufferStatus = when (it) {
-                                        null -> BufferStatus.OFFLINE
-                                        else -> BufferStatus.ONLINE
-                                      },
-                                      description = it?.topic() ?: "",
-                                      activity = activity,
-                                      highlights = highlights,
-                                      hiddenState = state,
-                                      matchMode = matchMode
-                                    )
-                                  }
-                                }
-                              }
-                            }
-                          }
-                          BufferInfo.Type.StatusBuffer.toInt()  -> {
-                            network.liveNetworkInfo().switchMap { networkInfo ->
-                              network.liveConnectionState().map { connectionState ->
-                                BufferProps(
-                                  info = info,
-                                  network = networkInfo,
-                                  networkConnectionState = connectionState,
-                                  bufferStatus = BufferStatus.OFFLINE,
-                                  description = "",
-                                  activity = activity,
-                                  highlights = highlights,
-                                  hiddenState = state,
-                                  matchMode = matchMode
-                                )
-                              }
-                            }
-                          }
-                          else                                  -> Observable.empty()
-                        }
-                      }
-                    }
-
-                  fun missingStatusBuffers(
-                    list: Collection<BufferId>): Sequence<Observable<BufferProps>?> {
-                    val totalNetworks = networks.keys
-                    val wantedNetworks = if (!currentConfig.networkId().isValidId()) totalNetworks
-                    else listOf(currentConfig.networkId())
-
-                    val availableNetworks = list.asSequence().mapNotNull { id ->
-                      bufferSyncer.bufferInfo(id)
-                    }.filter {
-                      it.type.hasFlag(Buffer_Type.StatusBuffer)
-                    }.map {
-                      it.networkId
-                    }
-
-                    val missingNetworks = wantedNetworks - availableNetworks
-
-                    return missingNetworks.asSequence().filter {
-                      !currentConfig.networkId().isValidId() || currentConfig.networkId() == it
-                    }.filter {
-                      currentConfig.allowedBufferTypes().hasFlag(Buffer_Type.StatusBuffer)
-                    }.mapNotNull {
-                      networks[it]
-                    }.filter {
-                      !config.hideInactiveNetworks() || it.isConnected()
-                    }.map<Network, Observable<BufferProps>?> { network ->
-                      network.liveNetworkInfo().switchMap { networkInfo ->
-                        network.liveConnectionState().map { connectionState ->
-                          BufferProps(
-                            info = BufferInfo(
-                              bufferId = BufferId(-networkInfo.networkId.id),
-                              networkId = networkInfo.networkId,
-                              groupId = 0,
-                              bufferName = networkInfo.networkName,
-                              type = Buffer_Type.of(Buffer_Type.StatusBuffer)
-                            ),
-                            network = networkInfo,
-                            networkConnectionState = connectionState,
-                            bufferStatus = BufferStatus.OFFLINE,
-                            description = "",
-                            activity = Message_Type.of(),
-                            highlights = 0,
-                            hiddenState = BufferHiddenState.VISIBLE
-                          )
-                        }
-                      }
-                    }
-                  }
-
-                  bufferSyncer.liveBufferInfos().switchMap {
-                    val buffers = if (showHidden || bufferSearch.isNotBlank()) {
-                      transformIds(ids, BufferHiddenState.VISIBLE) +
-                      transformIds(temp - ids, BufferHiddenState.HIDDEN_TEMPORARY) +
-                      transformIds(perm - temp - ids, BufferHiddenState.HIDDEN_PERMANENT) +
-                      missingStatusBuffers(ids + temp + perm)
-                    } else {
-                      transformIds(ids, BufferHiddenState.VISIBLE) +
-                      missingStatusBuffers(ids)
-                    }
-
-                    combineLatest<BufferProps>(buffers.toList()).map { list ->
-                      Pair<BufferViewConfig?, List<BufferProps>>(
-                        config,
-                        list.asSequence().filter {
-                          !config.hideInactiveNetworks() ||
-                          it.networkConnectionState == INetwork.ConnectionState.Initialized
-                        }.filter {
-                          (!config.hideInactiveBuffers()) ||
-                          it.bufferStatus != BufferStatus.OFFLINE ||
-                          it.info.type.hasFlag(Buffer_Type.StatusBuffer)
-                        }.let {
-                          if (config.sortAlphabetically())
-                            it.sortedBy { IrcCaseMappers.unicode.toLowerCaseNullable(it.info.bufferName) }
-                              .sortedBy { it.matchMode.priority }
-                              .sortedByDescending { it.hiddenState == BufferHiddenState.VISIBLE }
-                          else it
-                        }.distinctBy {
-                          it.info.bufferId
-                        }.toList()
-                      )
-                    }
-                  }
-                }
-              }
-          }
-        } else {
-          Observable.just(Pair<BufferViewConfig?, List<BufferProps>>(null, emptyList()))
-        }
-      }
-
-  companion object {
-    const val MAX_RECENT_MESSAGES = 20
-  }
 }
diff --git a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/helper/ChatViewModelHelper.kt b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/helper/ChatViewModelHelper.kt
new file mode 100644
index 000000000..720b65592
--- /dev/null
+++ b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/helper/ChatViewModelHelper.kt
@@ -0,0 +1,434 @@
+package de.kuschku.quasseldroid.viewmodel.helper
+
+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.NetworkId
+import de.kuschku.libquassel.quassel.BufferInfo
+import de.kuschku.libquassel.quassel.syncables.BufferViewConfig
+import de.kuschku.libquassel.quassel.syncables.IrcChannel
+import de.kuschku.libquassel.quassel.syncables.IrcUser
+import de.kuschku.libquassel.quassel.syncables.Network
+import de.kuschku.libquassel.quassel.syncables.interfaces.INetwork
+import de.kuschku.libquassel.util.Optional
+import de.kuschku.libquassel.util.flag.and
+import de.kuschku.libquassel.util.flag.hasFlag
+import de.kuschku.libquassel.util.helpers.flatMapSwitchMap
+import de.kuschku.libquassel.util.helpers.mapNullable
+import de.kuschku.libquassel.util.helpers.mapSwitchMap
+import de.kuschku.libquassel.util.helpers.switchMapNullable
+import de.kuschku.libquassel.util.irc.IrcCaseMappers
+import de.kuschku.quasseldroid.util.helper.combineLatest
+import de.kuschku.quasseldroid.viewmodel.ChatViewModel
+import de.kuschku.quasseldroid.viewmodel.QuasselViewModel
+import de.kuschku.quasseldroid.viewmodel.data.*
+import io.reactivex.Observable
+import java.util.concurrent.TimeUnit
+import javax.inject.Inject
+
+open class ChatViewModelHelper @Inject constructor(
+  val chat: ChatViewModel,
+  quassel: QuasselViewModel
+) : QuasselViewModelHelper(quassel) {
+  val bufferViewConfig = bufferViewManager.flatMapSwitchMap { manager ->
+    chat.bufferViewConfigId.map { id ->
+      Optional.ofNullable(manager.bufferViewConfig(id))
+    }.mapSwitchMap(BufferViewConfig::liveUpdates)
+  }
+
+  val network = combineLatest(bufferSyncer, networks, chat.buffer)
+    .map { (syncer, networks, buffer) ->
+      Optional.ofNullable(syncer.orNull()?.bufferInfo(buffer)?.let { networks[it.networkId] })
+    }
+
+  /**
+   * An observable of the changes of the markerline, as pairs of `(old, new)`
+   */
+  val markerLine = session.mapSwitchMap { currentSession ->
+    chat.buffer.switchMap { currentBuffer ->
+      // Get a stream of the latest marker line
+      currentSession.bufferSyncer.liveMarkerLine(currentBuffer)
+    }
+  }
+
+  val bufferData = combineLatest(session, chat.buffer)
+    .switchMap { (sessionOptional, id) ->
+      val session = sessionOptional.orNull()
+      val bufferSyncer = session?.bufferSyncer
+      if (bufferSyncer != null) {
+        session.liveNetworks().switchMap { networks ->
+          bufferSyncer.liveBufferInfos().switchMap {
+            val info = bufferSyncer.bufferInfo(id)
+            val network = networks[info?.networkId]
+            if (info == null || network == null) {
+              Observable.just(BufferData())
+            } else {
+              when (info.type.toInt()) {
+                BufferInfo.Type.QueryBuffer.toInt()   -> {
+                  network.liveIrcUser(info.bufferName).switchMap {
+                    it.updates().map { user ->
+                      BufferData(
+                        info = info,
+                        network = network,
+                        description = user.realName(),
+                        ircUser =
+                        if (user == IrcUser.NULL) null
+                        else user
+                      )
+                    }
+                  }
+                }
+                BufferInfo.Type.ChannelBuffer.toInt() -> {
+                  network.liveIrcChannel(
+                    info.bufferName
+                  ).switchMap { channel ->
+                    channel.updates().map {
+                      BufferData(
+                        info = info,
+                        network = network,
+                        description = it.topic(),
+                        userCount = it.userCount()
+                      )
+                    }
+                  }
+                }
+                BufferInfo.Type.StatusBuffer.toInt()  -> {
+                  network.liveConnectionState().map {
+                    BufferData(
+                      info = info,
+                      network = network
+                    )
+                  }
+                }
+                else                                  -> Observable.just(
+                  BufferData(
+                    description = "type is unknown: ${info.type.toInt()}"
+                  )
+                )
+              }
+            }
+          }
+        }
+      } else {
+        Observable.just(BufferData())
+      }
+    }
+  val bufferDataThrottled =
+    bufferData.distinctUntilChanged().throttleLast(100, TimeUnit.MILLISECONDS)
+
+  val nickData: Observable<List<IrcUserItem>> = combineLatest(session, chat.buffer)
+    .switchMap { (sessionOptional, buffer) ->
+      val session = sessionOptional.orNull()
+      val bufferSyncer = session?.bufferSyncer
+      val bufferInfo = bufferSyncer?.bufferInfo(buffer)
+      if (bufferInfo?.type?.hasFlag(Buffer_Type.ChannelBuffer) == true) {
+        session.liveNetworks().switchMap { networks ->
+          val network = networks[bufferInfo.networkId]
+          network?.liveIrcChannel(bufferInfo.bufferName)?.switchMapNullable(IrcChannel.NULL) { ircChannel ->
+            ircChannel?.liveIrcUsers()?.switchMap { users ->
+              combineLatest<IrcUserItem>(
+                users.map<IrcUser, Observable<IrcUserItem>?> {
+                  it.updates().map { user ->
+                    val userModes = ircChannel.userModes(user)
+                    val prefixModes = network.prefixModes()
+
+                    val lowestMode = userModes.asSequence().mapNotNull {
+                      prefixModes.indexOf(it)
+                    }.min() ?: prefixModes.size
+
+                    IrcUserItem(
+                      user.nick(),
+                      network.modesToPrefixes(userModes),
+                      lowestMode,
+                      user.realName(),
+                      user.hostMask(),
+                      user.isAway(),
+                      user.network().isMyNick(user.nick()),
+                      network.support("CASEMAPPING")
+                    )
+                  }
+                }
+              )
+            } ?: Observable.just(emptyList())
+          }
+        }
+      } else {
+        Observable.just(emptyList())
+      }
+    }
+  val nickDataThrottled =
+    nickData.distinctUntilChanged().throttleLast(100, TimeUnit.MILLISECONDS)
+
+  val selectedBuffer = combineLatest(session, chat.selectedBufferId, bufferViewConfig)
+    .switchMap { (sessionOptional, buffer, bufferViewConfigOptional) ->
+      val session = sessionOptional.orNull()
+      val bufferSyncer = session?.bufferSyncer
+      val bufferViewConfig = bufferViewConfigOptional.orNull()
+      if (bufferSyncer != null && bufferViewConfig != null) {
+        session.liveNetworks().switchMap { networks ->
+          val hiddenState = when {
+            bufferViewConfig.removedBuffers().contains(buffer)            ->
+              BufferHiddenState.HIDDEN_PERMANENT
+            bufferViewConfig.temporarilyRemovedBuffers().contains(buffer) ->
+              BufferHiddenState.HIDDEN_TEMPORARY
+            else                                                          ->
+              BufferHiddenState.VISIBLE
+          }
+
+          val info = if (!buffer.isValidId()) networks[NetworkId(-buffer.id)]?.let {
+            BufferInfo(
+              bufferId = buffer,
+              networkId = it.networkId(),
+              groupId = 0,
+              bufferName = it.networkName(),
+              type = Buffer_Type.of(Buffer_Type.StatusBuffer)
+            )
+          } else bufferSyncer.bufferInfo(buffer)
+          if (info != null) {
+            val network = networks[info.networkId]
+            when (info.type.enabledValues().firstOrNull()) {
+              Buffer_Type.StatusBuffer  -> {
+                network?.liveConnectionState()?.map {
+                  SelectedBufferItem(
+                    info,
+                    connectionState = it,
+                    hiddenState = hiddenState
+                  )
+                } ?: Observable.just(SelectedBufferItem(info, hiddenState = hiddenState))
+              }
+              Buffer_Type.ChannelBuffer -> {
+                network?.liveIrcChannel(info.bufferName)?.mapNullable(IrcChannel.NULL) {
+                  SelectedBufferItem(
+                    info,
+                    joined = it != null,
+                    hiddenState = hiddenState
+                  )
+                } ?: Observable.just(SelectedBufferItem(info, hiddenState = hiddenState))
+              }
+              else                      ->
+                Observable.just(SelectedBufferItem(info, hiddenState = hiddenState))
+            }
+          } else {
+            Observable.just(SelectedBufferItem())
+          }
+        }
+      } else {
+        Observable.just(SelectedBufferItem())
+      }
+    }
+
+  val bufferList: Observable<Pair<BufferViewConfig?, List<BufferProps>>> =
+    combineLatest(session, bufferViewConfig, chat.showHidden, chat.bufferSearch)
+      .switchMap { (sessionOptional, configOptional, showHiddenRaw, bufferSearch) ->
+        val session = sessionOptional.orNull()
+        val bufferSyncer = session?.bufferSyncer
+        val showHidden = showHiddenRaw ?: false
+        val config = configOptional.orNull()
+        if (bufferSyncer != null && config != null) {
+          session.liveNetworks().switchMap { networks ->
+            config.liveUpdates()
+              .switchMap { currentConfig ->
+                combineLatest<Collection<BufferId>>(
+                  listOf(
+                    config.liveBuffers(),
+                    config.liveTemporarilyRemovedBuffers(),
+                    config.liveRemovedBuffers()
+                  )
+                ).switchMap { (ids, temp, perm) ->
+                  fun transformIds(ids: Collection<BufferId>, state: BufferHiddenState) =
+                    ids.asSequence().mapNotNull { id ->
+                      bufferSyncer.bufferInfo(id)
+                    }.filter {
+                      bufferSearch.isBlank() ||
+                      it.type.hasFlag(Buffer_Type.StatusBuffer) ||
+                      it.bufferName?.contains(bufferSearch, ignoreCase = true) == true
+                    }.filter {
+                      !currentConfig.networkId().isValidId() || currentConfig.networkId() == it.networkId
+                    }.filter {
+                      (currentConfig.allowedBufferTypes() and it.type).isNotEmpty() ||
+                      (it.type.hasFlag(Buffer_Type.StatusBuffer) && !currentConfig.networkId().isValidId())
+                    }.mapNotNull {
+                      val network = networks[it.networkId]
+                      if (network == null) {
+                        null
+                      } else {
+                        it to network
+                      }
+                    }.map<Pair<BufferInfo, Network>, Observable<BufferProps>?> { (info, network) ->
+                      bufferSyncer.liveActivity(info.bufferId).switchMap { activity ->
+                        bufferSyncer.liveHighlightCount(info.bufferId).map { highlights ->
+                          activity to highlights
+                        }
+                      }.switchMap { (activity, highlights) ->
+                        val name = info.bufferName?.trim() ?: ""
+                        val search = bufferSearch.trim()
+                        val matchMode = when {
+                          name.equals(search, ignoreCase = true)     ->
+                            BufferProps.BufferMatchMode.EXACT
+                          name.startsWith(search, ignoreCase = true) ->
+                            BufferProps.BufferMatchMode.START
+                          else                                       ->
+                            BufferProps.BufferMatchMode.CONTAINS
+                        }
+                        when (info.type.toInt()) {
+                          BufferInfo.Type.QueryBuffer.toInt()   -> {
+                            network.liveNetworkInfo().switchMap { networkInfo ->
+                              network.liveConnectionState().switchMap { connectionState ->
+                                network.liveIrcUser(info.bufferName).switchMap {
+                                  it.updates().mapNullable(IrcUser.NULL) { user ->
+                                    BufferProps(
+                                      info = info,
+                                      network = networkInfo,
+                                      networkConnectionState = connectionState,
+                                      bufferStatus = when {
+                                        user == null  -> BufferStatus.OFFLINE
+                                        user.isAway() -> BufferStatus.AWAY
+                                        else          -> BufferStatus.ONLINE
+                                      },
+                                      description = user?.realName() ?: "",
+                                      activity = activity,
+                                      highlights = highlights,
+                                      hiddenState = state,
+                                      ircUser = user,
+                                      matchMode = matchMode
+                                    )
+                                  }
+                                }
+                              }
+                            }
+                          }
+                          BufferInfo.Type.ChannelBuffer.toInt() -> {
+                            network.liveNetworkInfo().switchMap { networkInfo ->
+                              network.liveConnectionState().switchMap { connectionState ->
+                                network.liveIrcChannel(info.bufferName).switchMap { channel ->
+                                  channel.updates().mapNullable(IrcChannel.NULL) {
+                                    BufferProps(
+                                      info = info,
+                                      network = networkInfo,
+                                      networkConnectionState = connectionState,
+                                      bufferStatus = when (it) {
+                                        null -> BufferStatus.OFFLINE
+                                        else -> BufferStatus.ONLINE
+                                      },
+                                      description = it?.topic() ?: "",
+                                      activity = activity,
+                                      highlights = highlights,
+                                      hiddenState = state,
+                                      matchMode = matchMode
+                                    )
+                                  }
+                                }
+                              }
+                            }
+                          }
+                          BufferInfo.Type.StatusBuffer.toInt()  -> {
+                            network.liveNetworkInfo().switchMap { networkInfo ->
+                              network.liveConnectionState().map { connectionState ->
+                                BufferProps(
+                                  info = info,
+                                  network = networkInfo,
+                                  networkConnectionState = connectionState,
+                                  bufferStatus = BufferStatus.OFFLINE,
+                                  description = "",
+                                  activity = activity,
+                                  highlights = highlights,
+                                  hiddenState = state,
+                                  matchMode = matchMode
+                                )
+                              }
+                            }
+                          }
+                          else                                  -> Observable.empty()
+                        }
+                      }
+                    }
+
+                  fun missingStatusBuffers(
+                    list: Collection<BufferId>): Sequence<Observable<BufferProps>?> {
+                    val totalNetworks = networks.keys
+                    val wantedNetworks = if (!currentConfig.networkId().isValidId()) totalNetworks
+                    else listOf(currentConfig.networkId())
+
+                    val availableNetworks = list.asSequence().mapNotNull { id ->
+                      bufferSyncer.bufferInfo(id)
+                    }.filter {
+                      it.type.hasFlag(Buffer_Type.StatusBuffer)
+                    }.map {
+                      it.networkId
+                    }
+
+                    val missingNetworks = wantedNetworks - availableNetworks
+
+                    return missingNetworks.asSequence().filter {
+                      !currentConfig.networkId().isValidId() || currentConfig.networkId() == it
+                    }.filter {
+                      currentConfig.allowedBufferTypes().hasFlag(Buffer_Type.StatusBuffer)
+                    }.mapNotNull {
+                      networks[it]
+                    }.filter {
+                      !config.hideInactiveNetworks() || it.isConnected()
+                    }.map<Network, Observable<BufferProps>?> { network ->
+                      network.liveNetworkInfo().switchMap { networkInfo ->
+                        network.liveConnectionState().map { connectionState ->
+                          BufferProps(
+                            info = BufferInfo(
+                              bufferId = BufferId(-networkInfo.networkId.id),
+                              networkId = networkInfo.networkId,
+                              groupId = 0,
+                              bufferName = networkInfo.networkName,
+                              type = Buffer_Type.of(Buffer_Type.StatusBuffer)
+                            ),
+                            network = networkInfo,
+                            networkConnectionState = connectionState,
+                            bufferStatus = BufferStatus.OFFLINE,
+                            description = "",
+                            activity = Message_Type.of(),
+                            highlights = 0,
+                            hiddenState = BufferHiddenState.VISIBLE
+                          )
+                        }
+                      }
+                    }
+                  }
+
+                  bufferSyncer.liveBufferInfos().switchMap {
+                    val buffers = if (showHidden || bufferSearch.isNotBlank()) {
+                      transformIds(ids, BufferHiddenState.VISIBLE) +
+                      transformIds(temp - ids, BufferHiddenState.HIDDEN_TEMPORARY) +
+                      transformIds(perm - temp - ids, BufferHiddenState.HIDDEN_PERMANENT) +
+                      missingStatusBuffers(ids + temp + perm)
+                    } else {
+                      transformIds(ids, BufferHiddenState.VISIBLE) +
+                      missingStatusBuffers(ids)
+                    }
+
+                    combineLatest<BufferProps>(buffers.toList()).map { list ->
+                      Pair<BufferViewConfig?, List<BufferProps>>(
+                        config,
+                        list.asSequence().filter {
+                          !config.hideInactiveNetworks() ||
+                          it.networkConnectionState == INetwork.ConnectionState.Initialized
+                        }.filter {
+                          (!config.hideInactiveBuffers()) ||
+                          it.bufferStatus != BufferStatus.OFFLINE ||
+                          it.info.type.hasFlag(Buffer_Type.StatusBuffer)
+                        }.let {
+                          if (config.sortAlphabetically())
+                            it.sortedBy { IrcCaseMappers.unicode.toLowerCaseNullable(it.info.bufferName) }
+                              .sortedBy { it.matchMode.priority }
+                              .sortedByDescending { it.hiddenState == BufferHiddenState.VISIBLE }
+                          else it
+                        }.distinctBy {
+                          it.info.bufferId
+                        }.toList()
+                      )
+                    }
+                  }
+                }
+              }
+          }
+        } else {
+          Observable.just(Pair<BufferViewConfig?, List<BufferProps>>(null, emptyList()))
+        }
+      }
+}
diff --git a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/helper/EditorViewModelHelper.kt b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/helper/EditorViewModelHelper.kt
new file mode 100644
index 000000000..89f1d1011
--- /dev/null
+++ b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/helper/EditorViewModelHelper.kt
@@ -0,0 +1,155 @@
+package de.kuschku.quasseldroid.viewmodel.helper
+
+import de.kuschku.libquassel.protocol.BufferId
+import de.kuschku.libquassel.protocol.Buffer_Type
+import de.kuschku.libquassel.quassel.syncables.AliasManager
+import de.kuschku.libquassel.quassel.syncables.IrcChannel
+import de.kuschku.libquassel.quassel.syncables.IrcUser
+import de.kuschku.libquassel.quassel.syncables.Network
+import de.kuschku.libquassel.session.ISession
+import de.kuschku.libquassel.util.Optional
+import de.kuschku.libquassel.util.flag.hasFlag
+import de.kuschku.libquassel.util.helpers.mapNullable
+import de.kuschku.libquassel.util.helpers.nullIf
+import de.kuschku.quasseldroid.util.helper.combineLatest
+import de.kuschku.quasseldroid.viewmodel.ChatViewModel
+import de.kuschku.quasseldroid.viewmodel.EditorViewModel
+import de.kuschku.quasseldroid.viewmodel.QuasselViewModel
+import de.kuschku.quasseldroid.viewmodel.data.AutoCompleteItem
+import de.kuschku.quasseldroid.viewmodel.data.BufferStatus
+import io.reactivex.Observable
+import java.util.concurrent.TimeUnit
+import javax.inject.Inject
+
+open class EditorViewModelHelper @Inject constructor(
+  val editor: EditorViewModel,
+  chat: ChatViewModel,
+  quassel: QuasselViewModel
+) : ChatViewModelHelper(chat, quassel) {
+  val rawAutoCompleteData: Observable<Triple<Optional<ISession>, BufferId, Pair<String, IntRange>>> =
+    combineLatest(session,
+                  chat.buffer,
+                  editor.lastWord).switchMap { (sessionOptional, id, lastWordWrapper) ->
+      lastWordWrapper
+        .distinctUntilChanged()
+        .map { lastWord ->
+          Triple(sessionOptional, id, lastWord)
+        }
+    }
+
+  val autoCompleteData: Observable<Pair<String, List<AutoCompleteItem>>> = rawAutoCompleteData
+    .distinctUntilChanged()
+    .debounce(300, TimeUnit.MILLISECONDS)
+    .switchMap { (sessionOptional, id, lastWord) ->
+      val session = sessionOptional.orNull()
+      val bufferSyncer = session?.bufferSyncer
+      val bufferInfo = bufferSyncer?.bufferInfo(id)
+      if (bufferSyncer != null) {
+        session.liveNetworks().switchMap { networks ->
+          bufferSyncer.liveBufferInfos().switchMap { infos ->
+            session.aliasManager.updates().map(AliasManager::aliasList).switchMap { aliases ->
+              val network = networks[bufferInfo?.networkId] ?: Network.NULL
+              val ircChannel = if (bufferInfo?.type?.hasFlag(Buffer_Type.ChannelBuffer) == true) {
+                network.ircChannel(bufferInfo.bufferName) ?: IrcChannel.NULL
+              } else IrcChannel.NULL
+              ircChannel.liveIrcUsers().switchMap { users ->
+                fun processResults(results: List<Observable<out AutoCompleteItem>>) =
+                  combineLatest<AutoCompleteItem>(results)
+                    .map { list ->
+                      val filtered = list.filter {
+                        it.name.trimStart(*IGNORED_CHARS)
+                          .startsWith(
+                            lastWord.first.trimStart(*IGNORED_CHARS),
+                            ignoreCase = true
+                          )
+                      }
+                      Pair(
+                        lastWord.first,
+                        filtered.sorted()
+                      )
+                    }
+
+                fun getAliases() = aliases.map {
+                  Observable.just(AutoCompleteItem.AliasItem(
+                    "/${it.name}",
+                    it.expansion
+                  ))
+                }
+
+                fun getBuffers() = infos.values
+                  .filter {
+                    it.type.toInt() == Buffer_Type.ChannelBuffer.toInt()
+                  }.mapNotNull { info ->
+                    networks[info.networkId]?.let { info to it }
+                  }.map { (info, network) ->
+                    network.liveIrcChannel(
+                      info.bufferName
+                    ).switchMap { channel ->
+                      channel.updates().mapNullable(IrcChannel.NULL) {
+                        AutoCompleteItem.ChannelItem(
+                          info = info,
+                          network = network.networkInfo(),
+                          bufferStatus = when (it) {
+                            null -> BufferStatus.OFFLINE
+                            else -> BufferStatus.ONLINE
+                          },
+                          description = it?.topic() ?: ""
+                        )
+                      }
+                    }
+                  }
+
+                fun getUsers(): Set<IrcUser> = when {
+                  bufferInfo?.type?.hasFlag(Buffer_Type.ChannelBuffer) == true ->
+                    users
+                  bufferInfo?.type?.hasFlag(Buffer_Type.QueryBuffer) == true   ->
+                    network.ircUser(bufferInfo.bufferName).nullIf { it == IrcUser.NULL }?.let {
+                      setOf(it)
+                    } ?: emptySet()
+                  else                                                         ->
+                    emptySet()
+                }
+
+                fun getNicks() = getUsers().map<IrcUser, Observable<AutoCompleteItem.UserItem>> {
+                  it.updates().map { user ->
+                    val userModes = ircChannel.userModes(user)
+                    val prefixModes = network.prefixModes()
+
+                    val lowestMode = userModes.mapNotNull(prefixModes::indexOf).min()
+                                     ?: prefixModes.size
+
+                    AutoCompleteItem.UserItem(
+                      user.nick(),
+                      user.hostMask(),
+                      network.modesToPrefixes(userModes),
+                      lowestMode,
+                      user.realName(),
+                      user.isAway(),
+                      user.network().isMyNick(user.nick()),
+                      network.support("CASEMAPPING")
+                    )
+                  }
+                }
+
+                when (lastWord.first.firstOrNull()) {
+                  '/'  -> processResults(getAliases())
+                  '@'  -> processResults(getNicks())
+                  '#'  -> processResults(getBuffers())
+                  else -> processResults(getAliases() + getNicks() + getBuffers())
+                }
+              }
+            }
+          }
+        }
+      } else {
+        Observable.just(Pair(lastWord.first, emptyList()))
+      }
+    }
+
+  companion object {
+    val IGNORED_CHARS = charArrayOf(
+      '!', '"', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', ':', ';', '<', '=',
+      '>', '?', '@', '[', '\\', ']', '^', '_', '`', '{', '|', '}', '~'
+    )
+  }
+}
diff --git a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/helper/QuasselViewModelHelper.kt b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/helper/QuasselViewModelHelper.kt
new file mode 100644
index 000000000..7c839ebf0
--- /dev/null
+++ b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/helper/QuasselViewModelHelper.kt
@@ -0,0 +1,110 @@
+package de.kuschku.quasseldroid.viewmodel.helper
+
+import de.kuschku.libquassel.connection.ConnectionState
+import de.kuschku.libquassel.connection.Features
+import de.kuschku.libquassel.protocol.BufferId
+import de.kuschku.libquassel.quassel.BufferInfo
+import de.kuschku.libquassel.quassel.syncables.BufferViewConfig
+import de.kuschku.libquassel.quassel.syncables.CoreInfo
+import de.kuschku.libquassel.session.Backend
+import de.kuschku.libquassel.session.ISession
+import de.kuschku.libquassel.session.SessionManager
+import de.kuschku.libquassel.ssl.X509Helper
+import de.kuschku.libquassel.util.Optional
+import de.kuschku.libquassel.util.helpers.*
+import de.kuschku.quasseldroid.viewmodel.QuasselViewModel
+import io.reactivex.Observable
+import javax.inject.Inject
+import javax.net.ssl.SSLSession
+
+open class QuasselViewModelHelper @Inject constructor(
+  val quassel: QuasselViewModel
+) {
+  val backend = quassel.backendWrapper.switchMap { it }
+  val sessionManager = backend.mapMapNullable(Backend::sessionManager)
+  val session = sessionManager.mapSwitchMap(SessionManager::session)
+  val rpcHandler = session.mapMap(ISession::rpcHandler)
+  val ircListHelper = session.mapMap(ISession::ircListHelper)
+  val features = sessionManager.mapSwitchMap { manager ->
+    manager.state.switchMap { state ->
+      if (state != ConnectionState.CONNECTED) {
+        Observable.just(Pair(false, Features.empty()))
+      } else {
+        manager.session.map {
+          Pair(true, it.features)
+        }
+      }
+    }
+  }.mapOrElse(Pair(false, Features.empty()))
+  val clientFeatures = features.map { (connected, features) ->
+    Pair(connected, features.client)
+  }
+  val negotiatedFeatures = features.map { (connected, features) ->
+    Pair(connected, features.negotiated)
+  }
+  val coreFeatures = features.map { (connected, features) ->
+    Pair(connected, features.core)
+  }
+
+  val connectionProgress = sessionManager.mapSwitchMap(SessionManager::connectionProgress)
+    .mapOrElse(Triple(ConnectionState.DISCONNECTED, 0, 0))
+
+  val bufferViewManager = session.mapMap(ISession::bufferViewManager)
+
+  val errors = sessionManager.switchMap {
+    it.orNull()?.error ?: Observable.empty()
+  }
+
+  val connectionErrors = sessionManager.switchMap {
+    it.orNull()?.connectionError ?: Observable.empty()
+  }
+
+  val sslSession = session.flatMapSwitchMap(ISession::sslSession)
+  val peerCertificateChain = sslSession.mapMap(SSLSession::getPeerCertificateChain).mapMap {
+    it.mapNotNull(X509Helper::convert)
+  }.mapOrElse(emptyList())
+  val leafCertificate = peerCertificateChain.map { Optional.ofNullable(it.firstOrNull()) }
+
+  val coreInfo = session.mapMap(ISession::coreInfo).mapSwitchMap(CoreInfo::liveInfo)
+  val coreInfoClients = coreInfo.mapMap(CoreInfo.CoreData::sessionConnectedClientData)
+    .mapOrElse(emptyList())
+
+  val networkConfig = session.mapMap(ISession::networkConfig)
+
+  val ignoreListManager = session.mapMap(ISession::ignoreListManager)
+
+  val highlightRuleManager = session.mapMap(ISession::highlightRuleManager)
+
+  val aliasManager = session.mapMap(ISession::aliasManager)
+
+  val networks = session.switchMap {
+    it.map(ISession::liveNetworks).orElse(Observable.just(emptyMap()))
+  }
+
+  val identities = session.switchMap {
+    it.map(ISession::liveIdentities).orElse(Observable.just(emptyMap()))
+  }
+
+  val bufferSyncer = session.mapMap(ISession::bufferSyncer)
+  val allBuffers = bufferSyncer.mapSwitchMap {
+    it.liveBufferInfos().map(Map<BufferId, BufferInfo>::values)
+  }.mapOrElse(emptyList())
+
+  val lag: Observable<Long> = session.mapSwitchMap(ISession::lag).mapOrElse(0)
+
+  val bufferViewConfigs = bufferViewManager.mapSwitchMap { manager ->
+    manager.liveBufferViewConfigs().map { ids ->
+      ids.mapNotNull { id ->
+        manager.bufferViewConfig(id)
+      }.sortedWith(BufferViewConfig.NameComparator)
+    }
+  }.mapOrElse(emptyList())
+
+  val bufferViewConfigMap = bufferViewManager.switchMap {
+    it.map { manager ->
+      manager.liveBufferViewConfigs().map {
+        it.mapNotNull(manager::bufferViewConfig).associateBy(BufferViewConfig::bufferViewId)
+      }
+    }.orElse(Observable.empty())
+  }
+}
-- 
GitLab