From a448f5071e1eeebc9ab870c25b1f5a4f4ea7e8fd Mon Sep 17 00:00:00 2001
From: Janne Koschinski <janne@kuschku.de>
Date: Sun, 25 Feb 2018 02:16:08 +0100
Subject: [PATCH] Allow contextual buffer actions

---
 .../ui/chat/buffers/BufferListAdapter.kt      |  22 ++--
 .../chat/buffers/BufferViewConfigFragment.kt  | 120 +++++++++++++++++-
 .../ui/viewmodel/QuasselViewModel.kt          |  41 +++++-
 app/src/main/res/menu/context_buffer.xml      |  19 +++
 app/src/main/res/values/strings.xml           |  11 +-
 .../libquassel/quassel/syncables/Network.kt   |   1 +
 6 files changed, 191 insertions(+), 23 deletions(-)
 create mode 100644 app/src/main/res/menu/context_buffer.xml

diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/buffers/BufferListAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/buffers/BufferListAdapter.kt
index 600c78c7c..19cf99251 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/buffers/BufferListAdapter.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/buffers/BufferListAdapter.kt
@@ -28,6 +28,7 @@ import de.kuschku.quasseldroid_ng.util.helper.zip
 class BufferListAdapter(
   lifecycleOwner: LifecycleOwner,
   liveData: LiveData<List<BufferProps>?>,
+  private val selectedBuffer: MutableLiveData<BufferId>,
   runInBackground: (() -> Unit) -> Any,
   runOnUiThread: (Runnable) -> Any,
   private val clickListener: ((BufferId) -> Unit)? = null,
@@ -37,8 +38,6 @@ class BufferListAdapter(
 
   private val collapsedNetworks = MutableLiveData<Set<NetworkId>>()
 
-  val selectedBuffers = MutableLiveData<Set<BufferId>>()
-
   fun expandListener(networkId: NetworkId) {
     if (collapsedNetworks.value.orEmpty().contains(networkId))
       collapsedNetworks.postValue(collapsedNetworks.value.orEmpty() - networkId)
@@ -47,28 +46,27 @@ class BufferListAdapter(
   }
 
   fun toggleSelection(buffer: BufferId) {
-    val value = selectedBuffers.value.orEmpty()
-    if (value.contains(buffer)) {
-      selectedBuffers.value = value - buffer
+    if (selectedBuffer.value == buffer) {
+      selectedBuffer.value = -1
     } else {
-      selectedBuffers.value = value + buffer
+      selectedBuffer.value = buffer
     }
   }
 
   fun unselectAll() {
-    selectedBuffers.value = emptySet()
+    selectedBuffer.value = -1
   }
 
   init {
     collapsedNetworks.value = emptySet()
-    selectedBuffers.value = emptySet()
+    selectedBuffer.value = -1
 
-    liveData.zip(collapsedNetworks, selectedBuffers).observe(
-      lifecycleOwner, Observer { it: Triple<List<BufferProps>?, Set<NetworkId>, Set<BufferId>>? ->
+    liveData.zip(collapsedNetworks, selectedBuffer).observe(
+      lifecycleOwner, Observer { it: Triple<List<BufferProps>?, Set<NetworkId>, BufferId>? ->
       runInBackground {
         val list = it?.first ?: emptyList()
         val collapsedNetworks = it?.second ?: emptySet()
-        val selected = it?.third ?: emptySet()
+        val selected = it?.third ?: -1
 
         val old: List<BufferListItem> = data
         val new: List<BufferListItem> = list.sortedBy { props ->
@@ -80,7 +78,7 @@ class BufferListAdapter(
               props,
               BufferState(
                 networkExpanded = !collapsedNetworks.contains(props.network.networkId),
-                selected = selected.contains(props.info.bufferId)
+                selected = selected == props.info.bufferId
               )
             )
         }.filter { (props, state) ->
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/buffers/BufferViewConfigFragment.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/buffers/BufferViewConfigFragment.kt
index 6e3b95730..a4a5ab9a5 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/buffers/BufferViewConfigFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/buffers/BufferViewConfigFragment.kt
@@ -1,5 +1,6 @@
 package de.kuschku.quasseldroid_ng.ui.chat.buffers
 
+import android.arch.lifecycle.Observer
 import android.arch.lifecycle.ViewModelProviders
 import android.os.Bundle
 import android.support.v7.widget.*
@@ -7,10 +8,13 @@ import android.view.*
 import android.widget.AdapterView
 import butterknife.BindView
 import butterknife.ButterKnife
+import com.afollestad.materialdialogs.MaterialDialog
 import de.kuschku.libquassel.protocol.BufferId
 import de.kuschku.libquassel.protocol.Buffer_Activity
 import de.kuschku.libquassel.protocol.Buffer_Type
 import de.kuschku.libquassel.protocol.Message_Type
+import de.kuschku.libquassel.quassel.BufferInfo
+import de.kuschku.libquassel.quassel.syncables.interfaces.INetwork
 import de.kuschku.libquassel.util.hasFlag
 import de.kuschku.libquassel.util.minus
 import de.kuschku.quasseldroid_ng.R
@@ -42,15 +46,63 @@ class BufferViewConfigFragment : ServiceBoundFragment() {
   private var ircFormatDeserializer: IrcFormatDeserializer? = null
   private lateinit var appearanceSettings: AppearanceSettings
 
-  private var isInActionMode = false
+  private var actionMode: ActionMode? = null
 
   private val actionModeCallback = object : ActionMode.Callback {
     override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
-      return true
+      val selected = viewModel.selectedBuffer.value
+      val info = selected?.info
+      val session = viewModel.session.value
+      val network = session?.networks?.get(selected?.info?.networkId)
+
+      return if (info != null && session != null) {
+        when (item?.itemId) {
+          R.id.action_connect    -> {
+            network?.requestConnect()
+            actionMode?.finish()
+            true
+          }
+          R.id.action_disconnect -> {
+            network?.requestDisconnect()
+            actionMode?.finish()
+            true
+          }
+          R.id.action_join       -> {
+            session.rpcHandler?.sendInput(info, "/join ${info.bufferName}")
+            actionMode?.finish()
+            true
+          }
+          R.id.action_part       -> {
+            session.rpcHandler?.sendInput(info, "/part ${info.bufferName}")
+            actionMode?.finish()
+            true
+          }
+          R.id.action_delete     -> {
+            MaterialDialog.Builder(activity!!)
+              .content(R.string.buffer_delete_confirmation)
+              .positiveText(R.string.label_yes)
+              .negativeText(R.string.label_no)
+              .negativeColorAttr(R.attr.colorTextPrimary)
+              .backgroundColorAttr(R.attr.colorBackgroundCard)
+              .contentColorAttr(R.attr.colorTextPrimary)
+              .onPositive { _, _ ->
+                session.bufferSyncer?.requestRemoveBuffer(selected.info.bufferId)
+              }
+              .build()
+              .show()
+            actionMode?.finish()
+            true
+          }
+          else                   -> false
+        }
+      } else {
+        false
+      }
     }
 
     override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
-      isInActionMode = true
+      actionMode = mode
+      mode?.menuInflater?.inflate(R.menu.context_buffer, menu)
       return true
     }
 
@@ -59,7 +111,7 @@ class BufferViewConfigFragment : ServiceBoundFragment() {
     }
 
     override fun onDestroyActionMode(mode: ActionMode?) {
-      isInActionMode = false
+      actionMode = null
       listAdapter.unselectAll()
     }
   }
@@ -127,6 +179,7 @@ class BufferViewConfigFragment : ServiceBoundFragment() {
             props.info.type.hasFlag(Buffer_Type.StatusBuffer)
           }
       },
+      viewModel.selectedBufferId,
       handlerThread::post,
       activity!!::runOnUiThread,
       clickListener,
@@ -134,6 +187,55 @@ class BufferViewConfigFragment : ServiceBoundFragment() {
     )
     chatList.adapter = listAdapter
 
+    viewModel.selectedBuffer.observe(this, Observer { buffer ->
+      if (buffer != null) {
+        val menu = actionMode?.menu
+        if (menu != null) {
+          val allActions = setOf(
+            R.id.action_connect,
+            R.id.action_disconnect,
+            R.id.action_join,
+            R.id.action_part,
+            R.id.action_delete
+          )
+
+          val availableActions = when (buffer.info?.type?.enabledValues()?.firstOrNull()) {
+            Buffer_Type.StatusBuffer  -> {
+              when (buffer.connectionState) {
+                INetwork.ConnectionState.Disconnected -> setOf(R.id.action_connect)
+                INetwork.ConnectionState.Initialized  -> setOf(R.id.action_disconnect)
+                else                                  -> setOf(
+                  R.id.action_connect, R.id.action_disconnect
+                )
+              }
+            }
+            Buffer_Type.ChannelBuffer -> {
+              if (buffer.joined) {
+                setOf(R.id.action_part)
+              } else {
+                setOf(R.id.action_join, R.id.action_delete)
+              }
+            }
+            Buffer_Type.QueryBuffer   -> {
+              setOf(R.id.action_delete)
+            }
+            else                      -> emptySet()
+          }
+
+          val unavailableActions = allActions - availableActions
+
+          for (action in availableActions) {
+            menu.findItem(action)?.isVisible = true
+          }
+          for (action in unavailableActions) {
+            menu.findItem(action)?.isVisible = false
+          }
+        }
+      } else {
+        actionMode?.finish()
+      }
+    })
+
     chatListToolbar.startActionMode(actionModeCallback)
     chatList.layoutManager = LinearLayoutManager(context)
     chatList.itemAnimator = DefaultItemAnimator()
@@ -147,7 +249,7 @@ class BufferViewConfigFragment : ServiceBoundFragment() {
   }
 
   private val clickListener: ((BufferId) -> Unit)? = {
-    if (isInActionMode) {
+    if (actionMode != null) {
       longClickListener?.invoke(it)
     } else {
       viewModel.setBuffer(it)
@@ -155,9 +257,15 @@ class BufferViewConfigFragment : ServiceBoundFragment() {
   }
 
   private val longClickListener: ((BufferId) -> Unit)? = {
-    if (!isInActionMode) {
+    if (actionMode == null) {
       chatListToolbar.startActionMode(actionModeCallback)
     }
     listAdapter.toggleSelection(it)
   }
+
+  data class SelectedItem(
+    val info: BufferInfo? = null,
+    val connectionState: INetwork.ConnectionState = INetwork.ConnectionState.Disconnected,
+    val joined: Boolean = false
+  )
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/viewmodel/QuasselViewModel.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/viewmodel/QuasselViewModel.kt
index d1214a1e6..eff40a23f 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/viewmodel/QuasselViewModel.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/viewmodel/QuasselViewModel.kt
@@ -17,6 +17,7 @@ import de.kuschku.libquassel.util.hasFlag
 import de.kuschku.quasseldroid_ng.ui.chat.NickListAdapter
 import de.kuschku.quasseldroid_ng.ui.chat.ToolbarFragment
 import de.kuschku.quasseldroid_ng.ui.chat.buffers.BufferListAdapter
+import de.kuschku.quasseldroid_ng.ui.chat.buffers.BufferViewConfigFragment
 import de.kuschku.quasseldroid_ng.util.helper.map
 import de.kuschku.quasseldroid_ng.util.helper.switchMap
 import de.kuschku.quasseldroid_ng.util.helper.switchMapRx
@@ -191,6 +192,40 @@ class QuasselViewModel : ViewModel() {
     }
   }
 
+  val selectedBufferId = MutableLiveData<BufferId>()
+  val selectedBuffer = session.zip(selectedBufferId).switchMapRx { (session, buffer) ->
+    val bufferSyncer = session?.bufferSyncer
+    if (bufferSyncer != null) {
+      val info = bufferSyncer.bufferInfo(buffer)
+      if (info != null) {
+        val network = session.networks[info.networkId]
+        when (info.type.enabledValues().firstOrNull()) {
+          Buffer_Type.StatusBuffer  -> {
+            network?.liveConnectionState?.map {
+              BufferViewConfigFragment.SelectedItem(
+                info,
+                connectionState = it
+              )
+            }
+          }
+          Buffer_Type.ChannelBuffer -> {
+            network?.liveIrcChannel(info.bufferName)?.map {
+              BufferViewConfigFragment.SelectedItem(
+                info,
+                joined = it != IrcChannel.NULL
+              )
+            }
+          }
+          else                      -> Observable.just(BufferViewConfigFragment.SelectedItem(info))
+        }
+      } else {
+        Observable.just(BufferViewConfigFragment.SelectedItem(info))
+      }
+    } else {
+      Observable.just(BufferViewConfigFragment.SelectedItem())
+    }
+  }
+
   val bufferList: LiveData<Pair<BufferViewConfig?, List<BufferListAdapter.BufferProps>>?> = session.zip(
     bufferViewConfig
   ).switchMapRx { (session, config) ->
@@ -221,7 +256,7 @@ class QuasselViewModel : ViewModel() {
                     }
                   }.switchMap { (activity, highlights) ->
                       when (info.type.toInt()) {
-                        BufferInfo.Type.QueryBuffer.toInt() -> {
+                        BufferInfo.Type.QueryBuffer.toInt()   -> {
                           network.liveIrcUser(info.bufferName).switchMap { user ->
                             user.live_away.switchMap { away ->
                               user.live_realName.map { realName ->
@@ -260,7 +295,7 @@ class QuasselViewModel : ViewModel() {
                             }
                           }
                         }
-                        BufferInfo.Type.StatusBuffer.toInt() -> {
+                        BufferInfo.Type.StatusBuffer.toInt()  -> {
                           network.liveConnectionState.map {
                             BufferListAdapter.BufferProps(
                               info = info,
@@ -272,7 +307,7 @@ class QuasselViewModel : ViewModel() {
                             )
                           }
                         }
-                        else -> Observable.just(
+                        else                                  -> Observable.just(
                           BufferListAdapter.BufferProps(
                             info = info,
                             network = network.networkInfo(),
diff --git a/app/src/main/res/menu/context_buffer.xml b/app/src/main/res/menu/context_buffer.xml
new file mode 100644
index 000000000..08152e25c
--- /dev/null
+++ b/app/src/main/res/menu/context_buffer.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+  <item
+    android:id="@+id/action_connect"
+    android:title="@string/label_connect" />
+  <item
+    android:id="@+id/action_disconnect"
+    android:title="@string/label_disconnect" />
+  <item
+    android:id="@+id/action_join"
+    android:title="@string/label_join" />
+  <item
+    android:id="@+id/action_part"
+    android:title="@string/label_part" />
+  <item
+    android:id="@+id/action_delete"
+    android:title="@string/label_delete" />
+  <!--<item android:title="Merge" />-->
+</menu>
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 62902007f..dd5bb3719 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -6,12 +6,15 @@
 
   <string name="label_cancel">Cancel</string>
   <string name="label_clear_backlog">Clear Backlog</string>
+  <string name="label_close">Close</string>
+  <string name="label_connect">Connect</string>
   <string name="label_delete">Delete</string>
   <string name="label_disconnect">Disconnect</string>
-  <string name="label_close">Close</string>
-  <string name="label_open">Open</string>
   <string name="label_filter_messages">Filter Messages</string>
   <string name="label_input_history">Input History</string>
+  <string name="label_join">Join</string>
+  <string name="label_open">Open</string>
+  <string name="label_part">Part</string>
   <string name="label_placeholder">Write a message…</string>
   <string name="label_save">Save</string>
   <string name="label_select_multiple">Select</string>
@@ -27,4 +30,8 @@
   <string name="notification_channel_background_title">Background</string>
   <string name="notification_channel_highlight" translatable="false">highlight</string>
   <string name="notification_channel_highlight_title">Highlight</string>
+
+  <string name="buffer_delete_confirmation">Do you want to delete this buffer permanently?</string>
+  <string name="label_yes">Yes</string>
+  <string name="label_no">No</string>
 </resources>
diff --git a/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/Network.kt b/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/Network.kt
index 680286716..246741dd4 100644
--- a/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/Network.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/Network.kt
@@ -434,6 +434,7 @@ class Network constructor(
     if (_connectionState == actualConnectionState)
       return
     _connectionState = actualConnectionState
+    liveConnectionState.onNext(_connectionState)
     super.setConnectionState(state)
   }
 
-- 
GitLab