Implement chat archive screen

parent 5b618e2d
Pipeline #483 canceled with stages
in 9 minutes and 34 seconds
......@@ -26,10 +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
import de.kuschku.quasseldroid.viewmodel.QueryCreateViewModel
import de.kuschku.quasseldroid.viewmodel.*
@Module
object ActivityBaseModule {
......@@ -72,4 +69,10 @@ object ActivityBaseModule {
@JvmStatic
fun provideQueryCreateViewModel(viewModelProvider: ViewModelProvider) =
viewModelProvider[QueryCreateViewModel::class.java]
@ActivityScope
@Provides
@JvmStatic
fun provideArchiveViewModel(viewModelProvider: ViewModelProvider) =
viewModelProvider[ArchiveViewModel::class.java]
}
......@@ -20,15 +20,32 @@
package de.kuschku.quasseldroid.ui.chat.archive
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.*
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView
import butterknife.ButterKnife
import com.afollestad.materialdialogs.MaterialDialog
import de.kuschku.libquassel.protocol.BufferId
import de.kuschku.libquassel.util.helper.combineLatest
import de.kuschku.libquassel.util.helper.value
import de.kuschku.quasseldroid.R
import de.kuschku.quasseldroid.persistence.db.AccountDatabase
import de.kuschku.quasseldroid.persistence.db.QuasselDatabase
import de.kuschku.quasseldroid.settings.MessageSettings
import de.kuschku.quasseldroid.ui.chat.ChatActivity
import de.kuschku.quasseldroid.ui.chat.buffers.BufferListAdapter
import de.kuschku.quasseldroid.ui.coresettings.network.NetworkEditActivity
import de.kuschku.quasseldroid.ui.info.channellist.ChannelListActivity
import de.kuschku.quasseldroid.util.helper.toLiveData
import de.kuschku.quasseldroid.util.service.ServiceBoundFragment
import de.kuschku.quasseldroid.viewmodel.helper.QuasselViewModelHelper
import de.kuschku.quasseldroid.util.ui.presenter.BufferContextPresenter
import de.kuschku.quasseldroid.util.ui.presenter.BufferPresenter
import de.kuschku.quasseldroid.viewmodel.data.BufferHiddenState
import de.kuschku.quasseldroid.viewmodel.helper.ArchiveViewModelHelper
import javax.inject.Inject
class ArchiveFragment : ServiceBoundFragment() {
......@@ -39,19 +56,149 @@ class ArchiveFragment : ServiceBoundFragment() {
lateinit var listPermanently: RecyclerView
@Inject
lateinit var modelHelper: QuasselViewModelHelper
lateinit var modelHelper: ArchiveViewModelHelper
@Inject
lateinit var database: QuasselDatabase
@Inject
lateinit var accountDatabase: AccountDatabase
@Inject
lateinit var bufferPresenter: BufferPresenter
@Inject
lateinit var messageSettings: MessageSettings
private lateinit var listTemporaryAdapter: BufferListAdapter
private lateinit var listPermanentlyAdapter: BufferListAdapter
private var actionMode: ActionMode? = null
private val actionModeCallback = object : ActionMode.Callback {
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
val selected = modelHelper.archive.selectedBufferId.value ?: BufferId(-1)
val session = modelHelper.connectedSession.value?.orNull()
val bufferSyncer = session?.bufferSyncer
val info = bufferSyncer?.bufferInfo(selected)
val network = session?.networks?.get(info?.networkId)
val bufferViewConfig = modelHelper.bufferViewConfig.value?.orNull()
return if (info != null) {
BufferContextPresenter.handleAction(
requireContext(),
mode,
item,
info,
session,
bufferSyncer,
bufferViewConfig,
network
)
} else {
false
}
}
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
actionMode = mode
mode?.menuInflater?.inflate(R.menu.context_buffer, menu)
return true
}
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
mode?.tag = "ARCHIVE"
return true
}
override fun onDestroyActionMode(mode: ActionMode?) {
actionMode = null
listTemporaryAdapter.unselectAll()
listPermanentlyAdapter.unselectAll()
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.chat_archive, container, false)
ButterKnife.bind(this, view)
val chatlistId = arguments?.getInt("chatlist_id", -1)
val chatlistId = arguments?.getInt("chatlist_id", -1) ?: -1
modelHelper.archive.bufferViewConfigId.onNext(chatlistId)
val chatlist = modelHelper.bufferViewConfigMap.map {
it[chatlistId]
}
listTemporaryAdapter = BufferListAdapter(
messageSettings,
modelHelper.archive.selectedBufferId,
modelHelper.archive.temporarilyExpandedNetworks
)
listTemporaryAdapter.setOnClickListener(::clickListener)
listTemporaryAdapter.setOnLongClickListener(::longClickListener)
listTemporary.adapter = listTemporaryAdapter
listTemporary.layoutManager = LinearLayoutManager(listTemporary.context)
listTemporary.itemAnimator = DefaultItemAnimator()
listPermanentlyAdapter = BufferListAdapter(
messageSettings,
modelHelper.archive.selectedBufferId,
modelHelper.archive.permanentlyExpandedNetworks
)
listPermanentlyAdapter.setOnClickListener(::clickListener)
listPermanentlyAdapter.setOnLongClickListener(::longClickListener)
listPermanently.adapter = listPermanentlyAdapter
listPermanently.layoutManager = LinearLayoutManager(listPermanently.context)
listPermanently.itemAnimator = DefaultItemAnimator()
fun processArchiveBufferList(bufferListType: BufferHiddenState, showHandle: Boolean) =
combineLatest(
modelHelper.processArchiveBufferList(bufferListType, showHandle),
database.filtered().listenRx(accountId).toObservable(),
accountDatabase.accounts().listenDefaultFiltered(accountId, 0).toObservable()
).map { (buffers, filteredList, defaultFiltered) ->
bufferPresenter.render(buffers, filteredList, defaultFiltered.toUInt())
}
processArchiveBufferList(BufferHiddenState.HIDDEN_TEMPORARY, false)
.toLiveData().observe(this, Observer { processedList ->
listTemporaryAdapter.submitList(processedList)
})
processArchiveBufferList(BufferHiddenState.HIDDEN_PERMANENT, false)
.toLiveData().observe(this, Observer { processedList ->
listPermanentlyAdapter.submitList(processedList)
})
modelHelper.selectedBuffer.toLiveData().observe(this, Observer { buffer ->
actionMode?.let {
BufferContextPresenter.present(it, buffer)
}
})
return view
}
private fun toggleSelection(buffer: BufferId): Boolean {
val next = if (modelHelper.archive.selectedBufferId.value == buffer) BufferId.MAX_VALUE else buffer
modelHelper.archive.selectedBufferId.onNext(next)
return next != BufferId.MAX_VALUE
}
private fun clickListener(bufferId: BufferId) {
if (actionMode != null) {
longClickListener(bufferId)
} else {
context?.let {
ChatActivity.launch(it, bufferId = bufferId)
}
}
}
private fun longClickListener(it: BufferId) {
if (actionMode == null) {
(activity as? AppCompatActivity)?.startActionMode(actionModeCallback)
}
if (!toggleSelection(it)) {
actionMode?.finish()
}
}
}
......@@ -21,6 +21,7 @@ package de.kuschku.quasseldroid.ui.chat.buffers
import android.graphics.drawable.Drawable
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
......@@ -61,16 +62,21 @@ class BufferListAdapter(
override fun getSectionName(position: Int) = getItem(position).props.network.networkName
private var clickListener: ((BufferId) -> Unit)? = null
private var longClickListener: ((BufferId) -> Unit)? = null
private var updateFinishedListener: ((List<BufferListItem>) -> Unit)? = null
fun setOnClickListener(listener: ((BufferId) -> Unit)?) {
this.clickListener = listener
}
private var longClickListener: ((BufferId) -> Unit)? = null
fun setOnLongClickListener(listener: ((BufferId) -> Unit)?) {
this.longClickListener = listener
}
private var dragListener: ((BufferViewHolder) -> Unit)? = null
fun setOnDragListener(listener: ((BufferViewHolder) -> Unit)?) {
dragListener = listener
}
private var updateFinishedListener: ((List<BufferListItem>) -> Unit)? = null
fun setOnUpdateFinishedListener(listener: ((List<BufferListItem>) -> Unit)?) {
this.updateFinishedListener = listener
}
......@@ -83,12 +89,6 @@ class BufferListAdapter(
expandedNetworks.onNext(expandedNetworks.value.orEmpty() + Pair(networkId, expand))
}
fun toggleSelection(buffer: BufferId): Boolean {
val next = if (selectedBuffer.value == buffer) BufferId.MAX_VALUE else buffer
selectedBuffer.onNext(next)
return next != BufferId.MAX_VALUE
}
fun unselectAll() {
selectedBuffer.onNext(BufferId.MAX_VALUE)
}
......@@ -102,7 +102,8 @@ class BufferListAdapter(
R.layout.widget_buffer, parent, false
),
clickListener = clickListener,
longClickListener = longClickListener
longClickListener = longClickListener,
dragListener = dragListener
)
BufferInfo.Type.QueryBuffer.toInt() -> BufferViewHolder.QueryBuffer(
LayoutInflater.from(parent.context).inflate(
......@@ -111,14 +112,16 @@ class BufferListAdapter(
, parent, false
),
clickListener = clickListener,
longClickListener = longClickListener
longClickListener = longClickListener,
dragListener = dragListener
)
BufferInfo.Type.GroupBuffer.toInt() -> BufferViewHolder.GroupBuffer(
LayoutInflater.from(parent.context).inflate(
R.layout.widget_buffer, parent, false
),
clickListener = clickListener,
longClickListener = longClickListener
longClickListener = longClickListener,
dragListener = dragListener
)
BufferInfo.Type.StatusBuffer.toInt() -> BufferViewHolder.StatusBuffer(
LayoutInflater.from(parent.context).inflate(
......@@ -230,7 +233,8 @@ class BufferListAdapter(
class GroupBuffer(
itemView: View,
private val clickListener: ((BufferId) -> Unit)? = null,
private val longClickListener: ((BufferId) -> Unit)? = null
private val longClickListener: ((BufferId) -> Unit)? = null,
private val dragListener: ((BufferViewHolder) -> Unit)? = null
) : BufferViewHolder(itemView) {
@BindView(R.id.status)
lateinit var status: ImageView
......@@ -241,6 +245,9 @@ class BufferListAdapter(
@BindView(R.id.description)
lateinit var description: TextView
@BindView(R.id.handle)
lateinit var handle: View
var bufferId: BufferId? = null
private val online: Drawable?
......@@ -269,6 +276,13 @@ class BufferListAdapter(
}
}
handle.setOnTouchListener { _, event ->
if (event.action == MotionEvent.ACTION_DOWN) {
dragListener?.invoke(this)
}
false
}
online = itemView.context.getVectorDrawableCompat(R.drawable.ic_status)?.mutate()
offline = itemView.context.getVectorDrawableCompat(R.drawable.ic_status_offline)?.mutate()
......@@ -304,6 +318,8 @@ class BufferListAdapter(
itemView.isSelected = state.selected
handle.visibleIf(state.showHandle)
description.visibleIf(props.description.isNotBlank())
status.setImageDrawable(
......@@ -318,7 +334,8 @@ class BufferListAdapter(
class ChannelBuffer(
itemView: View,
private val clickListener: ((BufferId) -> Unit)? = null,
private val longClickListener: ((BufferId) -> Unit)? = null
private val longClickListener: ((BufferId) -> Unit)? = null,
private val dragListener: ((BufferViewHolder) -> Unit)? = null
) : BufferViewHolder(itemView) {
@BindView(R.id.status)
lateinit var status: ImageView
......@@ -329,6 +346,9 @@ class BufferListAdapter(
@BindView(R.id.description)
lateinit var description: TextView
@BindView(R.id.handle)
lateinit var handle: View
var bufferId: BufferId? = null
private var none: Int = 0
......@@ -354,6 +374,13 @@ class BufferListAdapter(
}
}
handle.setOnTouchListener { _, event ->
if (event.action == MotionEvent.ACTION_DOWN) {
dragListener?.invoke(this)
}
false
}
itemView.context.theme.styledAttributes(
R.attr.colorTextPrimary, R.attr.colorTintActivity, R.attr.colorTintMessage,
R.attr.colorTintHighlight
......@@ -382,6 +409,8 @@ class BufferListAdapter(
itemView.isSelected = state.selected
handle.visibleIf(state.showHandle)
description.visibleIf(props.description.isNotBlank())
status.setImageDrawable(props.fallbackDrawable)
......@@ -391,7 +420,8 @@ class BufferListAdapter(
class QueryBuffer(
itemView: View,
private val clickListener: ((BufferId) -> Unit)? = null,
private val longClickListener: ((BufferId) -> Unit)? = null
private val longClickListener: ((BufferId) -> Unit)? = null,
private val dragListener: ((BufferViewHolder) -> Unit)? = null
) : BufferViewHolder(itemView) {
@BindView(R.id.status)
lateinit var status: ImageView
......@@ -402,6 +432,9 @@ class BufferListAdapter(
@BindView(R.id.description)
lateinit var description: TextView
@BindView(R.id.handle)
lateinit var handle: View
var bufferId: BufferId? = null
private var none: Int = 0
......@@ -427,6 +460,13 @@ class BufferListAdapter(
}
}
handle.setOnTouchListener { _, event ->
if (event.action == MotionEvent.ACTION_DOWN) {
dragListener?.invoke(this)
}
false
}
itemView.context.theme.styledAttributes(
R.attr.colorTextPrimary, R.attr.colorTintActivity, R.attr.colorTintMessage,
R.attr.colorTintHighlight
......@@ -455,6 +495,8 @@ class BufferListAdapter(
itemView.isSelected = state.selected
handle.visibleIf(state.showHandle)
description.visibleIf(props.description.isNotBlank())
status.loadAvatars(props.avatarUrls,
......
......@@ -53,19 +53,27 @@ class ColorContext @Inject constructor(
getColor(0, 0)
}
private val radius = context.resources.getDimensionPixelSize(R.dimen.avatar_radius)
val colorAccent = context.theme.styledAttributes(R.attr.colorAccent) {
getColor(0, 0)
}
val colorAway = context.theme.styledAttributes(R.attr.colorAway) {
getColor(0, 0)
}
val avatarRadius = context.resources.getDimensionPixelSize(R.dimen.avatar_radius)
val avatarSize = context.resources.getDimensionPixelSize(R.dimen.avatar_size_buffer)
fun prepareTextDrawable(@ColorInt textColor: Int = this.textColor) =
fun prepareTextDrawable(@ColorInt textColor: Int = this.textColor): TextDrawable.IShapeBuilder =
TextDrawable.builder()
.beginConfig()
.textColor(setAlpha(textColor, 0x8A))
.useFont(Typeface.DEFAULT_BOLD)
.endConfig()
fun buildTextDrawable(initial: String, @ColorInt backgroundColor: Int) =
fun buildTextDrawable(initial: String, @ColorInt backgroundColor: Int): TextDrawable =
prepareTextDrawable(textColor).let {
if (messageSettings.squareAvatars) it.buildRoundRect(initial, backgroundColor, radius)
if (messageSettings.squareAvatars) it.buildRoundRect(initial, backgroundColor, avatarRadius)
else it.buildRound(initial, backgroundColor)
}
......@@ -79,6 +87,8 @@ class ColorContext @Inject constructor(
return buildTextDrawable(initial, senderColor)
}
@ColorInt
private fun setAlpha(@ColorInt color: Int, alpha: Int) = (color and 0xFFFFFF) or (alpha shl 24)
companion object {
@ColorInt
private fun setAlpha(@ColorInt color: Int, alpha: Int) = (color and 0xFFFFFF) or (alpha shl 24)
}
}
/*
* Quasseldroid - Quassel client for Android
*
* Copyright (c) 2019 Janne Mareike Koschinski
* Copyright (c) 2019 The Quassel Project
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 3 as published
* by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package de.kuschku.quasseldroid.util.ui.presenter
import android.content.Context
import android.view.ActionMode
import android.view.MenuItem
import com.afollestad.materialdialogs.MaterialDialog
import de.kuschku.libquassel.protocol.Buffer_Type
import de.kuschku.libquassel.quassel.BufferInfo
import de.kuschku.libquassel.quassel.syncables.BufferSyncer
import de.kuschku.libquassel.quassel.syncables.BufferViewConfig
import de.kuschku.libquassel.quassel.syncables.Network
import de.kuschku.libquassel.quassel.syncables.interfaces.INetwork
import de.kuschku.libquassel.session.ISession
import de.kuschku.quasseldroid.R
import de.kuschku.quasseldroid.ui.coresettings.network.NetworkEditActivity
import de.kuschku.quasseldroid.ui.info.channellist.ChannelListActivity
import de.kuschku.quasseldroid.viewmodel.data.BufferHiddenState
import de.kuschku.quasseldroid.viewmodel.data.SelectedBufferItem
object BufferContextPresenter {
fun present(actionMode: ActionMode, buffer: SelectedBufferItem?) {
if (buffer != null) {
val menu = actionMode.menu
if (menu != null) {
val allActions = setOf(
R.id.action_channellist,
R.id.action_configure,
R.id.action_connect,
R.id.action_disconnect,
R.id.action_join,
R.id.action_part,
R.id.action_delete,
R.id.action_rename,
R.id.action_unhide,
R.id.action_archive
)
val visibilityActions = when (buffer.hiddenState) {
BufferHiddenState.VISIBLE -> setOf(
R.id.action_archive
)
BufferHiddenState.HIDDEN_TEMPORARY -> setOf(
R.id.action_unhide
)
BufferHiddenState.HIDDEN_PERMANENT -> setOf(
R.id.action_unhide
)
}
val availableActions = when (buffer.info?.type?.enabledValues()?.firstOrNull()) {
Buffer_Type.StatusBuffer -> {
when (buffer.connectionState) {
INetwork.ConnectionState.Disconnected -> setOf(
R.id.action_configure, R.id.action_connect
)
INetwork.ConnectionState.Initialized -> setOf(
R.id.action_channellist, R.id.action_configure, R.id.action_disconnect
)
else -> setOf(
R.id.action_configure, 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)
} + visibilityActions
}
Buffer_Type.QueryBuffer -> {
setOf(R.id.action_delete, R.id.action_rename) + visibilityActions
}
else -> visibilityActions
}
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()
}
}
fun handleAction(
context: Context,
actionMode: ActionMode,
item: MenuItem,
info: BufferInfo,
session: ISession,
bufferSyncer: BufferSyncer,
bufferViewConfig: BufferViewConfig?,
network: Network?
) = when (item.itemId) {
R.id.action_channellist -> {
network?.let {
ChannelListActivity.launch(context, network = it.networkId())
}
actionMode.finish()
true
}
R.id.action_configure -> {
network?.let {
NetworkEditActivity.launch(context, network = it.networkId())
}
actionMode.finish()
true
}
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(context)
.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(info.bufferId)
}
.onAny { _, _ ->
actionMode.finish()
}
.build()
.show()
true
}
R.id.action_rename -> {
MaterialDialog.Builder(context)
.input(
context.getString(R.string.label_buffer_name),
info.bufferName,
false
) { _, input ->
session.bufferSyncer.requestRenameBuffer(info.bufferId, input.toString())
}
.positiveText(R.string.label_save)
.negativeText(R.string.label_cancel)
.negativeColorAttr(R.attr.colorTextPrimary)
.backgroundColorAttr(R.attr.colorBackgroundCard)
.contentColorAttr(R.attr.colorTextPrimary)
.onAny { _, _ ->
actionMode.finish()
}
.build()
.show()
true
}
R.id.action_unhide -> {
bufferViewConfig?.insertBufferSorted(info, bufferSyncer)
actionMode.finish()
true
}
R.id.action_archive -> {
MaterialDialog.Builder(context)
.title(R.string.label_archive_chat)
.content(R.string.buffer_archive_confirmation)
.checkBoxPromptRes(R.string.buffer_archive_temporarily, true, null)
.positiveText(R.string.label_archive)
.negativeText(R.string.label_cancel)
.negativeColorAttr(R.attr.colorTextPrimary)
.backgroundColorAttr(R.attr.colorBackgroundCard)
.contentColorAttr(R.attr.colorTextPrimary)
.onAny { _, _ ->
actionMode.finish()
}
.onPositive { dialog, _ ->
if (dialog.isPromptCheckBoxChecked) {
bufferViewConfig?.requestRemoveBuffer(info.bufferId)