From bfcb2b974ecde20aab48259e7b62b2d12432df4a Mon Sep 17 00:00:00 2001 From: Janne Koschinski <janne@kuschku.de> Date: Mon, 20 May 2019 15:42:50 +0200 Subject: [PATCH] Implement chat archive screen --- .../quasseldroid/dagger/ActivityBaseModule.kt | 11 +- .../ui/chat/archive/ArchiveFragment.kt | 165 ++++++++- .../ui/chat/buffers/BufferListAdapter.kt | 70 +++- .../chat/buffers/BufferViewConfigFragment.kt | 323 +++--------------- .../kuschku/quasseldroid/util/ColorContext.kt | 22 +- .../ui/presenter/BufferContextPresenter.kt | 224 ++++++++++++ .../util/ui/presenter/BufferPresenter.kt | 101 ++++++ app/src/main/res/layout/chat_archive.xml | 9 +- app/src/main/res/layout/widget_buffer.xml | 15 +- .../main/res/layout/widget_buffer_away.xml | 11 + .../main/res/layout/widget_buffer_reorder.xml | 89 ----- .../res/layout/widget_buffer_reorder_away.xml | 101 ------ app/src/main/res/menu/context_buffer.xml | 8 +- app/src/main/res/menu/context_bufferlist.xml | 4 - app/src/main/res/values/strings.xml | 11 +- .../viewmodel/ArchiveViewModel.kt | 88 +++++ .../viewmodel/data/BufferState.kt | 3 +- .../helper/ArchiveViewModelHelper.kt | 186 ++++++++++ .../viewmodel/helper/ChatViewModelHelper.kt | 177 +--------- .../helper/QuasselViewModelHelper.kt | 210 +++++++++++- 20 files changed, 1149 insertions(+), 679 deletions(-) create mode 100644 app/src/main/java/de/kuschku/quasseldroid/util/ui/presenter/BufferContextPresenter.kt create mode 100644 app/src/main/java/de/kuschku/quasseldroid/util/ui/presenter/BufferPresenter.kt delete mode 100644 app/src/main/res/layout/widget_buffer_reorder.xml delete mode 100644 app/src/main/res/layout/widget_buffer_reorder_away.xml create mode 100644 viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/ArchiveViewModel.kt create mode 100644 viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/helper/ArchiveViewModelHelper.kt 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 aded7fa43..cf50a65a6 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/dagger/ActivityBaseModule.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/dagger/ActivityBaseModule.kt @@ -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] } diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/archive/ArchiveFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/archive/ArchiveFragment.kt index 8baf03ba0..ce962ebdc 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/archive/ArchiveFragment.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/archive/ArchiveFragment.kt @@ -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() + } + } } diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferListAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferListAdapter.kt index 35f7c0a67..128d769dd 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferListAdapter.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferListAdapter.kt @@ -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, 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 d316c46e5..f57e2ad30 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 @@ -40,15 +40,8 @@ import com.afollestad.materialdialogs.MaterialDialog import com.leinardi.android.speeddial.SpeedDialActionItem import com.leinardi.android.speeddial.SpeedDialView 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.ExtendedFeature import de.kuschku.libquassel.quassel.syncables.BufferViewConfig -import de.kuschku.libquassel.quassel.syncables.interfaces.INetwork -import de.kuschku.libquassel.util.flag.hasFlag -import de.kuschku.libquassel.util.flag.minus import de.kuschku.libquassel.util.helper.* import de.kuschku.quasseldroid.BuildConfig import de.kuschku.quasseldroid.R @@ -64,18 +57,15 @@ import de.kuschku.quasseldroid.ui.chat.archive.ArchiveActivity import de.kuschku.quasseldroid.ui.coresettings.network.NetworkEditActivity import de.kuschku.quasseldroid.ui.info.channellist.ChannelListActivity import de.kuschku.quasseldroid.util.ColorContext -import de.kuschku.quasseldroid.util.avatars.AvatarHelper import de.kuschku.quasseldroid.util.helper.setTooltip import de.kuschku.quasseldroid.util.helper.styledAttributes import de.kuschku.quasseldroid.util.helper.toLiveData import de.kuschku.quasseldroid.util.helper.visibleIf import de.kuschku.quasseldroid.util.irc.format.IrcFormatDeserializer import de.kuschku.quasseldroid.util.service.ServiceBoundFragment +import de.kuschku.quasseldroid.util.ui.presenter.BufferContextPresenter +import de.kuschku.quasseldroid.util.ui.presenter.BufferPresenter import de.kuschku.quasseldroid.util.ui.view.WarningBarView -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 @@ -122,115 +112,34 @@ class BufferViewConfigFragment : ServiceBoundFragment() { @Inject lateinit var modelHelper: ChatViewModelHelper + @Inject + lateinit var colorContext: ColorContext + + @Inject + lateinit var bufferPresenter: BufferPresenter + private var actionMode: ActionMode? = null private val actionModeCallback = object : ActionMode.Callback { - override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean { - val selected = modelHelper.selectedBuffer.value - val info = selected?.info + override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { + val selected = modelHelper.chat.selectedBufferId.value ?: BufferId(-1) val session = modelHelper.connectedSession.value?.orNull() val bufferSyncer = session?.bufferSyncer - val network = session?.networks?.get(selected?.info?.networkId) - val bufferViewConfig = modelHelper.bufferViewConfig.value - - return if (info != null && session != null) { - when (item?.itemId) { - R.id.action_channellist -> { - network?.let { - ChannelListActivity.launch(requireContext(), network = it.networkId()) - } - actionMode?.finish() - true - } - R.id.action_configure -> { - network?.let { - NetworkEditActivity.launch(requireContext(), 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(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 { _, _ -> - selected.info?.let { - session.bufferSyncer.requestRemoveBuffer(info.bufferId) - } - } - .onAny { _, _ -> - actionMode?.finish() - } - .build() - .show() - true - } - R.id.action_rename -> { - MaterialDialog.Builder(activity!!) - .input( - getString(R.string.label_buffer_name), - info.bufferName, - false - ) { _, input -> - selected.info?.let { - 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 -> { - bufferSyncer?.let { - bufferViewConfig?.orNull()?.insertBufferSorted(info, bufferSyncer) - } - actionMode?.finish() - true - } - R.id.action_hide_temp -> { - bufferViewConfig?.orNull()?.requestRemoveBuffer(info.bufferId) - actionMode?.finish() - true - } - R.id.action_hide_perm -> { - bufferViewConfig?.orNull()?.requestRemoveBufferPermanently(info.bufferId) - actionMode?.finish() - true - } - else -> false - } + 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 } @@ -300,18 +209,6 @@ class BufferViewConfigFragment : ServiceBoundFragment() { modelHelper.chat.expandedNetworks ) - val avatarSize = resources.getDimensionPixelSize(R.dimen.avatar_size_buffer) - - val colorContext = ColorContext(requireContext(), messageSettings) - - val colorAccent = requireContext().theme.styledAttributes(R.attr.colorAccent) { - getColor(0, 0) - } - - val colorAway = requireContext().theme.styledAttributes(R.attr.colorAway) { - getColor(0, 0) - } - var chatListState: Parcelable? = savedInstanceState?.getParcelable(KEY_STATE_LIST) var hasRestoredChatListState = false listAdapter.setOnUpdateFinishedListener { @@ -333,73 +230,11 @@ class BufferViewConfigFragment : ServiceBoundFragment() { }) combineLatest( - modelHelper.bufferList, - modelHelper.chat.expandedNetworks, - modelHelper.selectedBuffer, + modelHelper.processedBufferList, database.filtered().listenRx(accountId).toObservable(), accountDatabase.accounts().listenDefaultFiltered(accountId, 0).toObservable() - ).map { (info, expandedNetworks, selected, filteredList, defaultFiltered) -> - val (config, list) = info ?: Pair(null, emptyList()) - val minimumActivity = config?.minimumActivity() ?: Buffer_Activity.NONE - val activities = filteredList.associate { it.bufferId to it.filtered.toUInt() } - list.asSequence().sortedBy { props -> - !props.info.type.hasFlag(Buffer_Type.StatusBuffer) - }.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { props -> - props.network.networkName - }).map { props -> - val activity = props.activity - (activities[props.info.bufferId] - ?: defaultFiltered?.toUInt() - ?: 0u) - BufferListItem( - props.copy( - activity = activity, - description = ircFormatDeserializer.formatString( - props.description.toString(), - colorize = messageSettings.colorizeMirc - ), - bufferActivity = Buffer_Activity.of( - when { - props.highlights > 0 -> Buffer_Activity.Highlight - activity.hasFlag(Message_Type.Plain) || - activity.hasFlag(Message_Type.Notice) || - activity.hasFlag(Message_Type.Action) -> Buffer_Activity.NewMessage - activity.isNotEmpty() -> Buffer_Activity.OtherActivity - else -> Buffer_Activity.NoActivity - } - ), - fallbackDrawable = if (props.info.type.hasFlag(Buffer_Type.QueryBuffer)) { - props.ircUser?.let { - val nickName = it.nick() - val useSelfColor = when (messageSettings.colorizeNicknames) { - MessageSettings.ColorizeNicknamesMode.ALL -> false - MessageSettings.ColorizeNicknamesMode.ALL_BUT_MINE -> - props.ircUser?.network()?.isMyNick(nickName) == true - MessageSettings.ColorizeNicknamesMode.NONE -> true - } - - colorContext.buildTextDrawable(it.nick(), useSelfColor) - } ?: colorContext.buildTextDrawable("", colorAway) - } else { - val color = if (props.bufferStatus == BufferStatus.ONLINE) colorAccent - else colorAway - - colorContext.buildTextDrawable("#", color) - }, - avatarUrls = props.ircUser?.let { - AvatarHelper.avatar(messageSettings, it, avatarSize) - } ?: emptyList() - ), - BufferState( - networkExpanded = expandedNetworks[props.network.networkId] - ?: (props.networkConnectionState == INetwork.ConnectionState.Initialized), - selected = selected.info?.bufferId == props.info.bufferId - ) - ) - }.filter { (props, state) -> - (props.info.type.hasFlag(BufferInfo.Type.StatusBuffer) || state.networkExpanded) && - (minimumActivity.toInt() <= props.bufferActivity.toInt() || - props.info.type.hasFlag(Buffer_Type.StatusBuffer)) - }.toList() + ).map { (buffers, filteredList, defaultFiltered) -> + bufferPresenter.render(buffers, filteredList, defaultFiltered.toUInt()) }.toLiveData().observe(this, Observer { processedList -> if (hasRestoredChatListState) { chatListState = chatList.layoutManager?.onSaveInstanceState() @@ -407,81 +242,14 @@ class BufferViewConfigFragment : ServiceBoundFragment() { } listAdapter.submitList(processedList) }) + listAdapter.setOnClickListener(this@BufferViewConfigFragment::clickListener) listAdapter.setOnLongClickListener(this@BufferViewConfigFragment::longClickListener) chatList.adapter = listAdapter modelHelper.selectedBuffer.toLiveData().observe(this, Observer { buffer -> - 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_hide_temp, - R.id.action_hide_perm - ) - - val visibilityActions = when (buffer.hiddenState) { - BufferHiddenState.VISIBLE -> setOf( - R.id.action_hide_temp, - R.id.action_hide_perm - ) - BufferHiddenState.HIDDEN_TEMPORARY -> setOf( - R.id.action_unhide, - R.id.action_hide_perm - ) - BufferHiddenState.HIDDEN_PERMANENT -> setOf( - R.id.action_unhide, - R.id.action_hide_temp - ) - } - - 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() + actionMode?.let { + BufferContextPresenter.present(it, buffer) } }) @@ -491,21 +259,22 @@ class BufferViewConfigFragment : ServiceBoundFragment() { chatListToolbar.setOnMenuItemClickListener { item -> when (item.itemId) { R.id.action_archived_chats -> { - ArchiveActivity.launch(requireContext(), - chatlistId = modelHelper.chat.bufferViewConfigId.or(-1)) + context?.let { + modelHelper.chat.bufferViewConfigId.value?.let { chatlistId -> + ArchiveActivity.launch( + it, + chatlistId = chatlistId + ) + } + } true } - R.id.action_search -> { + R.id.action_search -> { item.isChecked = !item.isChecked modelHelper.chat.bufferSearchTemporarilyVisible.onNext(item.isChecked) true } - R.id.action_show_hidden -> { - item.isChecked = !item.isChecked - modelHelper.chat.showHidden.onNext(item.isChecked) - true - } - else -> false + else -> false } } chatList.layoutManager = LinearLayoutManager(context) @@ -600,7 +369,7 @@ class BufferViewConfigFragment : ServiceBoundFragment() { ) fab.setOnActionSelectedListener { - val networkId = modelHelper.bufferData?.value?.network?.networkId() + val networkId = modelHelper.bufferData.value?.network?.networkId() when (it.id) { R.id.fab_query -> { context?.let { @@ -637,6 +406,12 @@ class BufferViewConfigFragment : ServiceBoundFragment() { outState.putParcelable(KEY_STATE_LIST, chatList.layoutManager?.onSaveInstanceState()) } + private fun toggleSelection(buffer: BufferId): Boolean { + val next = if (modelHelper.chat.selectedBufferId.value == buffer) BufferId.MAX_VALUE else buffer + modelHelper.chat.selectedBufferId.onNext(next) + return next != BufferId.MAX_VALUE + } + private fun clickListener(bufferId: BufferId) { if (actionMode != null) { longClickListener(bufferId) @@ -652,7 +427,7 @@ class BufferViewConfigFragment : ServiceBoundFragment() { if (actionMode == null) { chatListToolbar.startActionMode(actionModeCallback) } - if (!listAdapter.toggleSelection(it)) { + if (!toggleSelection(it)) { actionMode?.finish() } } 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 048c49abc..eeedd6c3e 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/util/ColorContext.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/util/ColorContext.kt @@ -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) + } } diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/ui/presenter/BufferContextPresenter.kt b/app/src/main/java/de/kuschku/quasseldroid/util/ui/presenter/BufferContextPresenter.kt new file mode 100644 index 000000000..af99d3ab0 --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid/util/ui/presenter/BufferContextPresenter.kt @@ -0,0 +1,224 @@ +/* + * 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) + } else { + bufferViewConfig?.requestRemoveBufferPermanently(info.bufferId) + } + } + .build() + .show() + true + } + else -> false + } +} diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/ui/presenter/BufferPresenter.kt b/app/src/main/java/de/kuschku/quasseldroid/util/ui/presenter/BufferPresenter.kt new file mode 100644 index 000000000..7777d3499 --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid/util/ui/presenter/BufferPresenter.kt @@ -0,0 +1,101 @@ +/* + * 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 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.util.flag.hasFlag +import de.kuschku.libquassel.util.flag.minus +import de.kuschku.quasseldroid.persistence.models.Filtered +import de.kuschku.quasseldroid.settings.AppearanceSettings +import de.kuschku.quasseldroid.settings.MessageSettings +import de.kuschku.quasseldroid.util.ColorContext +import de.kuschku.quasseldroid.util.avatars.AvatarHelper +import de.kuschku.quasseldroid.util.irc.format.IrcFormatDeserializer +import de.kuschku.quasseldroid.viewmodel.data.BufferListItem +import de.kuschku.quasseldroid.viewmodel.data.BufferProps +import de.kuschku.quasseldroid.viewmodel.data.BufferStatus +import javax.inject.Inject + +class BufferPresenter @Inject constructor( + val appearanceSettings: AppearanceSettings, + val messageSettings: MessageSettings, + val ircFormatDeserializer: IrcFormatDeserializer, + val colorContext: ColorContext +) { + fun render(props: BufferProps, + activities: Map<BufferId, UInt>, + defaultFiltered: UInt + ): BufferProps { + val activity = props.activity - (activities[props.info.bufferId] + ?: defaultFiltered + ?: 0u) + + return props.copy( + activity = activity, + description = ircFormatDeserializer.formatString( + props.description.toString(), + colorize = messageSettings.colorizeMirc + ), + bufferActivity = Buffer_Activity.of( + when { + props.highlights > 0 -> Buffer_Activity.Highlight + activity.hasFlag(Message_Type.Plain) || + activity.hasFlag(Message_Type.Notice) || + activity.hasFlag(Message_Type.Action) -> Buffer_Activity.NewMessage + activity.isNotEmpty() -> Buffer_Activity.OtherActivity + else -> Buffer_Activity.NoActivity + } + ), + fallbackDrawable = if (props.info.type.hasFlag(Buffer_Type.QueryBuffer)) { + props.ircUser?.let { + val nickName = it.nick() + val useSelfColor = when (messageSettings.colorizeNicknames) { + MessageSettings.ColorizeNicknamesMode.ALL -> false + MessageSettings.ColorizeNicknamesMode.ALL_BUT_MINE -> + props.ircUser?.network()?.isMyNick(nickName) == true + MessageSettings.ColorizeNicknamesMode.NONE -> true + } + + colorContext.buildTextDrawable(it.nick(), useSelfColor) + } ?: colorContext.buildTextDrawable("", colorContext.colorAway) + } else { + val color = if (props.bufferStatus == BufferStatus.ONLINE) colorContext.colorAccent + else colorContext.colorAway + + colorContext.buildTextDrawable("#", color) + }, + avatarUrls = props.ircUser?.let { + AvatarHelper.avatar(messageSettings, it, colorContext.avatarSize) + } ?: emptyList() + ) + } + + fun render(buffers: List<BufferListItem>, filteredList: List<Filtered>, defaultFiltered: UInt) = + buffers.map { + it.copy(props = render( + it.props, + filteredList.associate { it.bufferId to it.filtered.toUInt() }, + defaultFiltered + )) + } +} diff --git a/app/src/main/res/layout/chat_archive.xml b/app/src/main/res/layout/chat_archive.xml index 3c6666c29..c66e128af 100644 --- a/app/src/main/res/layout/chat_archive.xml +++ b/app/src/main/res/layout/chat_archive.xml @@ -23,6 +23,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" + android:scrollbars="vertical" android:orientation="vertical"> <LinearLayout @@ -53,7 +54,9 @@ android:id="@+id/list_temporary" android:layout_width="match_parent" android:layout_height="wrap_content" - tools:listitem="@layout/widget_buffer_reorder" /> + android:layout_marginStart="56dp" + android:layout_marginLeft="56dp" + tools:listitem="@layout/widget_buffer" /> <LinearLayout style="@style/Widget.CoreSettings.PrimaryItemGroupHeader"> @@ -78,6 +81,8 @@ android:id="@+id/list_permanently" android:layout_width="match_parent" android:layout_height="wrap_content" - tools:listitem="@layout/widget_buffer_reorder" /> + android:layout_marginStart="56dp" + android:layout_marginLeft="56dp" + tools:listitem="@layout/widget_buffer" /> </LinearLayout> </androidx.core.widget.NestedScrollView> diff --git a/app/src/main/res/layout/widget_buffer.xml b/app/src/main/res/layout/widget_buffer.xml index 777b22f80..e5d92d591 100644 --- a/app/src/main/res/layout/widget_buffer.xml +++ b/app/src/main/res/layout/widget_buffer.xml @@ -18,6 +18,7 @@ --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" @@ -41,9 +42,10 @@ tools:src="@tools:sample/avatars" /> <LinearLayout - android:layout_width="match_parent" + android:layout_width="0dip" android:layout_height="wrap_content" android:layout_gravity="center_vertical" + android:layout_weight="1" android:orientation="vertical"> <TextView @@ -71,4 +73,15 @@ tools:text="@sample/messages.json/data/sender" tools:visibility="visible" /> </LinearLayout> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/handle" + android:layout_width="24dp" + android:layout_height="24dp" + android:layout_gravity="center_vertical" + android:layout_marginStart="16dp" + android:layout_marginLeft="16dp" + android:contentDescription="@string/label_reorder" + app:srcCompat="@drawable/ic_reorder" + app:tint="?colorTextSecondary" /> </LinearLayout> diff --git a/app/src/main/res/layout/widget_buffer_away.xml b/app/src/main/res/layout/widget_buffer_away.xml index 838477b53..3e4a9c043 100644 --- a/app/src/main/res/layout/widget_buffer_away.xml +++ b/app/src/main/res/layout/widget_buffer_away.xml @@ -85,4 +85,15 @@ android:contentDescription="@string/label_user_away" app:srcCompat="@drawable/ic_clock" app:tint="?colorTextSecondary" /> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/handle" + android:layout_width="24dp" + android:layout_height="24dp" + android:layout_gravity="center_vertical" + android:layout_marginStart="16dp" + android:layout_marginLeft="16dp" + android:contentDescription="@string/label_reorder" + app:srcCompat="@drawable/ic_reorder" + app:tint="?colorTextSecondary" /> </LinearLayout> diff --git a/app/src/main/res/layout/widget_buffer_reorder.xml b/app/src/main/res/layout/widget_buffer_reorder.xml deleted file mode 100644 index d44f6d9ae..000000000 --- a/app/src/main/res/layout/widget_buffer_reorder.xml +++ /dev/null @@ -1,89 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?><!-- - 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/>. - --> - -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - xmlns:tools="http://schemas.android.com/tools" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginStart="56dp" - android:layout_marginLeft="56dp" - android:background="?attr/backgroundMenuItem" - android:minHeight="?listPreferredItemHeightSmall" - android:orientation="horizontal" - android:paddingLeft="16dp" - android:paddingTop="4dp" - android:paddingRight="16dp" - android:paddingBottom="4dp" - android:textAppearance="?android:attr/textAppearanceListItemSmall"> - - <androidx.appcompat.widget.AppCompatImageView - android:id="@+id/status" - android:layout_width="@dimen/avatar_size_buffer" - android:layout_height="@dimen/avatar_size_buffer" - android:layout_gravity="center_vertical" - android:layout_marginEnd="16dp" - android:layout_marginRight="16dp" - android:contentDescription="@string/label_avatar" - tools:src="@tools:sample/avatars" /> - - <LinearLayout - android:layout_width="0dip" - android:layout_height="wrap_content" - android:layout_gravity="center_vertical" - android:layout_weight="1" - android:orientation="vertical"> - - <TextView - android:id="@+id/name" - style="@style/Widget.RtlConformTextView" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center_vertical|start" - android:ellipsize="marquee" - android:fontFamily="sans-serif-medium" - android:singleLine="true" - android:textColor="?attr/colorTextPrimary" - android:textSize="13sp" - tools:text="@sample/messages.json/data/sender" /> - - <TextView - android:id="@+id/description" - style="@style/Widget.RtlConformTextView" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:ellipsize="marquee" - android:singleLine="true" - android:textColor="?attr/colorTextSecondary" - android:textSize="12sp" - tools:text="@sample/messages.json/data/sender" - tools:visibility="visible" /> - </LinearLayout> - - <androidx.appcompat.widget.AppCompatImageView - android:id="@+id/reorder" - android:layout_width="24dp" - android:layout_height="24dp" - android:layout_gravity="center_vertical" - android:layout_marginStart="16dp" - android:layout_marginLeft="16dp" - android:contentDescription="@string/label_reorder" - app:srcCompat="@drawable/ic_reorder" - app:tint="?colorTextSecondary" /> -</LinearLayout> diff --git a/app/src/main/res/layout/widget_buffer_reorder_away.xml b/app/src/main/res/layout/widget_buffer_reorder_away.xml deleted file mode 100644 index f4b562f77..000000000 --- a/app/src/main/res/layout/widget_buffer_reorder_away.xml +++ /dev/null @@ -1,101 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?><!-- - 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/>. - --> - -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - xmlns:tools="http://schemas.android.com/tools" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginStart="56dp" - android:layout_marginLeft="56dp" - android:background="?attr/backgroundMenuItem" - android:minHeight="?listPreferredItemHeightSmall" - android:orientation="horizontal" - android:paddingLeft="16dp" - android:paddingTop="4dp" - android:paddingRight="16dp" - android:paddingBottom="4dp" - android:textAppearance="?android:attr/textAppearanceListItemSmall"> - - <androidx.appcompat.widget.AppCompatImageView - android:id="@+id/status" - android:layout_width="@dimen/avatar_size_buffer" - android:layout_height="@dimen/avatar_size_buffer" - android:layout_gravity="center_vertical" - android:layout_marginEnd="16dp" - android:layout_marginRight="16dp" - android:contentDescription="@string/label_avatar" - tools:src="@tools:sample/avatars" /> - - <LinearLayout - android:layout_width="0dip" - android:layout_height="wrap_content" - android:layout_gravity="center_vertical" - android:layout_weight="1" - android:orientation="vertical"> - - <TextView - android:id="@+id/name" - style="@style/Widget.RtlConformTextView" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center_vertical|start" - android:ellipsize="marquee" - android:fontFamily="sans-serif-medium" - android:singleLine="true" - android:textColor="?attr/colorTextSecondary" - android:textSize="13sp" - android:textStyle="italic" - tools:text="@sample/messages.json/data/sender" /> - - <TextView - android:id="@+id/description" - style="@style/Widget.RtlConformTextView" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:ellipsize="marquee" - android:singleLine="true" - android:textColor="?attr/colorTextSecondary" - android:textSize="12sp" - android:textStyle="italic" - tools:text="@sample/messages.json/data/sender" - tools:visibility="visible" /> - </LinearLayout> - - <androidx.appcompat.widget.AppCompatImageView - android:layout_width="24dp" - android:layout_height="24dp" - android:layout_gravity="center_vertical" - android:layout_marginStart="16dp" - android:layout_marginLeft="16dp" - android:contentDescription="@string/label_user_away" - app:srcCompat="@drawable/ic_clock" - app:tint="?colorTextSecondary" /> - - <androidx.appcompat.widget.AppCompatImageView - android:id="@+id/reorder" - android:layout_width="24dp" - android:layout_height="24dp" - android:layout_gravity="center_vertical" - android:layout_marginStart="16dp" - android:layout_marginLeft="16dp" - android:contentDescription="@string/label_reorder" - app:srcCompat="@drawable/ic_reorder" - app:tint="?colorTextSecondary" /> -</LinearLayout> diff --git a/app/src/main/res/menu/context_buffer.xml b/app/src/main/res/menu/context_buffer.xml index 87ea5e57d..7ad41c0a6 100644 --- a/app/src/main/res/menu/context_buffer.xml +++ b/app/src/main/res/menu/context_buffer.xml @@ -48,11 +48,7 @@ android:title="@string/label_unhide" app:showAsAction="never" /> <item - android:id="@+id/action_hide_temp" - android:title="@string/label_hide_temp" - app:showAsAction="never" /> - <item - android:id="@+id/action_hide_perm" - android:title="@string/label_hide_perm" + android:id="@+id/action_archive" + android:title="@string/label_archive" app:showAsAction="never" /> </menu> diff --git a/app/src/main/res/menu/context_bufferlist.xml b/app/src/main/res/menu/context_bufferlist.xml index c1c66b6c0..5aa271be7 100644 --- a/app/src/main/res/menu/context_bufferlist.xml +++ b/app/src/main/res/menu/context_bufferlist.xml @@ -25,8 +25,4 @@ android:id="@+id/action_search" android:checkable="true" android:title="@string/label_search_buffer" /> - <item - android:id="@+id/action_show_hidden" - android:checkable="true" - android:title="@string/label_show_hidden" /> </menu> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c327f3f31..3205d5d09 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -28,13 +28,15 @@ <string name="label_about">About</string> <string name="label_accept">Accept</string> <string name="label_acknowledgements">Acknowledgements</string> + <string name="label_archive">Archive</string> + <string name="label_archive_chat">Archive Chat</string> <string name="label_archived_chats">Archived Chats</string> <string name="label_ascending">Ascending</string> <string name="label_authors">Authors</string> <string name="label_autocomplete">Autocomplete</string> <string name="label_avatar">Avatar</string> <string name="label_back">Back</string> - <string name="label_buffer_name">Buffer Name</string> + <string name="label_buffer_name">Chat Name</string> <string name="label_cancel">Cancel</string> <string name="label_certificates">Certificates</string> <string name="label_channel_name">Channel Name</string> @@ -59,8 +61,8 @@ <string name="label_filter_messages">Filter Messages</string> <string name="label_finish">Finish</string> <string name="label_generate_crash_report">Generate Crash Report</string> - <string name="label_hide_perm">Hide Permanently</string> - <string name="label_hide_temp">Hide Temporarily</string> + <string name="label_hide_perm">Archive Permanently</string> + <string name="label_hide_temp">Archive Temporarily</string> <string name="label_ignore">Ignore</string> <string name="label_ignore_long">Add/remove user to/from ignore list</string> <string name="label_info">Details</string> @@ -131,7 +133,6 @@ <string name="label_share_crashreport">Share Crash Report</string> <string name="label_shortcut">Shortcut</string> <string name="label_shortcut_long">Create Shortcut on Homescreen</string> - <string name="label_show_hidden">Show Hidden</string> <string name="label_sort">Sort</string> <string name="label_source">Source</string> <string name="label_temporarily_archived">Temporarily Archived</string> @@ -190,6 +191,8 @@ <string name="info_missing_features" tools:ignore="StringFormatCount">Your core is missing features that are required for Quasseldroid to work correctly. You should <a href="https://quassel-irc.org>upgrade</a> your Quassel core to %1$s or newer.</string> <string name="buffer_delete_confirmation">Do you want to delete this buffer permanently?</string> + <string name="buffer_archive_confirmation">Do you want to archive this buffer?</string> + <string name="buffer_archive_temporarily">Automatically unhide this chat once it has new messages</string> <string name="delete_confirmation">Are you sure you want to delete this permanently? This can not be undone.</string> <string name="cancel_confirmation">You have unsaved changes. Do you wish to discard them?</string> diff --git a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/ArchiveViewModel.kt b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/ArchiveViewModel.kt new file mode 100644 index 000000000..b26f3a5ee --- /dev/null +++ b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/ArchiveViewModel.kt @@ -0,0 +1,88 @@ +/* + * 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.viewmodel + +import android.os.Bundle +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 ArchiveViewModel : QuasselViewModel() { + val bufferViewConfigId = BehaviorSubject.createDefault(-1) + val visibleExpandedNetworks = BehaviorSubject.createDefault(emptyMap<NetworkId, Boolean>()) + val temporarilyExpandedNetworks = BehaviorSubject.createDefault(emptyMap<NetworkId, Boolean>()) + val permanentlyExpandedNetworks = BehaviorSubject.createDefault(emptyMap<NetworkId, Boolean>()) + val selectedBufferId = BehaviorSubject.createDefault(BufferId.MAX_VALUE) + + fun onSaveInstanceState(outState: Bundle) { + outState.putInt( + KEY_BUFFER_VIEW_CONFIG_ID, + bufferViewConfigId.value) + outState.putSerializable( + KEY_VISIBLE_EXPANDED_NETWORKS, + HashMap(visibleExpandedNetworks.value)) + outState.putSerializable( + KEY_TEMPORARILY_EXPANDED_NETWORKS, + HashMap(temporarilyExpandedNetworks.value)) + outState.putSerializable( + KEY_PERMANENTLY_EXPANDED_NETWORKS, + HashMap(permanentlyExpandedNetworks.value)) + outState.putInt( + KEY_SELECTED_BUFFER_ID, + selectedBufferId.value.id) + } + + fun onRestoreInstanceState(savedInstanceState: Bundle) { + if (savedInstanceState.containsKey(KEY_BUFFER_VIEW_CONFIG_ID)) + bufferViewConfigId.onNext(savedInstanceState.getInt(KEY_BUFFER_VIEW_CONFIG_ID)) + if (savedInstanceState.containsKey(KEY_VISIBLE_EXPANDED_NETWORKS)) { + visibleExpandedNetworks.onNext( + savedInstanceState.getSerializable(KEY_VISIBLE_EXPANDED_NETWORKS) as? HashMap<NetworkId, Boolean> + ?: emptyMap() + ) + } + if (savedInstanceState.containsKey(KEY_TEMPORARILY_EXPANDED_NETWORKS)) { + temporarilyExpandedNetworks.onNext( + savedInstanceState.getSerializable(KEY_TEMPORARILY_EXPANDED_NETWORKS) as? HashMap<NetworkId, Boolean> + ?: emptyMap() + ) + } + if (savedInstanceState.containsKey(KEY_PERMANENTLY_EXPANDED_NETWORKS)) { + permanentlyExpandedNetworks.onNext( + savedInstanceState.getSerializable(KEY_PERMANENTLY_EXPANDED_NETWORKS) as? HashMap<NetworkId, Boolean> + ?: emptyMap() + ) + } + + if (savedInstanceState.containsKey(KEY_SELECTED_BUFFER_ID)) + selectedBufferId.onNext(BufferId(savedInstanceState.getInt(KEY_SELECTED_BUFFER_ID))) + } + + companion object { + const val KEY_BUFFER_VIEW_CONFIG_ID = "model_archive_bufferViewConfigId" + const val KEY_VISIBLE_EXPANDED_NETWORKS = "model_archive_visibleExpandedNetworks" + const val KEY_TEMPORARILY_EXPANDED_NETWORKS = "model_archive_temporarilyExpandedNetworks" + const val KEY_PERMANENTLY_EXPANDED_NETWORKS = "model_archive_permanentlyExpandedNetworks" + const val KEY_SELECTED_BUFFER_ID = "model_archive_selectedBufferId" + } +} diff --git a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/data/BufferState.kt b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/data/BufferState.kt index 7ed5b7123..f7b89d58d 100644 --- a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/data/BufferState.kt +++ b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/data/BufferState.kt @@ -21,5 +21,6 @@ package de.kuschku.quasseldroid.viewmodel.data data class BufferState( val networkExpanded: Boolean, - val selected: Boolean + val selected: Boolean, + val showHandle: Boolean ) diff --git a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/helper/ArchiveViewModelHelper.kt b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/helper/ArchiveViewModelHelper.kt new file mode 100644 index 000000000..2f555d87f --- /dev/null +++ b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/helper/ArchiveViewModelHelper.kt @@ -0,0 +1,186 @@ +/* + * 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.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.quassel.BufferInfo +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.util.Optional +import de.kuschku.libquassel.util.flag.hasFlag +import de.kuschku.libquassel.util.helper.combineLatest +import de.kuschku.libquassel.util.helper.flatMapSwitchMap +import de.kuschku.libquassel.util.helper.mapSwitchMap +import de.kuschku.libquassel.util.helper.safeSwitchMap +import de.kuschku.libquassel.util.irc.IrcCaseMappers +import de.kuschku.quasseldroid.viewmodel.ArchiveViewModel +import de.kuschku.quasseldroid.viewmodel.QuasselViewModel +import de.kuschku.quasseldroid.viewmodel.data.BufferHiddenState +import de.kuschku.quasseldroid.viewmodel.data.BufferProps +import de.kuschku.quasseldroid.viewmodel.data.BufferStatus +import io.reactivex.Observable +import javax.inject.Inject + +open class ArchiveViewModelHelper @Inject constructor( + val archive: ArchiveViewModel, + quassel: QuasselViewModel +) : QuasselViewModelHelper(quassel) { + val bufferViewConfig = bufferViewManager.flatMapSwitchMap { manager -> + archive.bufferViewConfigId.map { id -> + Optional.ofNullable(manager.bufferViewConfig(id)) + }.mapSwitchMap(BufferViewConfig::liveUpdates) + } + + fun processBufferList(bufferListType: BufferHiddenState) = + combineLatest(connectedSession, bufferViewConfig) + .safeSwitchMap { (sessionOptional, configOptional) -> + val session = sessionOptional.orNull() + val bufferSyncer = session?.bufferSyncer + val config = configOptional.orNull() + if (bufferSyncer != null && config != null) { + session.liveNetworks().safeSwitchMap { networks -> + config.liveUpdates() + .safeSwitchMap { currentConfig -> + combineLatest<Collection<BufferId>>( + listOf( + config.liveBuffers(), + config.liveTemporarilyRemovedBuffers(), + config.liveRemovedBuffers() + ) + ).safeSwitchMap { (ids, temp, perm) -> + fun missingStatusBuffers( + list: Collection<BufferId> + ): Sequence<Observable<BufferProps>?> { + val buffers = list.asSequence().mapNotNull { id -> + bufferSyncer.bufferInfo(id) + } + + val totalNetworks = buffers.filter { + !it.type.hasFlag(Buffer_Type.StatusBuffer) + }.map { + it.networkId + }.toList() + + val availableNetworks = buffers.filter { + it.type.hasFlag(Buffer_Type.StatusBuffer) + }.map { + it.networkId + }.toList() + + val wantedNetworks = if (!currentConfig.networkId().isValidId()) totalNetworks + else listOf(currentConfig.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().safeSwitchMap { 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 + ) + } + } + } + } + + fun transformIds(ids: Collection<BufferId>, state: BufferHiddenState) = + processRawBufferList(ids, state, bufferSyncer, networks, currentConfig) + + missingStatusBuffers(ids) + + bufferSyncer.liveBufferInfos().safeSwitchMap { + val buffers = when (bufferListType) { + BufferHiddenState.VISIBLE -> + transformIds(ids, BufferHiddenState.VISIBLE) + BufferHiddenState.HIDDEN_TEMPORARY -> + transformIds(temp - ids, BufferHiddenState.HIDDEN_TEMPORARY) + BufferHiddenState.HIDDEN_PERMANENT -> + transformIds(perm - temp - ids, BufferHiddenState.HIDDEN_PERMANENT) + } + + 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())) + } + } + + fun processArchiveBufferList( + bufferListType: BufferHiddenState, + showHandle: Boolean + ) = processInternalBufferList( + processBufferList(bufferListType), + when (bufferListType) { + BufferHiddenState.VISIBLE -> archive.visibleExpandedNetworks + BufferHiddenState.HIDDEN_TEMPORARY -> archive.temporarilyExpandedNetworks + BufferHiddenState.HIDDEN_PERMANENT -> archive.permanentlyExpandedNetworks + }, + archive.selectedBufferId, + showHandle + ) + + val selectedBuffer = processSelectedBuffer(archive.selectedBufferId, bufferViewConfig) +} 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 index 68b177a1e..f0016d17f 100644 --- a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/helper/ChatViewModelHelper.kt +++ b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/helper/ChatViewModelHelper.kt @@ -30,7 +30,6 @@ 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.helper.* import de.kuschku.libquassel.util.irc.IrcCaseMappers @@ -175,63 +174,7 @@ open class ChatViewModelHelper @Inject constructor( val nickDataThrottled = nickData.distinctUntilChanged().throttleLast(100, TimeUnit.MILLISECONDS) - val selectedBuffer = combineLatest(connectedSession, chat.selectedBufferId, bufferViewConfig) - .safeSwitchMap { (sessionOptional, buffer, bufferViewConfigOptional) -> - val session = sessionOptional.orNull() - val bufferSyncer = session?.bufferSyncer - val bufferViewConfig = bufferViewConfigOptional.orNull() - if (bufferSyncer != null && bufferViewConfig != null) { - session.liveNetworks().safeSwitchMap { 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 selectedBuffer = processSelectedBuffer(chat.selectedBufferId, bufferViewConfig) val bufferList: Observable<Pair<BufferViewConfig?, List<BufferProps>>> = combineLatest(connectedSession, bufferViewConfig, chat.showHidden, chat.bufferSearch) @@ -252,109 +195,14 @@ open class ChatViewModelHelper @Inject constructor( ) ).safeSwitchMap { (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).safeSwitchMap { activity -> - bufferSyncer.liveHighlightCount(info.bufferId).map { highlights -> - activity to highlights - } - }.safeSwitchMap { (activity, highlights) -> - val name = info.bufferName?.trim() ?: "" - val search = bufferSearch.trim() - val matchMode = when { - name.equals(search, ignoreCase = true) -> MatchMode.EXACT - name.startsWith(search, ignoreCase = true) -> MatchMode.START - else -> MatchMode.CONTAINS - } - when (info.type.toInt()) { - BufferInfo.Type.QueryBuffer.toInt() -> { - network.liveNetworkInfo().safeSwitchMap { networkInfo -> - network.liveConnectionState().safeSwitchMap { connectionState -> - network.liveIrcUser(info.bufferName).safeSwitchMap { - 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().safeSwitchMap { networkInfo -> - network.liveConnectionState().safeSwitchMap { connectionState -> - network.liveIrcChannel(info.bufferName).safeSwitchMap { 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().safeSwitchMap { 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() - } - } - } + processRawBufferList( + ids, + state, + bufferSyncer, + networks, + currentConfig, + bufferSearch + ) fun missingStatusBuffers( list: Collection<BufferId>): Sequence<Observable<BufferProps>?> { @@ -444,4 +292,11 @@ open class ChatViewModelHelper @Inject constructor( Observable.just(Pair<BufferViewConfig?, List<BufferProps>>(null, emptyList())) } } + + val processedBufferList = processInternalBufferList( + bufferList, + chat.expandedNetworks, + chat.selectedBufferId, + false + ) } 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 index 005e006e7..6ad271a7f 100644 --- a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/helper/QuasselViewModelHelper.kt +++ b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/helper/QuasselViewModelHelper.kt @@ -21,17 +21,21 @@ 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.protocol.* import de.kuschku.libquassel.quassel.BufferInfo -import de.kuschku.libquassel.quassel.syncables.BufferViewConfig -import de.kuschku.libquassel.quassel.syncables.CoreInfo +import de.kuschku.libquassel.quassel.syncables.* +import de.kuschku.libquassel.quassel.syncables.interfaces.INetwork 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.helper.* +import de.kuschku.libquassel.util.irc.IrcCaseMappers import de.kuschku.quasseldroid.Backend import de.kuschku.quasseldroid.viewmodel.QuasselViewModel +import de.kuschku.quasseldroid.viewmodel.data.* import io.reactivex.Observable import javax.inject.Inject import javax.net.ssl.SSLSession @@ -122,4 +126,204 @@ open class QuasselViewModelHelper @Inject constructor( } }.orElse(Observable.empty()) } + + fun processRawBufferList(ids: Collection<BufferId>, state: BufferHiddenState, bufferSyncer: BufferSyncer, networks: Map<NetworkId, Network>, currentConfig: BufferViewConfig, bufferSearch: String = "") = + 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).safeSwitchMap { activity -> + bufferSyncer.liveHighlightCount(info.bufferId).map { highlights -> + activity to highlights + } + }.safeSwitchMap { (activity, highlights) -> + val name = info.bufferName?.trim() ?: "" + val search = bufferSearch.trim() + val matchMode = when { + name.equals(search, ignoreCase = true) -> MatchMode.EXACT + name.startsWith(search, ignoreCase = true) -> MatchMode.START + else -> MatchMode.CONTAINS + } + when (info.type.toInt()) { + BufferInfo.Type.QueryBuffer.toInt() -> { + network.liveNetworkInfo().safeSwitchMap { networkInfo -> + network.liveConnectionState().safeSwitchMap { connectionState -> + network.liveIrcUser(info.bufferName).safeSwitchMap { + 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().safeSwitchMap { networkInfo -> + network.liveConnectionState().safeSwitchMap { connectionState -> + network.liveIrcChannel(info.bufferName).safeSwitchMap { 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().safeSwitchMap { 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 processInternalBufferList( + buffers: Observable<Pair<BufferViewConfig?, List<BufferProps>>>, + expandedNetworks: Observable<Map<NetworkId, Boolean>>, + selected: Observable<BufferId>, + showHandle: Boolean + ) = + combineLatest( + buffers, + expandedNetworks, + selected + ).map { (info, expandedNetworks, selected) -> + val (config, list) = info ?: Pair(null, emptyList()) + val minimumActivity = config?.minimumActivity() ?: Buffer_Activity.NONE + list.asSequence().sortedBy { props -> + !props.info.type.hasFlag(Buffer_Type.StatusBuffer) + }.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { props -> + props.network.networkName + }).map { props -> + BufferListItem( + props, + BufferState( + networkExpanded = expandedNetworks[props.network.networkId] + ?: (props.networkConnectionState == INetwork.ConnectionState.Initialized), + selected = selected == props.info.bufferId, + showHandle = showHandle && (config?.sortAlphabetically() == false) + ) + ) + }.filter { (props, state) -> + (props.info.type.hasFlag(BufferInfo.Type.StatusBuffer) || state.networkExpanded) && + (minimumActivity.toInt() <= props.bufferActivity.toInt() || + props.info.type.hasFlag(Buffer_Type.StatusBuffer)) + }.toList() + } + + fun processSelectedBuffer( + selectedBufferId: Observable<BufferId>, + bufferViewConfig: Observable<Optional<BufferViewConfig>> + ) = combineLatest(connectedSession, selectedBufferId, bufferViewConfig) + .safeSwitchMap { (sessionOptional, buffer, bufferViewConfigOptional) -> + val session = sessionOptional.orNull() + val bufferSyncer = session?.bufferSyncer + val bufferViewConfig = bufferViewConfigOptional.orNull() + if (bufferSyncer != null && bufferViewConfig != null) { + session.liveNetworks().safeSwitchMap { 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()) + } + } } -- GitLab