Merge pull request #674 from benwaffle/view-bindings-2

Finish migration to ViewBinding
This commit is contained in:
octoshrimpy
2026-02-07 09:19:11 -05:00
committed by GitHub
47 changed files with 1188 additions and 1139 deletions
-1
View File
@@ -19,7 +19,6 @@
apply plugin: 'com.android.library'
apply plugin: 'realm-android' // Realm needs to be before Kotlin or the build will fail
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
-5
View File
@@ -19,7 +19,6 @@
apply plugin: 'com.android.application'
apply plugin: 'realm-android' // Realm needs to be before Kotlin or the build will fail
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
@@ -107,10 +106,6 @@ android {
}
}
androidExtensions {
experimental = true
}
configurations {
debug
release
@@ -24,11 +24,13 @@ import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.WindowManager
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import dev.octoshrimpy.quik.R
import dev.octoshrimpy.quik.util.Preferences
import io.reactivex.subjects.BehaviorSubject
import io.reactivex.subjects.Subject
import kotlinx.android.synthetic.main.toolbar.*
import javax.inject.Inject
abstract class QkActivity : AppCompatActivity() {
@@ -36,6 +38,9 @@ abstract class QkActivity : AppCompatActivity() {
protected val menu: Subject<Menu> = BehaviorSubject.create()
protected val toolbar: Toolbar? get() = findViewById(R.id.toolbar)
protected val toolbarTitle: TextView? get() = findViewById(R.id.toolbarTitle)
@SuppressLint("InlinedApi")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -21,15 +21,14 @@ package dev.octoshrimpy.quik.common.base
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.LayoutRes
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.viewbinding.ViewBinding
import com.bluelinelabs.conductor.archlifecycle.LifecycleController
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.*
import kotlinx.android.synthetic.main.toolbar.view.*
import dev.octoshrimpy.quik.R
abstract class QkController<ViewContract : QkViewContract<State>, State, Presenter : QkPresenter<ViewContract, State>> : LifecycleController(), LayoutContainer {
abstract class QkController<VB : ViewBinding, ViewContract : QkViewContract<State>, State, Presenter : QkPresenter<ViewContract, State>> : LifecycleController() {
abstract var presenter: Presenter
@@ -39,16 +38,16 @@ abstract class QkController<ViewContract : QkViewContract<State>, State, Present
protected val themedActivity: QkThemedActivity?
get() = activity as? QkThemedActivity
override var containerView: View? = null
private var _binding: VB? = null
protected val binding: VB
get() = requireNotNull(_binding)
@LayoutRes
var layoutRes: Int = 0
protected abstract fun inflateBinding(inflater: LayoutInflater, container: ViewGroup): VB
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup): View {
return inflater.inflate(layoutRes, container, false).also {
containerView = it
onViewCreated()
}
val vb = inflateBinding(inflater, container)
_binding = vb
return vb.root.also { onViewCreated() }
}
open fun onViewCreated() {
@@ -60,7 +59,7 @@ abstract class QkController<ViewContract : QkViewContract<State>, State, Present
fun setTitle(title: CharSequence?) {
activity?.title = title
view?.toolbarTitle?.text = title
view?.findViewById<TextView>(R.id.toolbarTitle)?.text = title
}
fun showBackButton(show: Boolean) {
@@ -68,8 +67,8 @@ abstract class QkController<ViewContract : QkViewContract<State>, State, Present
}
override fun onDestroyView(view: View) {
containerView = null
clearFindViewByIdCache()
super.onDestroyView(view)
_binding = null
}
override fun onDestroy() {
@@ -43,7 +43,6 @@ import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.rxkotlin.Observables
import io.reactivex.subjects.BehaviorSubject
import io.reactivex.subjects.Subject
import kotlinx.android.synthetic.main.toolbar.*
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@@ -21,10 +21,7 @@ package dev.octoshrimpy.quik.common.base
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import kotlinx.android.extensions.LayoutContainer
open class QkViewHolder(view: View) : RecyclerView.ViewHolder(view), LayoutContainer {
override val containerView: View = view
}
open class QkViewHolder(view: View) : RecyclerView.ViewHolder(view)
open class QkBindingViewHolder<T : ViewBinding>(val binding: T) : QkViewHolder(binding.root)
@@ -38,7 +38,11 @@ class PreferenceView @JvmOverloads constructor(
private var layout: PreferenceViewBinding
val titleTextView: TextView get() = layout.titleView
val titleView: TextView get() = layout.titleView
val widget: View? get() = layout.widgetFrame.getChildAt(0)
val checkbox: QkSwitch? get() = widget as? QkSwitch
var title: String? = null
set(value) {
@@ -69,6 +69,10 @@ class RadioPreferenceView @JvmOverloads constructor(
}
}
val radioButton get() = layout.radioButton
val titleView get() = layout.titleView
val summaryView get() = layout.summaryView
init {
if (!isInEditMode) {
appComponent.inject(this)
@@ -22,7 +22,9 @@ import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Typeface
import android.net.Uri
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
@@ -41,14 +43,13 @@ import dev.octoshrimpy.quik.common.util.extensions.setTint
import dev.octoshrimpy.quik.common.widget.PreferenceView
import dev.octoshrimpy.quik.injection.appComponent
import dev.octoshrimpy.quik.repository.BackupRepository
import dev.octoshrimpy.quik.databinding.BackupControllerBinding
import io.reactivex.Observable
import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.Subject
import kotlinx.android.synthetic.main.backup_controller.*
import kotlinx.android.synthetic.main.preference_view.view.*
import javax.inject.Inject
class BackupController : QkController<BackupView, BackupState, BackupPresenter>(), BackupView {
class BackupController : QkController<BackupControllerBinding, BackupView, BackupState, BackupPresenter>(), BackupView {
@Inject override lateinit var presenter: BackupPresenter
@@ -109,9 +110,11 @@ class BackupController : QkController<BackupView, BackupState, BackupPresenter>(
init {
appComponent.inject(this)
layoutRes = R.layout.backup_controller
}
override fun inflateBinding(inflater: LayoutInflater, container: ViewGroup): BackupControllerBinding =
BackupControllerBinding.inflate(inflater, container, false)
override fun onContextAvailable(context: Context) {
// Init activity result contracts
openDirectory = themedActivity!!
@@ -136,57 +139,57 @@ class BackupController : QkController<BackupView, BackupState, BackupPresenter>(
super.onViewCreated()
themedActivity?.colors?.theme()?.let { theme ->
progressBar.indeterminateTintList = ColorStateList.valueOf(theme.theme)
progressBar.progressTintList = ColorStateList.valueOf(theme.theme)
fab.setBackgroundTint(theme.theme)
fabIcon.setTint(theme.textPrimary)
fabLabel.setTextColor(theme.textPrimary)
binding.progressBar.indeterminateTintList = ColorStateList.valueOf(theme.theme)
binding.progressBar.progressTintList = ColorStateList.valueOf(theme.theme)
binding.fab.setBackgroundTint(theme.theme)
binding.fabIcon.setTint(theme.textPrimary)
binding.fabLabel.setTextColor(theme.textPrimary)
}
// Make the list titles bold
linearLayout.children
.mapNotNull { it as? PreferenceView }
.map { it.titleView }
.forEach { it.setTypeface(it.typeface, Typeface.BOLD) }
binding.linearLayout.children
.mapNotNull { it as? PreferenceView }
.map { it.titleView }
.forEach { it.setTypeface(it.typeface, Typeface.BOLD) }
}
override fun render(state: BackupState) {
when {
state.backupProgress.running -> {
progressIcon.setImageResource(R.drawable.ic_file_upload_black_24dp)
progressTitle.setText(R.string.backup_backing_up)
progressSummary.text = state.backupProgress.getLabel(activity!!)
progressSummary.isVisible = progressSummary.text.isNotEmpty()
progressCancel.isVisible = false
binding.progressIcon.setImageResource(R.drawable.ic_file_upload_black_24dp)
binding.progressTitle.setText(R.string.backup_backing_up)
binding.progressSummary.text = state.backupProgress.getLabel(activity!!)
binding.progressSummary.isVisible = binding.progressSummary.text.isNotEmpty()
binding.progressCancel.isVisible = false
val running = (state.backupProgress as? BackupRepository.Progress.Running)
progressBar.isVisible =
binding.progressBar.isVisible =
state.backupProgress.indeterminate || (running?.max ?: 0) > 0
progressBar.isIndeterminate = state.backupProgress.indeterminate
progressBar.max = running?.max ?: 0
progressBar.progress = running?.count ?: 0
progress.isVisible = true
fab.isVisible = false
binding.progressBar.isIndeterminate = state.backupProgress.indeterminate
binding.progressBar.max = running?.max ?: 0
binding.progressBar.progress = running?.count ?: 0
binding.progress.isVisible = true
binding.fab.isVisible = false
}
state.restoreProgress.running -> {
progressIcon.setImageResource(R.drawable.ic_file_download_black_24dp)
progressTitle.setText(R.string.backup_restoring)
progressSummary.text = state.restoreProgress.getLabel(activity!!)
progressSummary.isVisible = progressSummary.text.isNotEmpty()
progressCancel.isVisible = true
binding.progressIcon.setImageResource(R.drawable.ic_file_download_black_24dp)
binding.progressTitle.setText(R.string.backup_restoring)
binding.progressSummary.text = state.restoreProgress.getLabel(activity!!)
binding.progressSummary.isVisible = binding.progressSummary.text.isNotEmpty()
binding.progressCancel.isVisible = true
val running = (state.restoreProgress as? BackupRepository.Progress.Running)
progressBar.isVisible =
binding.progressBar.isVisible =
state.restoreProgress.indeterminate || (running?.max ?: 0) > 0
progressBar.isIndeterminate = state.restoreProgress.indeterminate
progressBar.max = running?.max ?: 0
progressBar.progress = running?.count ?: 0
progress.isVisible = true
fab.isVisible = false
binding.progressBar.isIndeterminate = state.restoreProgress.indeterminate
binding.progressBar.max = running?.max ?: 0
binding.progressBar.progress = running?.count ?: 0
binding.progress.isVisible = true
binding.fab.isVisible = false
}
else -> {
progress.isVisible = false
fab.isVisible = true
binding.progress.isVisible = false
binding.fab.isVisible = true
}
}
@@ -199,20 +202,20 @@ class BackupController : QkController<BackupView, BackupState, BackupPresenter>(
stopRestoreDialog.setShowing(state.showStopRestoreDialog)
fabIcon.setImageResource(when (state.upgraded) {
binding.fabIcon.setImageResource(when (state.upgraded) {
true -> R.drawable.ic_file_upload_black_24dp
false -> R.drawable.ic_star_black_24dp
})
fabLabel.setText(when (state.upgraded) {
binding.fabLabel.setText(when (state.upgraded) {
true -> R.string.backup_now
false -> R.string.title_qksms_plus
})
}
override fun setBackupLocationClicks(): Observable<*> = location.clicks()
override fun setBackupLocationClicks(): Observable<*> = binding.location.clicks()
override fun restoreClicks(): Observable<*> = restore.clicks()
override fun restoreClicks(): Observable<*> = binding.restore.clicks()
override fun locationRationaleConfirmClicks(): Observable<*> = selectFolderConfirmSubject
@@ -224,13 +227,13 @@ class BackupController : QkController<BackupView, BackupState, BackupPresenter>(
override fun confirmRestoreBackupCancelClicks(): Observable<*> = confirmRestoreCancelSubject
override fun stopRestoreClicks(): Observable<*> = progressCancel.clicks()
override fun stopRestoreClicks(): Observable<*> = binding.progressCancel.clicks()
override fun stopRestoreConfirmed(): Observable<*> = stopRestoreConfirmSubject
override fun stopRestoreCancel(): Observable<*> = stopRestoreCancelSubject
override fun backupClicks(): Observable<*> = fab.clicks()
override fun backupClicks(): Observable<*> = binding.fab.clicks()
override fun documentTreeSelected(): Observable<Uri> = documentTreeSelectedSubject
@@ -18,7 +18,9 @@
*/
package dev.octoshrimpy.quik.feature.blocking
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.bluelinelabs.conductor.RouterTransaction
import com.jakewharton.rxbinding2.view.clicks
import dev.octoshrimpy.quik.R
@@ -31,17 +33,19 @@ import dev.octoshrimpy.quik.feature.blocking.messages.BlockedMessagesController
import dev.octoshrimpy.quik.feature.blocking.numbers.BlockedNumbersController
import dev.octoshrimpy.quik.feature.blocking.filters.MessageContentFiltersController
import dev.octoshrimpy.quik.injection.appComponent
import kotlinx.android.synthetic.main.blocking_controller.*
import kotlinx.android.synthetic.main.settings_switch_widget.view.*
import dev.octoshrimpy.quik.databinding.BlockingControllerBinding
import javax.inject.Inject
class BlockingController : QkController<BlockingView, BlockingState, BlockingPresenter>(), BlockingView {
class BlockingController : QkController<BlockingControllerBinding, BlockingView, BlockingState, BlockingPresenter>(), BlockingView {
override val blockingManagerIntent by lazy { blockingManager.clicks() }
override val blockedNumbersIntent by lazy { blockedNumbers.clicks() }
override val messageContentFiltersIntent by lazy { messageContentFilters.clicks() }
override val blockedMessagesIntent by lazy { blockedMessages.clicks() }
override val dropClickedIntent by lazy { drop.clicks() }
override fun inflateBinding(inflater: LayoutInflater, container: ViewGroup): BlockingControllerBinding =
BlockingControllerBinding.inflate(inflater, container, false)
override val blockingManagerIntent get() = binding.blockingManager.clicks()
override val blockedNumbersIntent get() = binding.blockedNumbers.clicks()
override val messageContentFiltersIntent get() = binding.messageContentFilters.clicks()
override val blockedMessagesIntent get() = binding.blockedMessages.clicks()
override val dropClickedIntent get() = binding.drop.clicks()
@Inject lateinit var colors: Colors
@Inject override lateinit var presenter: BlockingPresenter
@@ -49,12 +53,11 @@ class BlockingController : QkController<BlockingView, BlockingState, BlockingPre
init {
appComponent.inject(this)
retainViewMode = RetainViewMode.RETAIN_DETACH
layoutRes = R.layout.blocking_controller
}
override fun onViewCreated() {
super.onViewCreated()
parent.postDelayed({ parent?.animateLayoutChanges = true }, 100)
binding.parent.postDelayed({ binding.parent.animateLayoutChanges = true }, 100)
}
override fun onAttach(view: View) {
@@ -65,9 +68,9 @@ class BlockingController : QkController<BlockingView, BlockingState, BlockingPre
}
override fun render(state: BlockingState) {
blockingManager.summary = state.blockingManager
drop.checkbox.isChecked = state.dropEnabled
blockedMessages.isEnabled = !state.dropEnabled
binding.blockingManager.summary = state.blockingManager
binding.drop.checkbox?.isChecked = state.dropEnabled
binding.blockedMessages.isEnabled = !state.dropEnabled
}
override fun openBlockedNumbers() {
@@ -32,15 +32,14 @@ import dev.octoshrimpy.quik.common.util.extensions.setTint
import dev.octoshrimpy.quik.common.widget.PreferenceView
import dev.octoshrimpy.quik.injection.appComponent
import dev.octoshrimpy.quik.model.MessageContentFilterData
import dev.octoshrimpy.quik.databinding.MessageContentFiltersControllerBinding
import dev.octoshrimpy.quik.databinding.MessageContentFiltersAddDialogBinding
import io.reactivex.Observable
import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.Subject
import kotlinx.android.synthetic.main.message_content_filters_add_dialog.view.*
import kotlinx.android.synthetic.main.message_content_filters_controller.*
import kotlinx.android.synthetic.main.settings_switch_widget.view.*
import javax.inject.Inject
class MessageContentFiltersController : QkController<MessageContentFiltersView, MessageContentFiltersState,
class MessageContentFiltersController : QkController<MessageContentFiltersControllerBinding, MessageContentFiltersView, MessageContentFiltersState,
MessageContentFiltersPresenter>(), MessageContentFiltersView {
@Inject override lateinit var presenter: MessageContentFiltersPresenter
@@ -52,9 +51,11 @@ class MessageContentFiltersController : QkController<MessageContentFiltersView,
init {
appComponent.inject(this)
retainViewMode = RetainViewMode.RETAIN_DETACH
layoutRes = R.layout.message_content_filters_controller
}
override fun inflateBinding(inflater: android.view.LayoutInflater, container: android.view.ViewGroup): MessageContentFiltersControllerBinding =
MessageContentFiltersControllerBinding.inflate(inflater, container, false)
override fun onAttach(view: View) {
super.onAttach(view)
presenter.bindIntents(this)
@@ -64,10 +65,10 @@ class MessageContentFiltersController : QkController<MessageContentFiltersView,
override fun onViewCreated() {
super.onViewCreated()
add.setBackgroundTint(colors.theme().theme)
add.setTint(colors.theme().textPrimary)
adapter.emptyView = empty
filters.adapter = adapter
binding.add.setBackgroundTint(colors.theme().theme)
binding.add.setTint(colors.theme().textPrimary)
adapter.emptyView = binding.empty
binding.filters.adapter = adapter
}
override fun render(state: MessageContentFiltersState) {
@@ -75,35 +76,37 @@ class MessageContentFiltersController : QkController<MessageContentFiltersView,
}
override fun removeFilter(): Observable<Long> = adapter.removeMessageContentFilter
override fun addFilter(): Observable<*> = add.clicks()
override fun addFilter(): Observable<*> = binding.add.clicks()
override fun saveFilter(): Observable<MessageContentFilterData> = saveFilterSubject
override fun showAddDialog() {
val layout = LayoutInflater.from(activity).inflate(R.layout.message_content_filters_add_dialog, null)
val layout = MessageContentFiltersAddDialogBinding.inflate(LayoutInflater.from(activity))
(0 until layout.add_dialog.childCount)
.map { index -> layout.add_dialog.getChildAt(index) }
(0 until layout.addDialog.childCount)
.map { index -> layout.addDialog.getChildAt(index) }
.mapNotNull { view -> view as? PreferenceView }
.map { preference -> preference.clicks().map { preference } }
.let { Observable.merge(it) }
.autoDisposable(scope())
.subscribe {
it.checkbox.isChecked = !it.checkbox.isChecked
layout.caseSensitivity.isEnabled = !layout.regexp.checkbox.isChecked
.subscribe { pref ->
pref.checkbox?.let { it.isChecked = !it.isChecked }
val regexChecked = layout.regexp.checkbox?.isChecked ?: false
layout.caseSensitivity.isEnabled = !regexChecked
}
val dialog = AlertDialog.Builder(activity!!)
.setView(layout)
.setView(layout.root)
.setPositiveButton(R.string.message_content_filters_dialog_create) { _, _ ->
var text = layout.input.text.toString();
if (!text.isBlank()) {
if (!layout.regexp.checkbox.isChecked) text = text.trim()
val regexChecked = layout.regexp.checkbox?.isChecked ?: false
if (!regexChecked) text = text.trim()
saveFilterSubject.onNext(
MessageContentFilterData(
text,
layout.caseSensitivity.checkbox.isChecked && !layout.regexp.checkbox.isChecked,
layout.regexp.checkbox.isChecked,
layout.contacts.checkbox.isChecked
(layout.caseSensitivity.checkbox?.isChecked == true) && !regexChecked,
regexChecked,
layout.contacts.checkbox?.isChecked == true
)
)
}
@@ -3,25 +3,29 @@ package dev.octoshrimpy.quik.feature.blocking.manager
import android.app.Activity
import android.app.AlertDialog
import android.content.res.ColorStateList
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isInvisible
import com.jakewharton.rxbinding2.view.clicks
import dev.octoshrimpy.quik.R
import dev.octoshrimpy.quik.common.base.QkController
import dev.octoshrimpy.quik.common.util.Colors
import dev.octoshrimpy.quik.common.util.extensions.resolveThemeColor
import dev.octoshrimpy.quik.databinding.BlockingManagerControllerBinding
import dev.octoshrimpy.quik.injection.appComponent
import dev.octoshrimpy.quik.util.Preferences
import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.subjects.PublishSubject
import kotlinx.android.synthetic.main.blocking_manager_controller.*
import kotlinx.android.synthetic.main.blocking_manager_list_option.view.*
import javax.inject.Inject
class BlockingManagerController : QkController<BlockingManagerView, BlockingManagerState, BlockingManagerPresenter>(),
class BlockingManagerController : QkController<BlockingManagerControllerBinding, BlockingManagerView, BlockingManagerState, BlockingManagerPresenter>(),
BlockingManagerView {
override fun inflateBinding(inflater: LayoutInflater, container: ViewGroup): BlockingManagerControllerBinding =
BlockingManagerControllerBinding.inflate(inflater, container, false)
@Inject lateinit var colors: Colors
@Inject override lateinit var presenter: BlockingManagerPresenter
@@ -30,7 +34,6 @@ class BlockingManagerController : QkController<BlockingManagerView, BlockingMana
init {
appComponent.inject(this)
retainViewMode = RetainViewMode.RETAIN_DETACH
layoutRes = R.layout.blocking_manager_controller
}
override fun onAttach(view: View) {
@@ -46,10 +49,10 @@ class BlockingManagerController : QkController<BlockingManagerView, BlockingMana
val textTertiary = view.context.resolveThemeColor(android.R.attr.textColorTertiary)
val imageTintList = ColorStateList(states, intArrayOf(colors.theme().theme, textTertiary))
qksms.action.imageTintList = imageTintList
callBlocker.action.imageTintList = imageTintList
callControl.action.imageTintList = imageTintList
shouldIAnswer.action.imageTintList = imageTintList
binding.qksms.actionView.imageTintList = imageTintList
binding.callBlocker.actionView.imageTintList = imageTintList
binding.callControl.actionView.imageTintList = imageTintList
binding.shouldIAnswer.actionView.imageTintList = imageTintList
}
override fun onActivityResumed(activity: Activity) {
@@ -57,23 +60,23 @@ class BlockingManagerController : QkController<BlockingManagerView, BlockingMana
}
override fun render(state: BlockingManagerState) {
qksms.action.setImageResource(getActionIcon(true))
qksms.action.isActivated = true
qksms.action.isInvisible = state.blockingManager != Preferences.BLOCKING_MANAGER_QKSMS
binding.qksms.actionView.setImageResource(getActionIcon(true))
binding.qksms.actionView.isActivated = true
binding.qksms.actionView.isInvisible = state.blockingManager != Preferences.BLOCKING_MANAGER_QKSMS
callBlocker.action.setImageResource(getActionIcon(state.callBlockerInstalled))
callBlocker.action.isActivated = state.callBlockerInstalled
callBlocker.action.isInvisible = state.blockingManager != Preferences.BLOCKING_MANAGER_CB
binding.callBlocker.actionView.setImageResource(getActionIcon(state.callBlockerInstalled))
binding.callBlocker.actionView.isActivated = state.callBlockerInstalled
binding.callBlocker.actionView.isInvisible = state.blockingManager != Preferences.BLOCKING_MANAGER_CB
&& state.callBlockerInstalled
callControl.action.setImageResource(getActionIcon(state.callControlInstalled))
callControl.action.isActivated = state.callControlInstalled
callControl.action.isInvisible = state.blockingManager != Preferences.BLOCKING_MANAGER_CC
binding.callControl.actionView.setImageResource(getActionIcon(state.callControlInstalled))
binding.callControl.actionView.isActivated = state.callControlInstalled
binding.callControl.actionView.isInvisible = state.blockingManager != Preferences.BLOCKING_MANAGER_CC
&& state.callControlInstalled
shouldIAnswer.action.setImageResource(getActionIcon(state.siaInstalled))
shouldIAnswer.action.isActivated = state.siaInstalled
shouldIAnswer.action.isInvisible = state.blockingManager != Preferences.BLOCKING_MANAGER_SIA
binding.shouldIAnswer.actionView.setImageResource(getActionIcon(state.siaInstalled))
binding.shouldIAnswer.actionView.isActivated = state.siaInstalled
binding.shouldIAnswer.actionView.isInvisible = state.blockingManager != Preferences.BLOCKING_MANAGER_SIA
&& state.siaInstalled
}
@@ -83,10 +86,10 @@ class BlockingManagerController : QkController<BlockingManagerView, BlockingMana
}
override fun activityResumed(): Observable<*> = activityResumedSubject
override fun qksmsClicked(): Observable<*> = qksms.clicks()
override fun callBlockerClicked(): Observable<*> = callBlocker.clicks()
override fun callControlClicked(): Observable<*> = callControl.clicks()
override fun siaClicked(): Observable<*> = shouldIAnswer.clicks()
override fun qksmsClicked(): Observable<*> = binding.qksms.clicks()
override fun callBlockerClicked(): Observable<*> = binding.callBlocker.clicks()
override fun callControlClicked(): Observable<*> = binding.callControl.clicks()
override fun siaClicked(): Observable<*> = binding.shouldIAnswer.clicks()
override fun showCopyDialog(manager: String): Single<Boolean> = Single.create { emitter ->
AlertDialog.Builder(activity)
@@ -39,6 +39,8 @@ class BlockingManagerPreferenceView @JvmOverloads constructor(
private val binding: BlockingManagerPreferenceViewBinding =
BlockingManagerPreferenceViewBinding.inflate(LayoutInflater.from(context), this)
val actionView: ImageView get() = binding.widgetFrame.findViewById(R.id.action)
var icon: Drawable? = null
set(value) {
field = value
@@ -24,69 +24,69 @@ import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.view.isVisible
import dev.octoshrimpy.quik.R
import dev.octoshrimpy.quik.common.base.QkBindingViewHolder
import dev.octoshrimpy.quik.common.base.QkRealmAdapter
import dev.octoshrimpy.quik.common.base.QkViewHolder
import dev.octoshrimpy.quik.common.util.DateFormatter
import dev.octoshrimpy.quik.common.util.extensions.resolveThemeColor
import dev.octoshrimpy.quik.databinding.BlockedListItemBinding
import dev.octoshrimpy.quik.model.Conversation
import dev.octoshrimpy.quik.util.Preferences
import io.reactivex.subjects.PublishSubject
import kotlinx.android.synthetic.main.blocked_list_item.*
import kotlinx.android.synthetic.main.blocked_list_item.view.*
import javax.inject.Inject
class BlockedMessagesAdapter @Inject constructor(
private val context: Context,
private val dateFormatter: DateFormatter
) : QkRealmAdapter<Conversation, QkViewHolder>() {
) : QkRealmAdapter<Conversation, QkBindingViewHolder<BlockedListItemBinding>>() {
val clicks: PublishSubject<Long> = PublishSubject.create()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.blocked_list_item, parent, false)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkBindingViewHolder<BlockedListItemBinding> {
val binding = BlockedListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
if (viewType == 0) {
view.title.setTypeface(view.title.typeface, Typeface.BOLD)
view.date.setTypeface(view.date.typeface, Typeface.BOLD)
view.date.setTextColor(view.context.resolveThemeColor(android.R.attr.textColorPrimary))
binding.title.setTypeface(binding.title.typeface, Typeface.BOLD)
binding.date.setTypeface(binding.date.typeface, Typeface.BOLD)
binding.date.setTextColor(binding.root.context.resolveThemeColor(android.R.attr.textColorPrimary))
}
return QkViewHolder(view).apply {
view.setOnClickListener {
return QkBindingViewHolder(binding).apply {
binding.root.setOnClickListener {
val conversation = getItem(adapterPosition) ?: return@setOnClickListener
when (toggleSelection(conversation.id, false)) {
true -> view.isActivated = isSelected(conversation.id)
true -> binding.root.isActivated = isSelected(conversation.id)
false -> clicks.onNext(conversation.id)
}
}
view.setOnLongClickListener {
binding.root.setOnLongClickListener {
val conversation = getItem(adapterPosition) ?: return@setOnLongClickListener true
toggleSelection(conversation.id)
view.isActivated = isSelected(conversation.id)
binding.root.isActivated = isSelected(conversation.id)
true
}
}
}
override fun onBindViewHolder(holder: QkViewHolder, position: Int) {
override fun onBindViewHolder(holder: QkBindingViewHolder<BlockedListItemBinding>, position: Int) {
val conversation = getItem(position) ?: return
val binding = holder.binding
holder.containerView.isActivated = isSelected(conversation.id)
holder.itemView.isActivated = isSelected(conversation.id)
holder.avatars.recipients = conversation.recipients
holder.title.collapseEnabled = conversation.recipients.size > 1
holder.title.text = conversation.getTitle()
holder.date.text = dateFormatter.getConversationTimestamp(conversation.date)
binding.avatars.recipients = conversation.recipients
binding.title.collapseEnabled = conversation.recipients.size > 1
binding.title.text = conversation.getTitle()
binding.date.text = dateFormatter.getConversationTimestamp(conversation.date)
holder.blocker.text = when (conversation.blockingClient) {
binding.blocker.text = when (conversation.blockingClient) {
Preferences.BLOCKING_MANAGER_CC -> context.getString(R.string.blocking_manager_call_control_title)
Preferences.BLOCKING_MANAGER_SIA -> context.getString(R.string.blocking_manager_sia_title)
else -> null
}
holder.reason.text = conversation.blockReason
holder.blocker.isVisible = holder.blocker.text.isNotEmpty()
holder.reason.isVisible = holder.blocker.text.isNotEmpty()
binding.reason.text = conversation.blockReason
binding.blocker.isVisible = binding.blocker.text.isNotEmpty()
binding.reason.isVisible = binding.blocker.text.isNotEmpty()
}
override fun getItemViewType(position: Int): Int {
@@ -19,25 +19,30 @@
package dev.octoshrimpy.quik.feature.blocking.messages
import android.content.Context
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar
import dev.octoshrimpy.quik.R
import dev.octoshrimpy.quik.common.base.QkController
import dev.octoshrimpy.quik.common.util.Colors
import dev.octoshrimpy.quik.databinding.BlockedMessagesControllerBinding
import dev.octoshrimpy.quik.feature.blocking.BlockingDialog
import dev.octoshrimpy.quik.injection.appComponent
import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.Subject
import kotlinx.android.synthetic.main.blocked_messages_controller.*
import kotlinx.android.synthetic.main.container_activity.*
import javax.inject.Inject
class BlockedMessagesController : QkController<BlockedMessagesView, BlockedMessagesState, BlockedMessagesPresenter>(),
class BlockedMessagesController : QkController<BlockedMessagesControllerBinding, BlockedMessagesView, BlockedMessagesState, BlockedMessagesPresenter>(),
BlockedMessagesView {
override fun inflateBinding(inflater: LayoutInflater, container: ViewGroup): BlockedMessagesControllerBinding =
BlockedMessagesControllerBinding.inflate(inflater, container, false)
override val menuReadyIntent: Subject<Unit> = PublishSubject.create()
override val optionsItemIntent: Subject<Int> = PublishSubject.create()
override val conversationClicks by lazy { blockedMessagesAdapter.clicks }
@@ -54,13 +59,12 @@ class BlockedMessagesController : QkController<BlockedMessagesView, BlockedMessa
init {
appComponent.inject(this)
retainViewMode = RetainViewMode.RETAIN_DETACH
layoutRes = R.layout.blocked_messages_controller
}
override fun onViewCreated() {
super.onViewCreated()
blockedMessagesAdapter.emptyView = empty
conversations.adapter = blockedMessagesAdapter
blockedMessagesAdapter.emptyView = binding.empty
binding.conversations.adapter = blockedMessagesAdapter
}
override fun onAttach(view: View) {
@@ -90,8 +94,9 @@ class BlockedMessagesController : QkController<BlockedMessagesView, BlockedMessa
override fun render(state: BlockedMessagesState) {
blockedMessagesAdapter.updateData(state.data)
themedActivity?.toolbar?.menu?.findItem(R.id.block)?.isVisible = state.selected > 0
themedActivity?.toolbar?.menu?.findItem(R.id.delete)?.isVisible = state.selected > 0
val toolbarMenu = themedActivity?.findViewById<Toolbar>(R.id.toolbar)?.menu
toolbarMenu?.findItem(R.id.block)?.isVisible = state.selected > 0
toolbarMenu?.findItem(R.id.delete)?.isVisible = state.selected > 0
setTitle(when (state.selected) {
0 -> context.getString(R.string.blocked_messages_title)
@@ -20,6 +20,7 @@ package dev.octoshrimpy.quik.feature.blocking.numbers
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import com.jakewharton.rxbinding2.view.clicks
import dev.octoshrimpy.quik.R
@@ -32,11 +33,11 @@ import dev.octoshrimpy.quik.util.PhoneNumberUtils
import io.reactivex.Observable
import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.Subject
import kotlinx.android.synthetic.main.blocked_numbers_add_dialog.view.*
import kotlinx.android.synthetic.main.blocked_numbers_controller.*
import dev.octoshrimpy.quik.databinding.BlockedNumbersControllerBinding
import dev.octoshrimpy.quik.databinding.BlockedNumbersAddDialogBinding
import javax.inject.Inject
class BlockedNumbersController : QkController<BlockedNumbersView, BlockedNumbersState, BlockedNumbersPresenter>(),
class BlockedNumbersController : QkController<BlockedNumbersControllerBinding, BlockedNumbersView, BlockedNumbersState, BlockedNumbersPresenter>(),
BlockedNumbersView {
@Inject override lateinit var presenter: BlockedNumbersPresenter
@@ -49,9 +50,11 @@ class BlockedNumbersController : QkController<BlockedNumbersView, BlockedNumbers
init {
appComponent.inject(this)
retainViewMode = RetainViewMode.RETAIN_DETACH
layoutRes = R.layout.blocked_numbers_controller
}
override fun inflateBinding(inflater: LayoutInflater, container: ViewGroup): BlockedNumbersControllerBinding =
BlockedNumbersControllerBinding.inflate(inflater, container, false)
override fun onAttach(view: View) {
super.onAttach(view)
presenter.bindIntents(this)
@@ -61,10 +64,10 @@ class BlockedNumbersController : QkController<BlockedNumbersView, BlockedNumbers
override fun onViewCreated() {
super.onViewCreated()
add.setBackgroundTint(colors.theme().theme)
add.setTint(colors.theme().textPrimary)
adapter.emptyView = empty
numbers.adapter = adapter
binding.add.setBackgroundTint(colors.theme().theme)
binding.add.setTint(colors.theme().textPrimary)
adapter.emptyView = binding.empty
binding.numbers.adapter = adapter
}
override fun render(state: BlockedNumbersState) {
@@ -72,14 +75,14 @@ class BlockedNumbersController : QkController<BlockedNumbersView, BlockedNumbers
}
override fun unblockAddress(): Observable<Long> = adapter.unblockAddress
override fun addAddress(): Observable<*> = add.clicks()
override fun addAddress(): Observable<*> = binding.add.clicks()
override fun saveAddress(): Observable<String> = saveAddressSubject
override fun showAddDialog() {
val layout = LayoutInflater.from(activity).inflate(R.layout.blocked_numbers_add_dialog, null)
val layout = BlockedNumbersAddDialogBinding.inflate(LayoutInflater.from(activity))
val textWatcher = BlockedNumberTextWatcher(layout.input, phoneNumberUtils)
val dialog = AlertDialog.Builder(activity!!)
.setView(layout)
.setView(layout.root)
.setPositiveButton(R.string.blocked_numbers_dialog_block) { _, _ ->
saveAddressSubject.onNext(layout.input.text.toString())
}
@@ -75,7 +75,6 @@ import dev.octoshrimpy.quik.common.util.extensions.setTint
import dev.octoshrimpy.quik.common.util.extensions.setVisible
import dev.octoshrimpy.quik.common.util.extensions.showKeyboard
import dev.octoshrimpy.quik.common.widget.MicInputCloudView
import dev.octoshrimpy.quik.common.widget.QkEditText
import dev.octoshrimpy.quik.extensions.mapNotNull
import dev.octoshrimpy.quik.feature.compose.editing.ChipsAdapter
import dev.octoshrimpy.quik.feature.contacts.ContactsActivity
@@ -85,60 +84,9 @@ import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import dev.octoshrimpy.quik.databinding.ComposeActivityBinding
import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.Subject
import kotlinx.android.synthetic.main.compose_activity.attach
import kotlinx.android.synthetic.main.compose_activity.attachAFileIcon
import kotlinx.android.synthetic.main.compose_activity.attachAFileLabel
import kotlinx.android.synthetic.main.compose_activity.attachAnAudioMessageIcon
import kotlinx.android.synthetic.main.compose_activity.attachAnAudioMessageLabel
import kotlinx.android.synthetic.main.compose_activity.attaching
import kotlinx.android.synthetic.main.compose_activity.audioMsgAbort
import kotlinx.android.synthetic.main.compose_activity.audioMsgAttach
import kotlinx.android.synthetic.main.compose_activity.audioMsgBackground
import kotlinx.android.synthetic.main.compose_activity.audioMsgBluetooth
import kotlinx.android.synthetic.main.compose_activity.audioMsgDuration
import kotlinx.android.synthetic.main.compose_activity.audioMsgPlayerBackground
import kotlinx.android.synthetic.main.compose_activity.audioMsgPlayerPlayPause
import kotlinx.android.synthetic.main.compose_activity.audioMsgPlayerSeekBar
import kotlinx.android.synthetic.main.compose_activity.audioMsgRecord
import kotlinx.android.synthetic.main.compose_activity.camera
import kotlinx.android.synthetic.main.compose_activity.cameraLabel
import kotlinx.android.synthetic.main.compose_activity.chips
import kotlinx.android.synthetic.main.compose_activity.composeBar
import kotlinx.android.synthetic.main.compose_activity.contact
import kotlinx.android.synthetic.main.compose_activity.contactLabel
import kotlinx.android.synthetic.main.compose_activity.contentView
import kotlinx.android.synthetic.main.compose_activity.counter
import kotlinx.android.synthetic.main.compose_activity.gallery
import kotlinx.android.synthetic.main.compose_activity.galleryLabel
import kotlinx.android.synthetic.main.compose_activity.loading
import kotlinx.android.synthetic.main.compose_activity.message
import kotlinx.android.synthetic.main.compose_activity.messageAttachments
import kotlinx.android.synthetic.main.compose_activity.messageList
import kotlinx.android.synthetic.main.compose_activity.messagesEmpty
import kotlinx.android.synthetic.main.compose_activity.noValidRecipients
import kotlinx.android.synthetic.main.compose_activity.recordAudioMsg
import kotlinx.android.synthetic.main.compose_activity.schedule
import kotlinx.android.synthetic.main.compose_activity.scheduleLabel
import kotlinx.android.synthetic.main.compose_activity.scheduledCancel
import kotlinx.android.synthetic.main.compose_activity.scheduledGroup
import kotlinx.android.synthetic.main.compose_activity.scheduledTime
import kotlinx.android.synthetic.main.compose_activity.scheduledSend
import kotlinx.android.synthetic.main.compose_activity.send
import kotlinx.android.synthetic.main.compose_activity.sendAsGroup
import kotlinx.android.synthetic.main.compose_activity.sendAsGroupBackground
import kotlinx.android.synthetic.main.compose_activity.sendAsGroupSummary
import kotlinx.android.synthetic.main.compose_activity.sendAsGroupSwitch
import kotlinx.android.synthetic.main.compose_activity.shadeBackground
import kotlinx.android.synthetic.main.compose_activity.sim
import kotlinx.android.synthetic.main.compose_activity.simIndex
import kotlinx.android.synthetic.main.compose_activity.speechToTextFrame
import kotlinx.android.synthetic.main.compose_activity.speechToTextIcon
import kotlinx.android.synthetic.main.compose_activity.speechToTextIconBorder
import kotlinx.android.synthetic.main.compose_activity.toolbarSubtitle
import kotlinx.android.synthetic.main.compose_activity.toolbarTitle
import kotlinx.android.synthetic.main.main_activity.toolbar
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
@@ -156,6 +104,8 @@ class ComposeActivity : QkThemedActivity(), ComposeView {
@Inject lateinit var navigator: Navigator
@Inject lateinit var viewModelFactory: ViewModelProvider.Factory
private lateinit var binding: ComposeActivityBinding
override val activityVisibleIntent: Subject<Boolean> = PublishSubject.create()
override val chipsSelectedIntent: Subject<HashMap<String, String?>> = PublishSubject.create()
override val chipDeletedIntent: Subject<Recipient> by lazy { chipsAdapter.chipDeleted }
@@ -163,7 +113,7 @@ class ComposeActivity : QkThemedActivity(), ComposeView {
override val optionsItemIntent: Subject<Int> = PublishSubject.create()
override val contextItemIntent: Subject<MenuItem> = PublishSubject.create()
override val scheduleAction: Subject<Boolean> = PublishSubject.create()
override val sendAsGroupIntent by lazy { sendAsGroupBackground.clicks() }
override val sendAsGroupIntent by lazy { binding.sendAsGroupBackground.clicks() }
override val messagePartClickIntent: Subject<Long> by lazy { messageAdapter.partClicks }
override val messagePartContextMenuRegistrar: Subject<View> by lazy { messageAdapter.partContextMenuRegistrar }
override val messagesSelectedIntent by lazy { messageAdapter.selectionChanges }
@@ -171,36 +121,36 @@ class ComposeActivity : QkThemedActivity(), ComposeView {
override val sendDelayedNowIntent: Subject<Long> by lazy { messageAdapter.sendNowClicks }
override val resendIntent: Subject<Long> by lazy { messageAdapter.resendClicks }
override val attachmentDeletedIntent: Subject<Attachment> by lazy { composeAttachmentAdapter.attachmentDeleted }
override val textChangedIntent by lazy { message.textChanges() }
override val attachIntent: Observable<Unit> by lazy { Observable.merge(attach.clicks(), shadeBackground.clicks()) }
override val cameraIntent: Observable<Unit> by lazy { Observable.merge(camera.clicks(), cameraLabel.clicks()) }
override val attachImageFileIntent: Observable<Unit> by lazy { Observable.merge(gallery.clicks(), galleryLabel.clicks()) }
override val attachAnyFileIntent: Observable<Unit> by lazy { Observable.merge(attachAFileIcon.clicks(), attachAFileLabel.clicks()) }
override val scheduleIntent: Observable<Unit> by lazy { Observable.merge(schedule.clicks(), scheduleLabel.clicks()) }
override val attachContactIntent: Observable<Unit> by lazy { Observable.merge(contact.clicks(), contactLabel.clicks()) }
override val textChangedIntent by lazy { binding.message.textChanges() }
override val attachIntent: Observable<Unit> by lazy { Observable.merge(binding.attach.clicks(), binding.shadeBackground.clicks()) }
override val cameraIntent: Observable<Unit> by lazy { Observable.merge(binding.camera.clicks(), binding.cameraLabel.clicks()) }
override val attachImageFileIntent: Observable<Unit> by lazy { Observable.merge(binding.gallery.clicks(), binding.galleryLabel.clicks()) }
override val attachAnyFileIntent: Observable<Unit> by lazy { Observable.merge(binding.attachAFileIcon.clicks(), binding.attachAFileLabel.clicks()) }
override val scheduleIntent: Observable<Unit> by lazy { Observable.merge(binding.schedule.clicks(), binding.scheduleLabel.clicks()) }
override val attachContactIntent: Observable<Unit> by lazy { Observable.merge(binding.contact.clicks(), binding.contactLabel.clicks()) }
override val attachAnyFileSelectedIntent: Subject<Uri> = PublishSubject.create()
override val contactSelectedIntent: Subject<Uri> = PublishSubject.create()
override val inputContentIntent by lazy { message.inputContentSelected }
override val inputContentIntent by lazy { binding.message.inputContentSelected }
override val scheduleSelectedIntent: Subject<Long> = PublishSubject.create()
override val changeSimIntent by lazy { sim.clicks() }
override val scheduleCancelIntent by lazy { scheduledCancel.clicks() }
override val sendIntent by lazy { Observable.merge(send.clicks(), scheduledSend.clicks()) }
override val changeSimIntent by lazy { binding.sim.clicks() }
override val scheduleCancelIntent by lazy { binding.scheduledCancel.clicks() }
override val sendIntent by lazy { Observable.merge(binding.send.clicks(), binding.scheduledSend.clicks()) }
override val viewQksmsPlusIntent: Subject<Unit> = PublishSubject.create()
override val backPressedIntent: Subject<Unit> = PublishSubject.create()
override val confirmDeleteIntent: Subject<List<Long>> = PublishSubject.create()
override val clearCurrentMessageIntent: Subject<Boolean> = PublishSubject.create()
override val messageLinkAskIntent: Subject<Uri> by lazy { messageAdapter.messageLinkClicks }
override val reactionClickIntent: Subject<Long> by lazy { messageAdapter.reactionClicks }
override val speechRecogniserIntent by lazy { speechToTextIcon.clicks() }
override val shadeIntent by lazy { shadeBackground.clicks() }
override val speechRecogniserIntent by lazy { binding.speechToTextIcon.clicks() }
override val shadeIntent by lazy { binding.shadeBackground.clicks() }
override val recordAudioStartStopRecording: Subject<Boolean> = PublishSubject.create()
override val recordAnAudioMessage: Observable<Unit> by lazy {
Observable.merge(recordAudioMsg.clicks(),
attachAnAudioMessageIcon.clicks(),
attachAnAudioMessageLabel.clicks())
Observable.merge(binding.recordAudioMsg.clicks(),
binding.attachAnAudioMessageIcon.clicks(),
binding.attachAnAudioMessageLabel.clicks())
}
override val recordAudioAbort by lazy { audioMsgAbort.clicks() }
override val recordAudioAttach by lazy { audioMsgAttach.clicks() }
override val recordAudioAbort by lazy { binding.audioMsgAbort.clicks() }
override val recordAudioAttach by lazy { binding.audioMsgAttach.clicks() }
override val recordAudioPlayerPlayPause: Subject<QkMediaPlayer.PlayingState> = PublishSubject.create()
override val recordAudioPlayerConfigUI: Subject<QkMediaPlayer.PlayingState> = PublishSubject.create()
override val recordAudioPlayerVisible: Subject<Boolean> = PublishSubject.create()
@@ -228,56 +178,57 @@ class ComposeActivity : QkThemedActivity(), ComposeView {
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.compose_activity)
binding = ComposeActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
showBackButton(true)
viewModel.bindView(this)
contentView.layoutTransition = LayoutTransition().apply {
binding.contentView.layoutTransition = LayoutTransition().apply {
disableTransitionType(LayoutTransition.CHANGING)
}
chipsAdapter.view = chips
chipsAdapter.view = binding.chips
chips.itemAnimator = null
chips.layoutManager = FlexboxLayoutManager(this)
binding.chips.itemAnimator = null
binding.chips.layoutManager = FlexboxLayoutManager(this)
messageAdapter.autoScrollToStart(messageList)
messageAdapter.emptyView = messagesEmpty
messageAdapter.autoScrollToStart(binding.messageList)
messageAdapter.emptyView = binding.messagesEmpty
messageList.setHasFixedSize(true)
messageList.adapter = messageAdapter
binding.messageList.setHasFixedSize(true)
binding.messageList.adapter = messageAdapter
messageAttachments.adapter = composeAttachmentAdapter
binding.messageAttachments.adapter = composeAttachmentAdapter
message.supportsInputContent = true
binding.message.supportsInputContent = true
theme
.doOnNext {
loading.setTint(it.theme)
binding.loading.setTint(it.theme)
// entire attach menu
attach.setBackgroundTint(it.theme); attach.setTint(it.textPrimary)
contact.setBackgroundTint(it.theme); contact.setTint(it.textPrimary)
contactLabel.setBackgroundTint(it.theme); contactLabel.setTint(it.textPrimary)
schedule.setBackgroundTint(it.theme); schedule.setTint(it.textPrimary)
scheduleLabel.setBackgroundTint(it.theme); scheduleLabel.setTint(it.textPrimary)
attachAFileIcon.setBackgroundTint(it.theme); attachAFileIcon.setTint(it.textPrimary)
attachAFileLabel.setBackgroundTint(it.theme); attachAFileLabel.setTint(it.textPrimary)
attachAnAudioMessageIcon.setBackgroundTint(it.theme); attachAnAudioMessageIcon.setTint(it.textPrimary)
attachAnAudioMessageLabel.setBackgroundTint(it.theme); attachAnAudioMessageLabel.setTint(it.textPrimary)
gallery.setBackgroundTint(it.theme); gallery.setTint(it.textPrimary)
galleryLabel.setBackgroundTint(it.theme); galleryLabel.setTint(it.textPrimary)
camera.setBackgroundTint(it.theme); camera.setTint(it.textPrimary)
cameraLabel.setBackgroundTint(it.theme); cameraLabel.setTint(it.textPrimary)
binding.attach.setBackgroundTint(it.theme); binding.attach.setTint(it.textPrimary)
binding.contact.setBackgroundTint(it.theme); binding.contact.setTint(it.textPrimary)
binding.contactLabel.setBackgroundTint(it.theme); binding.contactLabel.setTint(it.textPrimary)
binding.schedule.setBackgroundTint(it.theme); binding.schedule.setTint(it.textPrimary)
binding.scheduleLabel.setBackgroundTint(it.theme); binding.scheduleLabel.setTint(it.textPrimary)
binding.attachAFileIcon.setBackgroundTint(it.theme); binding.attachAFileIcon.setTint(it.textPrimary)
binding.attachAFileLabel.setBackgroundTint(it.theme); binding.attachAFileLabel.setTint(it.textPrimary)
binding.attachAnAudioMessageIcon.setBackgroundTint(it.theme); binding.attachAnAudioMessageIcon.setTint(it.textPrimary)
binding.attachAnAudioMessageLabel.setBackgroundTint(it.theme); binding.attachAnAudioMessageLabel.setTint(it.textPrimary)
binding.gallery.setBackgroundTint(it.theme); binding.gallery.setTint(it.textPrimary)
binding.galleryLabel.setBackgroundTint(it.theme); binding.galleryLabel.setTint(it.textPrimary)
binding.camera.setBackgroundTint(it.theme); binding.camera.setTint(it.textPrimary)
binding.cameraLabel.setBackgroundTint(it.theme); binding.cameraLabel.setTint(it.textPrimary)
// speech to text floating button
speechToTextIconBorder.setBackgroundTint(it.theme)
speechToTextIcon.setBackgroundTint(it.textPrimary)
speechToTextIcon.setTint(it.theme)
binding.speechToTextIconBorder.setBackgroundTint(it.theme)
binding.speechToTextIcon.setBackgroundTint(it.textPrimary)
binding.speechToTextIcon.setTint(it.theme)
// audio message recording
audioMsgRecord.setColor(it.theme)
audioMsgPlayerPlayPause.setTint(it.theme)
audioMsgPlayerSeekBar.apply {
binding.audioMsgRecord.setColor(it.theme)
binding.audioMsgPlayerPlayPause.setTint(it.theme)
binding.audioMsgPlayerSeekBar.apply {
thumbTintList = ColorStateList.valueOf(it.theme)
progressBackgroundTintList = ColorStateList.valueOf(it.theme)
progressTintList = ColorStateList.valueOf(it.theme)
@@ -296,23 +247,23 @@ class ComposeActivity : QkThemedActivity(), ComposeView {
// drag drop handlers for speech-to-text icon
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
speechToTextIcon.setOnLongClickListener {
it.startDragAndDrop(null, View.DragShadowBuilder(speechToTextFrame), null, 0)
speechToTextFrame.isVisible = false
binding.speechToTextIcon.setOnLongClickListener {
it.startDragAndDrop(null, View.DragShadowBuilder(binding.speechToTextFrame), null, 0)
binding.speechToTextFrame.isVisible = false
contentView.setOnDragListener { _, event ->
binding.contentView.setOnDragListener { _, event ->
when (event.action) {
ACTION_DROP -> {
speechToTextFrame.x = (event.x - (speechToTextFrame.width / 2))
speechToTextFrame.y = (event.y - (speechToTextFrame.height / 2))
binding.speechToTextFrame.x = (event.x - (binding.speechToTextFrame.width / 2))
binding.speechToTextFrame.y = (event.y - (binding.speechToTextFrame.height / 2))
// get offset from root view as a percentage of root view for saving
prefs.showSttOffsetX.set((speechToTextFrame.x - contentView.x) / contentView.width)
prefs.showSttOffsetY.set((speechToTextFrame.y - contentView.y) / contentView.height)
prefs.showSttOffsetX.set((binding.speechToTextFrame.x - binding.contentView.x) / binding.contentView.width)
prefs.showSttOffsetY.set((binding.speechToTextFrame.y - binding.contentView.y) / binding.contentView.height)
}
ACTION_DRAG_ENDED, ACTION_DRAG_EXITED -> {
speechToTextFrame.isVisible = true
binding.speechToTextFrame.isVisible = true
}
}
true
@@ -322,8 +273,8 @@ class ComposeActivity : QkThemedActivity(), ComposeView {
}
// start/stop audio message recording
audioMsgRecord.setOnClickListener {
recordAudioRecord.onNext(audioMsgRecord.getState())
binding.audioMsgRecord.setOnClickListener {
recordAudioRecord.onNext(binding.audioMsgRecord.getState())
}
recordAudioChronometer
@@ -332,17 +283,17 @@ class ComposeActivity : QkThemedActivity(), ComposeView {
.autoDisposable(scope())
.subscribe {
if (it) {
audioMsgDuration.base = SystemClock.elapsedRealtime()
audioMsgDuration.start()
binding.audioMsgDuration.base = SystemClock.elapsedRealtime()
binding.audioMsgDuration.start()
} else {
audioMsgDuration.stop()
binding.audioMsgDuration.stop()
}
}
// audio record playback play/pause button
audioMsgPlayerPlayPause.setOnClickListener {
binding.audioMsgPlayerPlayPause.setOnClickListener {
recordAudioPlayerPlayPause.onNext(
audioMsgPlayerPlayPause.tag as QkMediaPlayer.PlayingState
binding.audioMsgPlayerPlayPause.tag as QkMediaPlayer.PlayingState
)
}
@@ -351,10 +302,10 @@ class ComposeActivity : QkThemedActivity(), ComposeView {
.distinctUntilChanged()
.autoDisposable(scope())
.subscribe {
audioMsgRecord.isVisible = it
audioMsgDuration.isVisible =
binding.audioMsgRecord.isVisible = it
binding.audioMsgDuration.isVisible =
it // chronometer follows record button visibility
audioMsgBluetooth.isVisible = !it
binding.audioMsgBluetooth.isVisible = !it
}
recordAudioPlayerVisible
@@ -362,7 +313,7 @@ class ComposeActivity : QkThemedActivity(), ComposeView {
.distinctUntilChanged()
.autoDisposable(scope())
.subscribe {
audioMsgPlayerBackground.isVisible = it
binding.audioMsgPlayerBackground.isVisible = it
recordAudioPlayerConfigUI.onNext(QkMediaPlayer.PlayingState.Stopped)
}
@@ -373,35 +324,35 @@ class ComposeActivity : QkThemedActivity(), ComposeView {
.subscribe {
when (it) {
QkMediaPlayer.PlayingState.Playing -> {
audioMsgPlayerPlayPause.tag = QkMediaPlayer.PlayingState.Playing
binding.audioMsgPlayerPlayPause.tag = QkMediaPlayer.PlayingState.Playing
QkMediaPlayer.start()
audioMsgPlayerPlayPause.setImageResource(R.drawable.exo_icon_pause)
binding.audioMsgPlayerPlayPause.setImageResource(R.drawable.exo_icon_pause)
seekBarUpdater = getSeekBarUpdater().subscribe {
audioMsgPlayerSeekBar.progress = QkMediaPlayer.currentPosition
audioMsgPlayerSeekBar.max = QkMediaPlayer.duration
binding.audioMsgPlayerSeekBar.progress = QkMediaPlayer.currentPosition
binding.audioMsgPlayerSeekBar.max = QkMediaPlayer.duration
}
audioMsgPlayerSeekBar.isEnabled = true
binding.audioMsgPlayerSeekBar.isEnabled = true
}
QkMediaPlayer.PlayingState.Paused -> {
audioMsgPlayerPlayPause.tag = QkMediaPlayer.PlayingState.Paused
binding.audioMsgPlayerPlayPause.tag = QkMediaPlayer.PlayingState.Paused
QkMediaPlayer.pause()
audioMsgPlayerPlayPause.setImageResource(R.drawable.exo_icon_play)
binding.audioMsgPlayerPlayPause.setImageResource(R.drawable.exo_icon_play)
seekBarUpdater?.dispose()
}
else -> {
audioMsgPlayerPlayPause.tag = QkMediaPlayer.PlayingState.Stopped
binding.audioMsgPlayerPlayPause.tag = QkMediaPlayer.PlayingState.Stopped
QkMediaPlayer.reset()
audioMsgPlayerPlayPause.setImageResource(R.drawable.exo_icon_play)
binding.audioMsgPlayerPlayPause.setImageResource(R.drawable.exo_icon_play)
seekBarUpdater?.dispose()
audioMsgPlayerSeekBar.progress = 0
audioMsgPlayerSeekBar.isEnabled = false
binding.audioMsgPlayerSeekBar.progress = 0
binding.audioMsgPlayerSeekBar.isEnabled = false
}
}
}
// audio msg player seek bar handler
audioMsgPlayerSeekBar.setOnSeekBarChangeListener(
binding.audioMsgPlayerSeekBar.setOnSeekBarChangeListener(
object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(p0: SeekBar?, progress: Int, fromUser: Boolean) {
// if seek was initiated by the user and this part is currently playing
@@ -460,62 +411,62 @@ class ComposeActivity : QkThemedActivity(), ComposeView {
else -> state.conversationtitle
}
toolbarSubtitle.setVisible(state.query.isNotEmpty())
toolbarSubtitle.text = getString(R.string.compose_subtitle_results, state.searchSelectionPosition,
binding.toolbarSubtitle.setVisible(state.query.isNotEmpty())
binding.toolbarSubtitle.text = getString(R.string.compose_subtitle_results, state.searchSelectionPosition,
state.searchResults)
toolbarTitle.setVisible(!state.editingMode)
chips.setVisible(state.editingMode)
composeBar.setVisible(!state.loading)
binding.toolbarTitle.setVisible(!state.editingMode)
binding.chips.setVisible(state.editingMode)
binding.composeBar.setVisible(!state.loading)
// Don't set the adapters unless needed
if (state.editingMode && chips.adapter == null) chips.adapter = chipsAdapter
if (state.editingMode && binding.chips.adapter == null) binding.chips.adapter = chipsAdapter
toolbar.menu.findItem(R.id.viewScheduledMessages)?.isVisible = !state.editingMode && state.selectedMessages == 0
binding.toolbar.menu.findItem(R.id.viewScheduledMessages)?.isVisible = !state.editingMode && state.selectedMessages == 0
&& state.query.isEmpty() && state.hasScheduledMessages
toolbar.menu.findItem(R.id.select_all)?.isVisible = !state.editingMode && (messageAdapter.itemCount > 1) && state.selectedMessages != 0
toolbar.menu.findItem(R.id.add)?.isVisible = state.editingMode
toolbar.menu.findItem(R.id.call)?.isVisible = !state.editingMode && state.selectedMessages == 0
binding.toolbar.menu.findItem(R.id.select_all)?.isVisible = !state.editingMode && (messageAdapter.itemCount > 1) && state.selectedMessages != 0
binding.toolbar.menu.findItem(R.id.add)?.isVisible = state.editingMode
binding.toolbar.menu.findItem(R.id.call)?.isVisible = !state.editingMode && state.selectedMessages == 0
&& state.query.isEmpty()
toolbar.menu.findItem(R.id.info)?.isVisible = !state.editingMode && state.selectedMessages == 0
binding.toolbar.menu.findItem(R.id.info)?.isVisible = !state.editingMode && state.selectedMessages == 0
&& state.query.isEmpty()
toolbar.menu.findItem(R.id.copy)?.isVisible =
binding.toolbar.menu.findItem(R.id.copy)?.isVisible =
!state.editingMode && state.selectedMessages > 0 && state.selectedMessagesHaveText
toolbar.menu.findItem(R.id.share)?.isVisible =
binding.toolbar.menu.findItem(R.id.share)?.isVisible =
!state.editingMode && state.selectedMessages > 0 && state.selectedMessagesHaveText
toolbar.menu.findItem(R.id.details)?.isVisible = !state.editingMode && state.selectedMessages == 1
toolbar.menu.findItem(R.id.delete)?.isVisible = !state.editingMode && ((state.selectedMessages > 0) || state.canSend)
toolbar.menu.findItem(R.id.forward)?.isVisible = !state.editingMode && state.selectedMessages == 1
toolbar.menu.findItem(R.id.show_status)?.isVisible = !state.editingMode && state.selectedMessages > 0
toolbar.menu.findItem(R.id.previous)?.isVisible = state.selectedMessages == 0 && state.query.isNotEmpty()
toolbar.menu.findItem(R.id.next)?.isVisible = state.selectedMessages == 0 && state.query.isNotEmpty()
toolbar.menu.findItem(R.id.clear)?.isVisible = state.selectedMessages == 0 && state.query.isNotEmpty()
binding.toolbar.menu.findItem(R.id.details)?.isVisible = !state.editingMode && state.selectedMessages == 1
binding.toolbar.menu.findItem(R.id.delete)?.isVisible = !state.editingMode && ((state.selectedMessages > 0) || state.canSend)
binding.toolbar.menu.findItem(R.id.forward)?.isVisible = !state.editingMode && state.selectedMessages == 1
binding.toolbar.menu.findItem(R.id.show_status)?.isVisible = !state.editingMode && state.selectedMessages > 0
binding.toolbar.menu.findItem(R.id.previous)?.isVisible = state.selectedMessages == 0 && state.query.isNotEmpty()
binding.toolbar.menu.findItem(R.id.next)?.isVisible = state.selectedMessages == 0 && state.query.isNotEmpty()
binding.toolbar.menu.findItem(R.id.clear)?.isVisible = state.selectedMessages == 0 && state.query.isNotEmpty()
chipsAdapter.data = state.selectedChips
loading.setVisible(state.loading)
binding.loading.setVisible(state.loading)
sendAsGroup.setVisible(state.recipientCount > 1)
sendAsGroupSwitch.isChecked = state.sendAsGroup
sendAsGroupSummary.setText(
if (sendAsGroupSwitch.isChecked) R.string.compose_send_group_summary_on
binding.sendAsGroup.setVisible(state.recipientCount > 1)
binding.sendAsGroupSwitch.isChecked = state.sendAsGroup
binding.sendAsGroupSummary.setText(
if (binding.sendAsGroupSwitch.isChecked) R.string.compose_send_group_summary_on
else R.string.compose_send_group_summary_off
)
messageList.setVisible(!state.editingMode || state.sendAsGroup || state.selectedChips.size == 1)
binding.messageList.setVisible(!state.editingMode || state.sendAsGroup || state.selectedChips.size == 1)
messageAdapter.data = state.messages
messageAdapter.highlight = state.searchSelectionId
scheduledGroup.isVisible = state.scheduled != 0L
scheduledTime.text = dateFormatter.getScheduledTimestamp(state.scheduled)
binding.scheduledGroup.isVisible = state.scheduled != 0L
binding.scheduledTime.text = dateFormatter.getScheduledTimestamp(state.scheduled)
messageAttachments.setVisible(state.attachments.isNotEmpty())
binding.messageAttachments.setVisible(state.attachments.isNotEmpty())
composeAttachmentAdapter.data = state.attachments
attach.animate().rotation(if (state.attaching) 135f else 0f).start()
attaching.isVisible = state.attaching
binding.attach.animate().rotation(if (state.attaching) 135f else 0f).start()
binding.attaching.isVisible = state.attaching
shadeBackground.apply {
binding.shadeBackground.apply {
when {
state.attaching -> {
visibility = View.VISIBLE
@@ -532,31 +483,31 @@ class ComposeActivity : QkThemedActivity(), ComposeView {
}
// show or hide audio message recording panel and shade background
audioMsgBackground.isVisible = state.audioMsgRecording
binding.audioMsgBackground.isVisible = state.audioMsgRecording
counter.text = state.remaining
counter.setVisible(counter.text.isNotBlank())
binding.counter.text = state.remaining
binding.counter.setVisible(binding.counter.text.isNotBlank())
sim.setVisible(state.subscription != null)
sim.contentDescription = getString(R.string.compose_sim_cd, state.subscription?.displayName)
simIndex.text = state.subscription?.simSlotIndex?.plus(1)?.toString()
binding.sim.setVisible(state.subscription != null)
binding.sim.contentDescription = getString(R.string.compose_sim_cd, state.subscription?.displayName)
binding.simIndex.text = state.subscription?.simSlotIndex?.plus(1)?.toString()
// show either send, audio msg record, or sendScheduled button
send.visibility = if (state.canSend && !state.loading && state.scheduled == 0L) View.VISIBLE else View.INVISIBLE
recordAudioMsg.visibility = if (state.canSend && !state.loading) View.INVISIBLE else View.VISIBLE
scheduledSend.visibility = if (state.canSend && (state.scheduled != 0L) && !state.loading) View.VISIBLE else View.INVISIBLE
binding.send.visibility = if (state.canSend && !state.loading && state.scheduled == 0L) View.VISIBLE else View.INVISIBLE
binding.recordAudioMsg.visibility = if (state.canSend && !state.loading) View.INVISIBLE else View.VISIBLE
binding.scheduledSend.visibility = if (state.canSend && (state.scheduled != 0L) && !state.loading) View.VISIBLE else View.INVISIBLE
// if not in editing mode, and there are no non-me participants that can be sent to,
// hide controls that allow constructing a reply and inform user no valid recipients
if (!state.editingMode && (state.validRecipientNumbers == 0)) {
composeBar.visibility = View.GONE
sim.visibility = View.GONE
recordAudioMsg.visibility = View.GONE
noValidRecipients.visibility = View.VISIBLE
binding.composeBar.visibility = View.GONE
binding.sim.visibility = View.GONE
binding.recordAudioMsg.visibility = View.GONE
binding.noValidRecipients.visibility = View.VISIBLE
// change constraint of messageList to constrain bottom to top of noValidRecipients
ConstraintSet().apply {
clone(contentView)
clone(binding.contentView)
connect(
R.id.messageList,
ConstraintSet.BOTTOM,
@@ -564,7 +515,7 @@ class ComposeActivity : QkThemedActivity(), ComposeView {
ConstraintSet.TOP,
0
)
applyTo(contentView)
applyTo(binding.contentView)
}
}
@@ -574,15 +525,15 @@ class ComposeActivity : QkThemedActivity(), ComposeView {
// if stt is available and preference is set to show stt button
if (isSpeechRecognitionAvailable() && prefs.showStt.get()) {
speechToTextFrame.isVisible = true
binding.speechToTextFrame.isVisible = true
val xPercent = prefs.showSttOffsetX.get()
val yPercent = prefs.showSttOffsetY.get()
// if the stt icon has a custom position, move it
if ((xPercent != Float.MAX_VALUE) && (yPercent != Float.MAX_VALUE)) {
speechToTextFrame.x = (contentView.x + (xPercent * contentView.width))
speechToTextFrame.y = (contentView.y + (yPercent * contentView.height))
binding.speechToTextFrame.x = (binding.contentView.x + (xPercent * binding.contentView.width))
binding.speechToTextFrame.y = (binding.contentView.y + (yPercent * binding.contentView.height))
}
}
}
@@ -655,7 +606,7 @@ class ComposeActivity : QkThemedActivity(), ComposeView {
}, calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH)).show()
// On some devices, the keyboard can cover the date picker
message.hideKeyboard()
binding.message.hideKeyboard()
}
override fun requestContact() {
@@ -666,7 +617,7 @@ class ComposeActivity : QkThemedActivity(), ComposeView {
}
override fun showContacts(sharing: Boolean, chips: List<Recipient>) {
message.hideKeyboard()
binding.message.hideKeyboard()
val serialized = HashMap(chips.associate { chip -> chip.address to chip.contact?.lookupKey })
val intent = Intent(this, ContactsActivity::class.java)
.putExtra(ContactsActivity.SHARING_KEY, sharing)
@@ -688,12 +639,12 @@ class ComposeActivity : QkThemedActivity(), ComposeView {
}
override fun themeChanged() {
messageList.scrapViews()
binding.messageList.scrapViews()
}
override fun showKeyboard() {
message.postDelayed({
message.showKeyboard()
binding.message.postDelayed({
binding.message.showKeyboard()
}, 200)
}
@@ -718,19 +669,19 @@ class ComposeActivity : QkThemedActivity(), ComposeView {
}
override fun setDraft(draft: String) {
message.setText(draft)
message.setSelection(draft.length)
binding.message.setText(draft)
binding.message.setSelection(draft.length)
}
override fun scrollToMessage(id: Long) {
messageAdapter.data?.second
?.indexOfLast { message -> message.id == id }
?.takeIf { position -> position != -1 }
?.let(messageList::scrollToPosition)
?.let(binding.messageList::scrollToPosition)
}
override fun showQksmsPlusSnackbar(message: Int) {
Snackbar.make(contentView, message, Snackbar.LENGTH_LONG).run {
Snackbar.make(binding.contentView, message, Snackbar.LENGTH_LONG).run {
setAction(R.string.button_more) { viewQksmsPlusIntent.onNext(Unit) }
setActionTextColor(colors.theme().theme)
show()
@@ -825,14 +776,10 @@ class ComposeActivity : QkThemedActivity(), ComposeView {
// check returned results are good
val match = data?.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)
if ((match !== null) && (match.size > 0) && (!match[0].isNullOrEmpty())) {
// get the edit text view
val messageEditBox = findViewById<QkEditText>(R.id.message)
if (messageEditBox !== null) {
// populate message box with data returned by STT, set cursor to end, and focus
messageEditBox.append(match[0])
messageEditBox.setSelection(messageEditBox.text?.length ?: 0)
messageEditBox.requestFocus()
}
// populate message box with data returned by STT, set cursor to end, and focus
binding.message.append(match[0])
binding.message.setSelection(binding.message.text?.length ?: 0)
binding.message.requestFocus()
}
}
@@ -853,6 +800,6 @@ class ComposeActivity : QkThemedActivity(), ComposeView {
override fun onBackPressed() = backPressedIntent.onNext(Unit)
override fun focusMessage() {
message.requestFocus()
binding.message.requestFocus()
}
}
@@ -27,6 +27,8 @@ import dev.octoshrimpy.quik.R
import dev.octoshrimpy.quik.common.base.QkAdapter
import dev.octoshrimpy.quik.common.base.QkViewHolder
import dev.octoshrimpy.quik.common.util.extensions.getDisplayName
import dev.octoshrimpy.quik.databinding.AttachmentContactListItemBinding
import dev.octoshrimpy.quik.databinding.AttachmentFileListItemBinding
import dev.octoshrimpy.quik.extensions.getName
import dev.octoshrimpy.quik.feature.extensions.LoadBestIconIntoImageView
import dev.octoshrimpy.quik.feature.extensions.loadBestIconIntoImageView
@@ -34,9 +36,6 @@ import dev.octoshrimpy.quik.model.Attachment
import ezvcard.Ezvcard
import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.Subject
import kotlinx.android.synthetic.main.attachment_contact_list_item.*
import kotlinx.android.synthetic.main.scheduled_message_image_list_item.fileName
import kotlinx.android.synthetic.main.scheduled_message_image_list_item.thumbnail
import javax.inject.Inject
@@ -54,12 +53,11 @@ class ComposeAttachmentAdapter @Inject constructor(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkViewHolder {
val inflater = LayoutInflater.from(parent.context)
val view = inflater.inflate(
if (viewType == VIEW_TYPE_CONTACT) R.layout.attachment_contact_list_item
else R.layout.attachment_file_list_item,
parent,
false
)
val view = if (viewType == VIEW_TYPE_CONTACT) {
AttachmentContactListItemBinding.inflate(inflater, parent, false).root
} else {
AttachmentFileListItemBinding.inflate(inflater, parent, false).root
}
return QkViewHolder(view).apply {
view.setOnClickListener {
@@ -73,36 +71,38 @@ class ComposeAttachmentAdapter @Inject constructor(
val attachment = getItem(position)
if (attachment.isVCard(context)) {
val binding = AttachmentContactListItemBinding.bind(holder.itemView)
try {
val displayName = Ezvcard.parse(
String(attachment.getResourceBytes(context))
).first().getDisplayName() ?: ""
holder.name.text = displayName
holder.name.isVisible = displayName.isNotEmpty()
binding.name.text = displayName
binding.name.isVisible = displayName.isNotEmpty()
} catch (e: Exception) {
// npe from Ezvcard first() call above can be thrown if resource bytes cannot
// be retrieved from contact resource provider
holder.vCardAvatar.setImageResource(android.R.drawable.ic_delete)
holder.name.text = context.getString(R.string.attachment_missing)
holder.name.isVisible = true
binding.vCardAvatar.setImageResource(android.R.drawable.ic_delete)
binding.name.text = context.getString(R.string.attachment_missing)
binding.name.isVisible = true
}
return
}
val binding = AttachmentFileListItemBinding.bind(holder.itemView)
// set best image and text to use for icon
when (attachment.uri.loadBestIconIntoImageView(context, holder.thumbnail)) {
when (attachment.uri.loadBestIconIntoImageView(context, binding.thumbnail)) {
LoadBestIconIntoImageView.Missing -> {
holder.fileName.text = context.getString(R.string.attachment_missing)
holder.fileName.visibility = View.VISIBLE
binding.fileName.text = context.getString(R.string.attachment_missing)
binding.fileName.visibility = View.VISIBLE
}
LoadBestIconIntoImageView.ActivityIcon,
LoadBestIconIntoImageView.DefaultAudioIcon,
LoadBestIconIntoImageView.GenericIcon -> {
// generic style icon used, also show name
holder.fileName.text = attachment.uri.getName(context)
holder.fileName.visibility = View.VISIBLE
binding.fileName.text = attachment.uri.getName(context)
binding.fileName.visibility = View.VISIBLE
}
else -> holder.fileName.visibility = View.GONE
else -> binding.fileName.visibility = View.GONE
}
}
@@ -37,6 +37,7 @@ import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import androidx.core.net.toUri
import com.jakewharton.rxbinding2.view.clicks
import com.moez.QKSMS.common.QkMediaPlayer
@@ -65,24 +66,14 @@ import dev.octoshrimpy.quik.feature.extensions.isEmojiOnly
import dev.octoshrimpy.quik.model.Conversation
import dev.octoshrimpy.quik.model.Message
import dev.octoshrimpy.quik.model.Recipient
import dev.octoshrimpy.quik.databinding.MessageListItemInBinding
import dev.octoshrimpy.quik.databinding.MessageListItemOutBinding
import dev.octoshrimpy.quik.util.PhoneNumberUtils
import dev.octoshrimpy.quik.util.Preferences
import io.reactivex.disposables.Disposable
import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.Subject
import io.realm.RealmResults
import kotlinx.android.synthetic.main.message_list_item_in.*
import kotlinx.android.synthetic.main.message_list_item_in.body
import kotlinx.android.synthetic.main.message_list_item_in.reactionText
import kotlinx.android.synthetic.main.message_list_item_in.parts
import kotlinx.android.synthetic.main.message_list_item_in.reactions
import kotlinx.android.synthetic.main.message_list_item_in.sim
import kotlinx.android.synthetic.main.message_list_item_in.simIndex
import kotlinx.android.synthetic.main.message_list_item_in.status
import kotlinx.android.synthetic.main.message_list_item_in.timestamp
import kotlinx.android.synthetic.main.message_list_item_in.view.*
import kotlinx.android.synthetic.main.message_list_item_out.*
import kotlinx.android.synthetic.main.message_list_item_out.view.cancel
import java.util.*
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@@ -149,20 +140,33 @@ class MessagesAdapter @Inject constructor(
// Use the parent's context to inflate the layout, otherwise link clicks will crash the app
val inflater = LayoutInflater.from(parent.context)
val view = if (viewType == VIEW_TYPE_MESSAGE_OUT) {
inflater.inflate(R.layout.message_list_item_out, parent,false).apply {
findViewById<ImageView>(R.id.cancelIcon).setTint(theme.theme)
findViewById<ProgressBar>(R.id.cancel).setTint(theme.theme)
findViewById<ImageView>(R.id.sendNowIcon).setTint(theme.theme)
findViewById<ImageView>(R.id.resendIcon).setTint(theme.theme)
}
} else
inflater.inflate(R.layout.message_list_item_in, parent, false)
val view: View
val body: TextView
val parts: View
val status: View
view.body.hyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE
if (viewType == VIEW_TYPE_MESSAGE_OUT) {
val binding = MessageListItemOutBinding.inflate(inflater, parent, false)
view = binding.root
body = binding.body
parts = binding.parts
status = binding.status
binding.cancelIcon.setTint(theme.theme)
binding.cancel.setTint(theme.theme)
binding.sendNowIcon.setTint(theme.theme)
binding.resendIcon.setTint(theme.theme)
} else {
val binding = MessageListItemInBinding.inflate(inflater, parent, false)
view = binding.root
body = binding.body
parts = binding.parts
status = binding.status
}
body.hyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE
// register recycler view with compose activity for context menus
partContextMenuRegistrar.onNext(view.parts)
partContextMenuRegistrar.onNext(parts)
return QkViewHolder(view).apply {
view.setOnClickListener {
@@ -170,7 +174,7 @@ class MessagesAdapter @Inject constructor(
when (toggleSelection(it.id, false)) {
true -> view.isActivated = isSelected(it.id)
false -> {
expanded[it.id] = view.status.visibility != View.VISIBLE
expanded[it.id] = status.visibility != View.VISIBLE
notifyItemChanged(adapterPosition)
}
}
@@ -197,55 +201,108 @@ class MessagesAdapter @Inject constructor(
}
// Update the selected state
holder.containerView.isActivated = isSelected(message.id) || highlight == message.id
holder.itemView.isActivated = isSelected(message.id) || highlight == message.id
// bind the cancelFrame (cancel button) and send now button
holder.cancelFrame?.let { cancelFrame ->
holder.sendNowIcon?.let { sendNowIcon ->
val isCancellable = message.isSending() && message.date > System.currentTimeMillis()
// Get views based on message type
val isOutgoing = message.isMe()
val timestamp: TextView
val simIndex: TextView
val sim: ImageView
val body: TextView
val parts: androidx.recyclerview.widget.RecyclerView
val reactions: View
val reactionText: TextView
val status: TextView
if (isCancellable) {
cancelFrame.visibility = View.VISIBLE
sendNowIcon.visibility = View.VISIBLE
if (isOutgoing) {
val binding = MessageListItemOutBinding.bind(holder.itemView)
timestamp = binding.timestamp
simIndex = binding.simIndex
sim = binding.sim
body = binding.body
parts = binding.parts
reactions = binding.reactions
reactionText = binding.reactionText
status = binding.status
cancelFrame.setOnClickListener { cancelSendingClicks.onNext(message.id) }
sendNowIcon.setOnClickListener { sendNowClicks.onNext(message.id) }
// bind the cancelFrame (cancel button) and send now button
val isCancellable = message.isSending() && message.date > System.currentTimeMillis()
cancelFrame.cancel.progress = 2
if (isCancellable) {
binding.cancelFrame.visibility = View.VISIBLE
binding.sendNowIcon.visibility = View.VISIBLE
val delay = when (prefs.sendDelay.get()) {
Preferences.SEND_DELAY_SHORT -> 3000
Preferences.SEND_DELAY_MEDIUM -> 5000
Preferences.SEND_DELAY_LONG -> 10000
else -> 0
}
val progress =
(1 - (message.date - System.currentTimeMillis()) / delay.toFloat()) * 100
binding.cancelFrame.setOnClickListener { cancelSendingClicks.onNext(message.id) }
binding.sendNowIcon.setOnClickListener { sendNowClicks.onNext(message.id) }
ObjectAnimator.ofInt(cancelFrame.cancel, "progress", progress.toInt(), 100)
.setDuration(message.date - System.currentTimeMillis())
.start()
binding.cancel.progress = 2
val delay = when (prefs.sendDelay.get()) {
Preferences.SEND_DELAY_SHORT -> 3000
Preferences.SEND_DELAY_MEDIUM -> 5000
Preferences.SEND_DELAY_LONG -> 10000
else -> 0
}
else {
cancelFrame.visibility = View.GONE
sendNowIcon.visibility = View.GONE
val progress =
(1 - (message.date - System.currentTimeMillis()) / delay.toFloat()) * 100
cancelFrame.setOnClickListener(null)
sendNowIcon.setOnClickListener(null)
ObjectAnimator.ofInt(binding.cancel, "progress", progress.toInt(), 100)
.setDuration(message.date - System.currentTimeMillis())
.start()
} else {
binding.cancelFrame.visibility = View.GONE
binding.sendNowIcon.visibility = View.GONE
binding.cancelFrame.setOnClickListener(null)
binding.sendNowIcon.setOnClickListener(null)
}
// bind the resend icon view
if (message.isFailedMessage()) {
binding.resendIcon.visibility = View.VISIBLE
binding.resendIcon.clicks().subscribe {
resendClicks.onNext(message.id)
binding.resendIcon.visibility = View.GONE
}
} else {
binding.resendIcon.visibility = View.GONE
}
body.apply {
highlightColor = theme.theme.withAlpha(0x5d)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
textSelectHandle?.setTint(theme.theme.withAlpha(0xad))
textSelectHandleLeft?.setTint(theme.theme.withAlpha(0xad))
textSelectHandleRight?.setTint(theme.theme.withAlpha(0xad))
}
}
}
} else {
val binding = MessageListItemInBinding.bind(holder.itemView)
timestamp = binding.timestamp
simIndex = binding.simIndex
sim = binding.sim
body = binding.body
parts = binding.parts
reactions = binding.reactions
reactionText = binding.reactionText
status = binding.status
// bind the resend icon view
holder.resendIcon?.let { resendIcon ->
if (message.isFailedMessage()) {
resendIcon.visibility = View.VISIBLE
resendIcon.clicks().subscribe {
resendClicks.onNext(message.id)
resendIcon.visibility = View.GONE
// Bind the avatar and bubble colour
binding.avatar.apply {
setRecipient(contactCache[message.address])
setVisible(!canGroup(message, next), View.INVISIBLE)
}
body.apply {
setTextColor(theme.textPrimary)
setBackgroundTint(theme.theme)
highlightColor = R.attr.bubbleColor.withAlpha(0x5d)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
textSelectHandle?.setTint(R.attr.bubbleColor.withAlpha(0x7d))
textSelectHandleLeft?.setTint(R.attr.bubbleColor.withAlpha(0x7d))
textSelectHandleRight?.setTint(R.attr.bubbleColor.withAlpha(0x7d))
}
} else
resendIcon.visibility = View.GONE
}
}
val subject = message.getCleansedSubject()
@@ -271,12 +328,12 @@ class MessagesAdapter @Inject constructor(
}
// Bind the message status
bindStatus(holder, isMsgTextTruncated, message, next)
bindStatus(status, isMsgTextTruncated, message, next)
// Bind the timestamp
val subscription = subs.find { it.subscriptionId == message.subId }
holder.timestamp.apply {
timestamp.apply {
text = dateFormatter.getMessageTimestamp(message.date)
setVisible(
((message.date - (previous?.date ?: 0))
@@ -286,49 +343,22 @@ class MessagesAdapter @Inject constructor(
)
}
holder.simIndex.text = subscription?.simSlotIndex?.plus(1)?.toString()
simIndex.text = subscription?.simSlotIndex?.plus(1)?.toString()
((message.subId != previous?.subId) && (subscription != null) && (subs.size > 1)).also {
holder.sim.setVisible(it)
holder.simIndex.setVisible(it)
sim.setVisible(it)
simIndex.setVisible(it)
}
// Bind the grouping
holder.containerView.setPadding(
holder.itemView.setPadding(
bottom = if (canGroup(message, next)) 0 else 16.dpToPx(context)
)
// Bind the avatar and bubble colour
if (!message.isMe()) {
holder.avatar.apply {
setRecipient(contactCache[message.address])
setVisible(!canGroup(message, next), View.INVISIBLE)
}
holder.body.apply {
setTextColor(theme.textPrimary)
setBackgroundTint(theme.theme)
highlightColor = R.attr.bubbleColor.withAlpha(0x5d)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
textSelectHandle?.setTint(R.attr.bubbleColor.withAlpha(0x7d))
textSelectHandleLeft?.setTint(R.attr.bubbleColor.withAlpha(0x7d))
textSelectHandleRight?.setTint(R.attr.bubbleColor.withAlpha(0x7d))
}
}
} else
holder.body.apply {
highlightColor = theme.theme.withAlpha(0x5d)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
textSelectHandle?.setTint(theme.theme.withAlpha(0xad))
textSelectHandleLeft?.setTint(theme.theme.withAlpha(0xad))
textSelectHandleRight?.setTint(theme.theme.withAlpha(0xad))
}
}
// Bind the body text
val emojiOnly = displayText.isEmojiOnly()
textViewStyler.setTextSize(
holder.body,
body,
when (emojiOnly) {
true -> TextViewStyler.SIZE_EMOJI
false -> TextViewStyler.SIZE_PRIMARY
@@ -338,10 +368,10 @@ class MessagesAdapter @Inject constructor(
val spanString = SpannableStringBuilder(displayText)
when (prefs.messageLinkHandling.get()) {
Preferences.MESSAGE_LINK_HANDLING_BLOCK -> holder.body.autoLinkMask = 0
Preferences.MESSAGE_LINK_HANDLING_BLOCK -> body.autoLinkMask = 0
Preferences.MESSAGE_LINK_HANDLING_ASK -> {
// manually handle link clicks if user has set to ask before opening links
holder.body.apply {
body.apply {
isClickable = false
linksClickable = false
movementMethod = LinkMovementMethod.getInstance()
@@ -368,10 +398,10 @@ class MessagesAdapter @Inject constructor(
}
}
}
else -> holder.body.movementMethod = LinkMovementMethod.getInstance()
else -> body.movementMethod = LinkMovementMethod.getInstance()
}
holder.body.apply {
body.apply {
text = spanString
setVisible(message.isSms() || spanString.isNotBlank())
@@ -387,49 +417,48 @@ class MessagesAdapter @Inject constructor(
}
// Bind the parts
holder.parts.adapter = partsAdapterProvider.get().apply {
parts.adapter = partsAdapterProvider.get().apply {
this.theme = theme
setData(message, previous, next, holder, audioState)
contextMenuValue = message.id
clicks.subscribe(partClicks) // part clicks gets passed back to compose view model
}
showEmojiReactions(holder, message)
showEmojiReactions(reactions, reactionText, message)
}
private fun showEmojiReactions(holder: QkViewHolder, message: Message) {
holder.reactions?.let { reactionsContainer ->
val reactions = message.emojiReactions
val hasReactions = reactions.isNotEmpty()
private fun showEmojiReactions(reactionsContainer: View, reactionTextView: TextView, message: Message) {
val reactions = message.emojiReactions
val hasReactions = reactions.isNotEmpty()
if (hasReactions) {
val uniqueEmojis = reactions.map { it.emoji }.distinct()
val totalCount = reactions.size
if (hasReactions) {
val uniqueEmojis = reactions.map { it.emoji }.distinct()
val totalCount = reactions.size
// Show unique emojis followed by total count
val reactionText = if (totalCount == 1) {
uniqueEmojis.first()
} else {
"${uniqueEmojis.joinToString("")}\u00A0$totalCount"
}
holder.reactionText?.text = reactionText
holder.reactionText?.setOnClickListener { reactionClicks.onNext(message.id) }
reactionsContainer.setVisible(true)
// Show unique emojis followed by total count
val reactionText = if (totalCount == 1) {
uniqueEmojis.first()
} else {
reactionsContainer.setVisible(false)
holder.reactionText?.setOnClickListener(null)
"${uniqueEmojis.joinToString("")}\u00A0$totalCount"
}
reactionTextView.text = reactionText
reactionTextView.setOnClickListener { reactionClicks.onNext(message.id) }
reactionsContainer.setVisible(true)
} else {
reactionsContainer.setVisible(false)
reactionTextView.setOnClickListener(null)
}
}
private fun bindStatus(
holder: QkViewHolder,
statusView: TextView,
bodyTextTruncated: Boolean,
message: Message,
next: Message?
) {
holder.status.apply {
statusView.apply {
text = when {
message.isSending() -> context.getString(R.string.message_status_sending)
message.isDelivered() -> context.getString(
@@ -24,7 +24,7 @@ import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import dev.octoshrimpy.quik.R
import dev.octoshrimpy.quik.common.base.QkAdapter
import dev.octoshrimpy.quik.common.base.QkViewHolder
import dev.octoshrimpy.quik.common.base.QkBindingViewHolder
import dev.octoshrimpy.quik.common.util.Colors
import dev.octoshrimpy.quik.common.util.extensions.forwardTouches
import dev.octoshrimpy.quik.common.util.extensions.setTint
@@ -36,16 +36,15 @@ import dev.octoshrimpy.quik.model.Recipient
import dev.octoshrimpy.quik.repository.ConversationRepository
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.rxkotlin.plusAssign
import dev.octoshrimpy.quik.databinding.ContactListItemBinding
import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.Subject
import kotlinx.android.synthetic.main.contact_list_item.*
import kotlinx.android.synthetic.main.contact_list_item.view.*
import javax.inject.Inject
class ComposeItemAdapter @Inject constructor(
private val colors: Colors,
private val conversationRepo: ConversationRepository
) : QkAdapter<ComposeItem, QkViewHolder>() {
) : QkAdapter<ComposeItem, QkBindingViewHolder<ContactListItemBinding>>() {
val clicks: Subject<ComposeItem> = PublishSubject.create()
val longClicks: Subject<ComposeItem> = PublishSubject.create()
@@ -59,22 +58,22 @@ class ComposeItemAdapter @Inject constructor(
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkViewHolder {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkBindingViewHolder<ContactListItemBinding> {
val layoutInflater = LayoutInflater.from(parent.context)
val view = layoutInflater.inflate(R.layout.contact_list_item, parent, false)
val binding = ContactListItemBinding.inflate(layoutInflater, parent, false)
view.icon.setTint(colors.theme().theme)
binding.icon.setTint(colors.theme().theme)
view.numbers.setRecycledViewPool(numbersViewPool)
view.numbers.adapter = PhoneNumberAdapter()
view.numbers.forwardTouches(view)
binding.numbers.setRecycledViewPool(numbersViewPool)
binding.numbers.adapter = PhoneNumberAdapter()
binding.numbers.forwardTouches(binding.root)
return QkViewHolder(view).apply {
view.setOnClickListener {
return QkBindingViewHolder(binding).apply {
binding.root.setOnClickListener {
val item = getItem(adapterPosition)
clicks.onNext(item)
}
view.setOnLongClickListener {
binding.root.setOnLongClickListener {
val item = getItem(adapterPosition)
longClicks.onNext(item)
true
@@ -82,103 +81,104 @@ class ComposeItemAdapter @Inject constructor(
}
}
override fun onBindViewHolder(holder: QkViewHolder, position: Int) {
override fun onBindViewHolder(holder: QkBindingViewHolder<ContactListItemBinding>, position: Int) {
val binding = holder.binding
val prevItem = if (position > 0) getItem(position - 1) else null
when (val item = getItem(position)) {
is ComposeItem.New -> bindNew(holder, item.value)
is ComposeItem.Recent -> bindRecent(holder, item.value, prevItem)
is ComposeItem.Starred -> bindStarred(holder, item.value, prevItem)
is ComposeItem.Person -> bindPerson(holder, item.value, prevItem)
is ComposeItem.Group -> bindGroup(holder, item.value, prevItem)
is ComposeItem.New -> bindNew(binding, item.value)
is ComposeItem.Recent -> bindRecent(binding, item.value, prevItem)
is ComposeItem.Starred -> bindStarred(binding, item.value, prevItem)
is ComposeItem.Person -> bindPerson(binding, item.value, prevItem)
is ComposeItem.Group -> bindGroup(binding, item.value, prevItem)
}
}
private fun bindNew(holder: QkViewHolder, contact: Contact) {
holder.index.isVisible = false
private fun bindNew(binding: ContactListItemBinding, contact: Contact) {
binding.index.isVisible = false
holder.icon.isVisible = false
binding.icon.isVisible = false
holder.avatar.recipients = listOf(createRecipient(contact))
binding.avatar.recipients = listOf(createRecipient(contact))
holder.title.text = contact.numbers.joinToString { it.address }
binding.title.text = contact.numbers.joinToString { it.address }
holder.subtitle.isVisible = false
binding.subtitle.isVisible = false
holder.numbers.isVisible = false
binding.numbers.isVisible = false
}
private fun bindRecent(holder: QkViewHolder, conversation: Conversation, prev: ComposeItem?) {
holder.index.isVisible = false
private fun bindRecent(binding: ContactListItemBinding, conversation: Conversation, prev: ComposeItem?) {
binding.index.isVisible = false
holder.icon.isVisible = prev !is ComposeItem.Recent
holder.icon.setImageResource(R.drawable.ic_history_black_24dp)
binding.icon.isVisible = prev !is ComposeItem.Recent
binding.icon.setImageResource(R.drawable.ic_history_black_24dp)
holder.avatar.recipients = conversation.recipients
binding.avatar.recipients = conversation.recipients
holder.title.text = conversation.getTitle()
binding.title.text = conversation.getTitle()
holder.subtitle.isVisible = conversation.recipients.size > 1 && conversation.name.isBlank()
holder.subtitle.text = conversation.recipients.joinToString(", ") { recipient ->
binding.subtitle.isVisible = conversation.recipients.size > 1 && conversation.name.isBlank()
binding.subtitle.text = conversation.recipients.joinToString(", ") { recipient ->
recipient.contact?.name ?: recipient.address
}
holder.subtitle.collapseEnabled = conversation.recipients.size > 1
binding.subtitle.collapseEnabled = conversation.recipients.size > 1
holder.numbers.isVisible = conversation.recipients.size == 1
(holder.numbers.adapter as PhoneNumberAdapter).data = conversation.recipients
binding.numbers.isVisible = conversation.recipients.size == 1
(binding.numbers.adapter as PhoneNumberAdapter).data = conversation.recipients
.mapNotNull { recipient -> recipient.contact }
.flatMap { contact -> contact.numbers }
}
private fun bindStarred(holder: QkViewHolder, contact: Contact, prev: ComposeItem?) {
holder.index.isVisible = false
private fun bindStarred(binding: ContactListItemBinding, contact: Contact, prev: ComposeItem?) {
binding.index.isVisible = false
holder.icon.isVisible = prev !is ComposeItem.Starred
holder.icon.setImageResource(R.drawable.ic_star_black_24dp)
binding.icon.isVisible = prev !is ComposeItem.Starred
binding.icon.setImageResource(R.drawable.ic_star_black_24dp)
holder.avatar.recipients = listOf(createRecipient(contact))
binding.avatar.recipients = listOf(createRecipient(contact))
holder.title.text = contact.name
binding.title.text = contact.name
holder.subtitle.isVisible = false
binding.subtitle.isVisible = false
holder.numbers.isVisible = true
(holder.numbers.adapter as PhoneNumberAdapter).data = contact.numbers
binding.numbers.isVisible = true
(binding.numbers.adapter as PhoneNumberAdapter).data = contact.numbers
}
private fun bindGroup(holder: QkViewHolder, group: ContactGroup, prev: ComposeItem?) {
holder.index.isVisible = false
private fun bindGroup(binding: ContactListItemBinding, group: ContactGroup, prev: ComposeItem?) {
binding.index.isVisible = false
holder.icon.isVisible = prev !is ComposeItem.Group
holder.icon.setImageResource(R.drawable.ic_people_black_24dp)
binding.icon.isVisible = prev !is ComposeItem.Group
binding.icon.setImageResource(R.drawable.ic_people_black_24dp)
holder.avatar.recipients = group.contacts.map(::createRecipient)
binding.avatar.recipients = group.contacts.map(::createRecipient)
holder.title.text = group.title
binding.title.text = group.title
holder.subtitle.isVisible = true
holder.subtitle.text = group.contacts.joinToString(", ") { it.name }
holder.subtitle.collapseEnabled = group.contacts.size > 1
binding.subtitle.isVisible = true
binding.subtitle.text = group.contacts.joinToString(", ") { it.name }
binding.subtitle.collapseEnabled = group.contacts.size > 1
holder.numbers.isVisible = false
binding.numbers.isVisible = false
}
private fun bindPerson(holder: QkViewHolder, contact: Contact, prev: ComposeItem?) {
holder.index.isVisible = true
holder.index.text = if (contact.name.getOrNull(0)?.isLetter() == true) contact.name[0].toString() else "#"
holder.index.isVisible = prev !is ComposeItem.Person ||
private fun bindPerson(binding: ContactListItemBinding, contact: Contact, prev: ComposeItem?) {
binding.index.isVisible = true
binding.index.text = if (contact.name.getOrNull(0)?.isLetter() == true) contact.name[0].toString() else "#"
binding.index.isVisible = prev !is ComposeItem.Person ||
(contact.name[0].isLetter() && !contact.name[0].equals(prev.value.name[0], ignoreCase = true)) ||
(!contact.name[0].isLetter() && prev.value.name[0].isLetter())
holder.icon.isVisible = false
binding.icon.isVisible = false
holder.avatar.recipients = listOf(createRecipient(contact))
binding.avatar.recipients = listOf(createRecipient(contact))
holder.title.text = contact.name
binding.title.text = contact.name
holder.subtitle.isVisible = false
binding.subtitle.isVisible = false
holder.numbers.isVisible = true
(holder.numbers.adapter as PhoneNumberAdapter).data = contact.numbers
binding.numbers.isVisible = true
(binding.numbers.adapter as PhoneNumberAdapter).data = contact.numbers
}
private fun createRecipient(contact: Contact): Recipient {
@@ -19,24 +19,26 @@
package dev.octoshrimpy.quik.feature.compose.editing
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.animation.AlphaAnimation
import android.widget.RelativeLayout
import dev.octoshrimpy.quik.R
import dev.octoshrimpy.quik.common.util.Colors
import dev.octoshrimpy.quik.common.util.extensions.setBackgroundTint
import dev.octoshrimpy.quik.common.util.extensions.setTint
import dev.octoshrimpy.quik.databinding.ContactChipDetailedBinding
import dev.octoshrimpy.quik.injection.appComponent
import dev.octoshrimpy.quik.model.Recipient
import kotlinx.android.synthetic.main.contact_chip_detailed.view.*
import javax.inject.Inject
class DetailedChipView(context: Context) : RelativeLayout(context) {
@Inject lateinit var colors: Colors
private val binding: ContactChipDetailedBinding
init {
View.inflate(context, R.layout.contact_chip_detailed, this)
binding = ContactChipDetailedBinding.inflate(LayoutInflater.from(context), this, true)
appComponent.inject(this)
setOnClickListener { hide() }
@@ -48,15 +50,15 @@ class DetailedChipView(context: Context) : RelativeLayout(context) {
}
fun setRecipient(recipient: Recipient) {
avatar.setRecipient(recipient)
name.text = recipient.contact?.name?.takeIf { it.isNotBlank() } ?: recipient.address
info.text = recipient.address
binding.avatar.setRecipient(recipient)
binding.name.text = recipient.contact?.name?.takeIf { it.isNotBlank() } ?: recipient.address
binding.info.text = recipient.address
colors.theme(recipient).let { theme ->
card.setBackgroundTint(theme.theme)
name.setTextColor(theme.textPrimary)
info.setTextColor(theme.textTertiary)
delete.setTint(theme.textPrimary)
binding.card.setBackgroundTint(theme.theme)
binding.name.setTextColor(theme.textPrimary)
binding.info.setTextColor(theme.textTertiary)
binding.delete.setTint(theme.textPrimary)
}
}
@@ -77,7 +79,7 @@ class DetailedChipView(context: Context) : RelativeLayout(context) {
}
fun setOnDeleteListener(listener: (View) -> Unit) {
delete.setOnClickListener(listener)
binding.delete.setOnClickListener(listener)
}
}
@@ -23,20 +23,18 @@ import android.view.LayoutInflater
import android.view.ViewGroup
import dev.octoshrimpy.quik.R
import dev.octoshrimpy.quik.common.base.QkAdapter
import dev.octoshrimpy.quik.common.base.QkViewHolder
import dev.octoshrimpy.quik.common.base.QkBindingViewHolder
import dev.octoshrimpy.quik.common.util.extensions.forwardTouches
import dev.octoshrimpy.quik.databinding.PhoneNumberListItemBinding
import dev.octoshrimpy.quik.extensions.Optional
import dev.octoshrimpy.quik.model.PhoneNumber
import io.reactivex.subjects.BehaviorSubject
import io.reactivex.subjects.Subject
import kotlinx.android.synthetic.main.phone_number_list_item.*
import kotlinx.android.synthetic.main.radio_preference_view.*
import kotlinx.android.synthetic.main.radio_preference_view.view.*
import javax.inject.Inject
class PhoneNumberPickerAdapter @Inject constructor(
private val context: Context
) : QkAdapter<PhoneNumber, QkViewHolder>() {
) : QkAdapter<PhoneNumber, QkBindingViewHolder<PhoneNumberListItemBinding>>() {
val selectedItemChanges: Subject<Optional<Long>> = BehaviorSubject.create()
@@ -48,25 +46,26 @@ class PhoneNumberPickerAdapter @Inject constructor(
selectedItemChanges.onNext(Optional(value))
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkViewHolder {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkBindingViewHolder<PhoneNumberListItemBinding> {
val inflater = LayoutInflater.from(parent.context)
val view = inflater.inflate(R.layout.phone_number_list_item, parent, false)
return QkViewHolder(view).apply {
radioButton.forwardTouches(itemView)
val binding = PhoneNumberListItemBinding.inflate(inflater, parent, false)
return QkBindingViewHolder(binding).apply {
binding.number.radioButton.forwardTouches(itemView)
view.setOnClickListener {
binding.root.setOnClickListener {
val phoneNumber = getItem(adapterPosition)
selectedItem = phoneNumber.id
}
}
}
override fun onBindViewHolder(holder: QkViewHolder, position: Int) {
override fun onBindViewHolder(holder: QkBindingViewHolder<PhoneNumberListItemBinding>, position: Int) {
val binding = holder.binding
val phoneNumber = getItem(position)
holder.number.radioButton.isChecked = phoneNumber.id == selectedItem
holder.number.titleView.text = phoneNumber.address
holder.number.summaryView.text = when (phoneNumber.isDefault) {
binding.number.radioButton.isChecked = phoneNumber.id == selectedItem
binding.number.titleView.text = phoneNumber.address
binding.number.summaryView.text = when (phoneNumber.isDefault) {
true -> context.getString(R.string.compose_number_picker_default, phoneNumber.type)
false -> phoneNumber.type
}
@@ -35,6 +35,7 @@ import dev.octoshrimpy.quik.common.util.extensions.setBackgroundTint
import dev.octoshrimpy.quik.common.util.extensions.setTint
import dev.octoshrimpy.quik.common.util.extensions.withAlpha
import dev.octoshrimpy.quik.common.widget.BubbleImageView
import dev.octoshrimpy.quik.databinding.MmsAudioPreviewListItemBinding
import dev.octoshrimpy.quik.extensions.isAudio
import dev.octoshrimpy.quik.extensions.resourceExists
import dev.octoshrimpy.quik.feature.compose.MessagesAdapter
@@ -44,8 +45,6 @@ import dev.octoshrimpy.quik.util.GlideApp
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import kotlinx.android.synthetic.main.mms_audio_preview_list_item.*
import kotlinx.android.synthetic.main.mms_image_preview_list_item.thumbnail
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@@ -69,33 +68,36 @@ class AudioBinder @Inject constructor(colors: Colors, private val context: Conte
.subscribeOn(Schedulers.single())
.observeOn(AndroidSchedulers.mainThread())
.doOnNext {
viewHolder?.seekBar?.progress = QkMediaPlayer.currentPosition
viewHolder?.let { holder ->
val binding = MmsAudioPreviewListItemBinding.bind(holder.itemView)
binding.seekBar.progress = QkMediaPlayer.currentPosition
}
}
.subscribe()
}
}
private fun uiToPlaying(viewHolder: QkViewHolder) {
viewHolder.seekBar.max = QkMediaPlayer.duration
viewHolder.seekBar.isEnabled = true
viewHolder.seekBar.progress = QkMediaPlayer.currentPosition
viewHolder.playPause.setImageResource(R.drawable.exo_icon_pause)
viewHolder.playPause.tag = QkMediaPlayer.PlayingState.Playing
viewHolder.metadataTitle.isSelected = true // start marquee
private fun uiToPlaying(binding: MmsAudioPreviewListItemBinding) {
binding.seekBar.max = QkMediaPlayer.duration
binding.seekBar.isEnabled = true
binding.seekBar.progress = QkMediaPlayer.currentPosition
binding.playPause.setImageResource(R.drawable.exo_icon_pause)
binding.playPause.tag = QkMediaPlayer.PlayingState.Playing
binding.metadataTitle.isSelected = true // start marquee
}
private fun uiToPaused(viewHolder: QkViewHolder) {
viewHolder.playPause.setImageResource(R.drawable.exo_icon_play)
viewHolder.playPause.tag = QkMediaPlayer.PlayingState.Paused
private fun uiToPaused(binding: MmsAudioPreviewListItemBinding) {
binding.playPause.setImageResource(R.drawable.exo_icon_play)
binding.playPause.tag = QkMediaPlayer.PlayingState.Paused
}
private fun uiToStopped(viewHolder: QkViewHolder) {
viewHolder.seekBar.progress = 0
viewHolder.seekBar.max = 0
viewHolder.seekBar.isEnabled = false
viewHolder.playPause.setImageResource(R.drawable.exo_icon_play)
viewHolder.playPause.tag = QkMediaPlayer.PlayingState.Stopped
viewHolder.metadataTitle.isSelected = false // stop marquee
private fun uiToStopped(binding: MmsAudioPreviewListItemBinding) {
binding.seekBar.progress = 0
binding.seekBar.max = 0
binding.seekBar.isEnabled = false
binding.playPause.setImageResource(R.drawable.exo_icon_play)
binding.playPause.tag = QkMediaPlayer.PlayingState.Stopped
binding.metadataTitle.isSelected = false // stop marquee
}
override fun bindPart(
@@ -105,16 +107,18 @@ class AudioBinder @Inject constructor(colors: Colors, private val context: Conte
canGroupWithPrevious: Boolean,
canGroupWithNext: Boolean,
) {
val binding = MmsAudioPreviewListItemBinding.bind(holder.itemView)
// click on background - passes back to compose view model
holder.containerView.setOnClickListener { clicks.onNext(part.id) }
holder.itemView.setOnClickListener { clicks.onNext(part.id) }
// play/pause button click handling
holder.playPause.setOnClickListener {
when (holder.playPause.tag) {
binding.playPause.setOnClickListener {
when (binding.playPause.tag) {
QkMediaPlayer.PlayingState.Playing -> {
if (audioState.partId == part.id) {
QkMediaPlayer.pause()
uiToPaused(holder)
uiToPaused(binding)
audioState.state = QkMediaPlayer.PlayingState.Paused
// stop progress bar update timer
@@ -124,7 +128,7 @@ class AudioBinder @Inject constructor(colors: Colors, private val context: Conte
QkMediaPlayer.PlayingState.Paused -> {
if (audioState.partId == part.id) {
QkMediaPlayer.start()
uiToPlaying(holder)
uiToPlaying(binding)
audioState.state = QkMediaPlayer.PlayingState.Playing
// start progress bar update timer
@@ -139,7 +143,7 @@ class AudioBinder @Inject constructor(colors: Colors, private val context: Conte
// start media playing
QkMediaPlayer.start()
uiToPlaying(holder)
uiToPlaying(binding)
// set current view holder and part as active
audioState.apply {
@@ -155,8 +159,10 @@ class AudioBinder @Inject constructor(colors: Colors, private val context: Conte
QkMediaPlayer.setOnCompletionListener { // also called on error because we don't have an onerrorlistener
audioState.apply {
// if this part is currently active, set it to stopped and inactive
if ((partId == part.id) && (viewHolder != null))
uiToStopped(viewHolder!!)
if ((partId == part.id) && (viewHolder != null)) {
val stoppedBinding = MmsAudioPreviewListItemBinding.bind(viewHolder!!.itemView)
uiToStopped(stoppedBinding)
}
state = QkMediaPlayer.PlayingState.Stopped
partId = -1
@@ -192,18 +198,18 @@ class AudioBinder @Inject constructor(colors: Colors, private val context: Conte
if (!message.isMe())
theme.theme
else
holder.containerView.context.resolveThemeColor(R.attr.bubbleColor)
holder.itemView.context.resolveThemeColor(R.attr.bubbleColor)
val primaryColor =
if (!message.isMe())
theme.textPrimary
else
holder.containerView.context.resolveThemeColor(android.R.attr.textColorPrimary)
holder.itemView.context.resolveThemeColor(android.R.attr.textColorPrimary)
// sound wave
holder.soundWave.setTint(primaryColor)
binding.soundWave.setTint(primaryColor)
// seek bar
holder.seekBar.apply {
binding.seekBar.apply {
setTint(secondaryColor)
thumbTintList = ColorStateList.valueOf(secondaryColor)
@@ -219,15 +225,15 @@ class AudioBinder @Inject constructor(colors: Colors, private val context: Conte
}
// playPause button
holder.playPause. apply {
binding.playPause.apply {
if ((audioState.partId == part.id) &&
(audioState.state == QkMediaPlayer.PlayingState.Playing))
uiToPlaying(holder)
uiToPlaying(binding)
else if ((audioState.partId == part.id) &&
(audioState.state == QkMediaPlayer.PlayingState.Paused))
uiToPaused(holder)
uiToPaused(binding)
else
uiToStopped(holder)
uiToStopped(binding)
setTint(secondaryColor)
setBackgroundTint(primaryColor)
@@ -239,7 +245,7 @@ class AudioBinder @Inject constructor(colors: Colors, private val context: Conte
setDataSource(context, part.getUri())
// metadata title
holder.metadataTitle.apply {
binding.metadataTitle.apply {
text = extractMetadata(METADATA_KEY_TITLE)
if (text.isEmpty())
@@ -252,7 +258,7 @@ class AudioBinder @Inject constructor(colors: Colors, private val context: Conte
}
// bubble / embedded image
holder.thumbnail.apply {
binding.thumbnail.apply {
bubbleStyle = when {
!canGroupWithPrevious && canGroupWithNext ->
if (message.isMe()) BubbleImageView.Style.OUT_FIRST else BubbleImageView.Style.IN_FIRST
@@ -268,18 +274,18 @@ class AudioBinder @Inject constructor(colors: Colors, private val context: Conte
val embeddedPicture = embeddedPicture
if (embeddedPicture == null) {
holder.frame.layoutParams.height = (holder.frame.layoutParams.width / 2)
binding.frame.layoutParams.height = (binding.frame.layoutParams.width / 2)
setTint(secondaryColor)
setImageResource(R.drawable.rectangle)
} else {
holder.frame.layoutParams.height = holder.frame.layoutParams.width
binding.frame.layoutParams.height = binding.frame.layoutParams.width
setTint(null)
GlideApp.with(context)
.asBitmap()
.load(embeddedPicture)
.override(
holder.frame.layoutParams.width,
holder.frame.layoutParams.height
binding.frame.layoutParams.width,
binding.frame.layoutParams.height
)
.into(this)
}
@@ -27,6 +27,7 @@ import dev.octoshrimpy.quik.common.util.Colors
import dev.octoshrimpy.quik.common.util.extensions.resolveThemeColor
import dev.octoshrimpy.quik.common.util.extensions.setBackgroundTint
import dev.octoshrimpy.quik.common.util.extensions.setTint
import dev.octoshrimpy.quik.databinding.MmsFileListItemBinding
import dev.octoshrimpy.quik.extensions.mapNotNull
import dev.octoshrimpy.quik.feature.compose.BubbleUtils
import dev.octoshrimpy.quik.model.Message
@@ -35,7 +36,6 @@ import dev.octoshrimpy.quik.util.tryOrNull
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import kotlinx.android.synthetic.main.mms_file_list_item.*
import javax.inject.Inject
class FileBinder @Inject constructor(colors: Colors, private val context: Context) : PartBinder() {
@@ -56,8 +56,9 @@ class FileBinder @Inject constructor(colors: Colors, private val context: Contex
canGroupWithPrevious: Boolean,
canGroupWithNext: Boolean
) {
val binding = MmsFileListItemBinding.bind(holder.itemView)
BubbleUtils.getBubble(false, canGroupWithPrevious, canGroupWithNext, message.isMe())
.let(holder.fileBackground::setBackgroundResource)
.let(binding.fileBackground::setBackgroundResource)
tryOrNull(true) {
Observable.just(part.getUri())
@@ -76,21 +77,21 @@ class FileBinder @Inject constructor(colors: Colors, private val context: Contex
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { size -> holder.size.text = size }
.subscribe { size -> binding.size.text = size }
holder.filename.text = part.getBestFilename()
binding.filename.text = part.getBestFilename()
}
if (!message.isMe()) {
holder.fileBackground.setBackgroundTint(theme.theme)
holder.icon.setTint(theme.textPrimary)
holder.filename.setTextColor(theme.textPrimary)
holder.size.setTextColor(theme.textTertiary)
binding.fileBackground.setBackgroundTint(theme.theme)
binding.icon.setTint(theme.textPrimary)
binding.filename.setTextColor(theme.textPrimary)
binding.size.setTextColor(theme.textTertiary)
} else {
holder.fileBackground.setBackgroundTint(holder.containerView.context.resolveThemeColor(R.attr.bubbleColor))
holder.icon.setTint(holder.containerView.context.resolveThemeColor(android.R.attr.textColorSecondary))
holder.filename.setTextColor(holder.containerView.context.resolveThemeColor(android.R.attr.textColorPrimary))
holder.size.setTextColor(holder.containerView.context.resolveThemeColor(android.R.attr.textColorTertiary))
binding.fileBackground.setBackgroundTint(holder.itemView.context.resolveThemeColor(R.attr.bubbleColor))
binding.icon.setTint(holder.itemView.context.resolveThemeColor(android.R.attr.textColorSecondary))
binding.filename.setTextColor(holder.itemView.context.resolveThemeColor(android.R.attr.textColorPrimary))
binding.size.setTextColor(holder.itemView.context.resolveThemeColor(android.R.attr.textColorTertiary))
}
}
@@ -24,13 +24,13 @@ import dev.octoshrimpy.quik.common.base.QkViewHolder
import dev.octoshrimpy.quik.common.util.Colors
import dev.octoshrimpy.quik.common.util.extensions.setVisible
import dev.octoshrimpy.quik.common.widget.BubbleImageView
import dev.octoshrimpy.quik.databinding.MmsImagePreviewListItemBinding
import dev.octoshrimpy.quik.extensions.isImage
import dev.octoshrimpy.quik.extensions.isVideo
import dev.octoshrimpy.quik.model.Message
import dev.octoshrimpy.quik.model.MmsPart
import dev.octoshrimpy.quik.util.GlideApp
import dev.octoshrimpy.quik.util.tryOrNull
import kotlinx.android.synthetic.main.mms_image_preview_list_item.*
import javax.inject.Inject
class ImageBinder @Inject constructor(colors: Colors, private val context: Context) : PartBinder() {
@@ -47,10 +47,11 @@ class ImageBinder @Inject constructor(colors: Colors, private val context: Conte
canGroupWithPrevious: Boolean,
canGroupWithNext: Boolean
) {
holder.video.setVisible(part.isVideo())
holder.containerView.setOnClickListener { clicks.onNext(part.id) }
val binding = MmsImagePreviewListItemBinding.bind(holder.itemView)
binding.video.setVisible(part.isVideo())
holder.itemView.setOnClickListener { clicks.onNext(part.id) }
holder.thumbnail.bubbleStyle = when {
binding.thumbnail.bubbleStyle = when {
!canGroupWithPrevious && canGroupWithNext -> if (message.isMe()) BubbleImageView.Style.OUT_FIRST else BubbleImageView.Style.IN_FIRST
canGroupWithPrevious && canGroupWithNext -> if (message.isMe()) BubbleImageView.Style.OUT_MIDDLE else BubbleImageView.Style.IN_MIDDLE
canGroupWithPrevious && !canGroupWithNext -> if (message.isMe()) BubbleImageView.Style.OUT_LAST else BubbleImageView.Style.IN_LAST
@@ -58,7 +59,7 @@ class ImageBinder @Inject constructor(colors: Colors, private val context: Conte
}
tryOrNull(true) {
GlideApp.with(context).load(part.getUri()).fitCenter().into(holder.thumbnail)
GlideApp.with(context).load(part.getUri()).fitCenter().into(binding.thumbnail)
}
}
@@ -24,6 +24,8 @@ import android.view.ViewGroup
import dev.octoshrimpy.quik.common.base.QkViewHolder
import dev.octoshrimpy.quik.common.util.Colors
import dev.octoshrimpy.quik.common.widget.QkContextMenuRecyclerView
import dev.octoshrimpy.quik.databinding.MessageListItemInBinding
import dev.octoshrimpy.quik.databinding.MessageListItemOutBinding
import dev.octoshrimpy.quik.extensions.isSmil
import dev.octoshrimpy.quik.extensions.isText
import dev.octoshrimpy.quik.feature.compose.BubbleUtils.canGroup
@@ -31,7 +33,6 @@ import dev.octoshrimpy.quik.feature.compose.MessagesAdapter
import dev.octoshrimpy.quik.model.Message
import dev.octoshrimpy.quik.model.MmsPart
import io.reactivex.Observable
import kotlinx.android.synthetic.main.message_list_item_in.*
import javax.inject.Inject
@@ -69,7 +70,12 @@ class PartsAdapter @Inject constructor(
this.message = message
this.previous = previous
this.next = next
this.bodyVisible = holder.body.visibility == View.VISIBLE
// Get body visibility from appropriate binding based on message type
this.bodyVisible = if (message.isMe()) {
MessageListItemOutBinding.bind(holder.itemView).body.visibility == View.VISIBLE
} else {
MessageListItemInBinding.bind(holder.itemView).body.visibility == View.VISIBLE
}
this.data = message.parts.filter { !it.isSmil() && !it.isText() }
this.audioState = audioState
}
@@ -27,6 +27,7 @@ import dev.octoshrimpy.quik.common.util.extensions.getDisplayName
import dev.octoshrimpy.quik.common.util.extensions.resolveThemeColor
import dev.octoshrimpy.quik.common.util.extensions.setBackgroundTint
import dev.octoshrimpy.quik.common.util.extensions.setTint
import dev.octoshrimpy.quik.databinding.MmsVcardListItemBinding
import dev.octoshrimpy.quik.extensions.isVCard
import dev.octoshrimpy.quik.extensions.mapNotNull
import dev.octoshrimpy.quik.feature.compose.BubbleUtils
@@ -37,7 +38,6 @@ import ezvcard.Ezvcard
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import kotlinx.android.synthetic.main.mms_vcard_list_item.*
import javax.inject.Inject
class VCardBinder @Inject constructor(colors: Colors, private val context: Context) : PartBinder() {
@@ -54,10 +54,11 @@ class VCardBinder @Inject constructor(colors: Colors, private val context: Conte
canGroupWithPrevious: Boolean,
canGroupWithNext: Boolean
) {
val binding = MmsVcardListItemBinding.bind(holder.itemView)
BubbleUtils.getBubble(false, canGroupWithPrevious, canGroupWithNext, message.isMe())
.let(holder.vCardBackground::setBackgroundResource)
.let(binding.vCardBackground::setBackgroundResource)
holder.containerView.setOnClickListener { clicks.onNext(part.id) }
holder.itemView.setOnClickListener { clicks.onNext(part.id) }
tryOrNull(true) {
Observable.just(part.getUri())
@@ -67,21 +68,21 @@ class VCardBinder @Inject constructor(colors: Colors, private val context: Conte
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { displayName ->
holder.name?.text = displayName
holder.name.isVisible = displayName.isNotEmpty()
binding.name.text = displayName
binding.name.isVisible = displayName.isNotEmpty()
}
}
if (!message.isMe()) {
holder.vCardBackground.setBackgroundTint(theme.theme)
holder.vCardAvatar.setTint(theme.textPrimary)
holder.name.setTextColor(theme.textPrimary)
holder.label.setTextColor(theme.textTertiary)
binding.vCardBackground.setBackgroundTint(theme.theme)
binding.vCardAvatar.setTint(theme.textPrimary)
binding.name.setTextColor(theme.textPrimary)
binding.label.setTextColor(theme.textTertiary)
} else {
holder.vCardBackground.setBackgroundTint(holder.containerView.context.resolveThemeColor(R.attr.bubbleColor))
holder.vCardAvatar.setTint(holder.containerView.context.resolveThemeColor(android.R.attr.textColorSecondary))
holder.name.setTextColor(holder.containerView.context.resolveThemeColor(android.R.attr.textColorPrimary))
holder.label.setTextColor(holder.containerView.context.resolveThemeColor(android.R.attr.textColorTertiary))
binding.vCardBackground.setBackgroundTint(holder.itemView.context.resolveThemeColor(R.attr.bubbleColor))
binding.vCardAvatar.setTint(holder.itemView.context.resolveThemeColor(android.R.attr.textColorSecondary))
binding.name.setTextColor(holder.itemView.context.resolveThemeColor(android.R.attr.textColorPrimary))
binding.label.setTextColor(holder.itemView.context.resolveThemeColor(android.R.attr.textColorTertiary))
}
}
@@ -35,6 +35,7 @@ import dev.octoshrimpy.quik.common.base.QkThemedActivity
import dev.octoshrimpy.quik.common.util.extensions.hideKeyboard
import dev.octoshrimpy.quik.common.util.extensions.showKeyboard
import dev.octoshrimpy.quik.common.widget.QkDialog
import dev.octoshrimpy.quik.databinding.ContactsActivityBinding
import dev.octoshrimpy.quik.extensions.Optional
import dev.octoshrimpy.quik.feature.compose.editing.ComposeItem
import dev.octoshrimpy.quik.feature.compose.editing.ComposeItemAdapter
@@ -43,7 +44,6 @@ import dev.octoshrimpy.quik.feature.compose.editing.PhoneNumberPickerAdapter
import io.reactivex.Observable
import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.Subject
import kotlinx.android.synthetic.main.contacts_activity.*
import javax.inject.Inject
class ContactsActivity : QkThemedActivity(), ContactsContract {
@@ -53,14 +53,16 @@ class ContactsActivity : QkThemedActivity(), ContactsContract {
const val CHIPS_KEY = "chips"
}
private lateinit var binding: ContactsActivityBinding
@Inject lateinit var contactsAdapter: ComposeItemAdapter
@Inject lateinit var phoneNumberAdapter: PhoneNumberPickerAdapter
@Inject lateinit var viewModelFactory: ViewModelFactory
@Inject lateinit var navigator: Navigator
override val queryChangedIntent: Observable<CharSequence> by lazy { search.textChanges() }
override val queryClearedIntent: Observable<*> by lazy { cancel.clicks() }
override val queryEditorActionIntent: Observable<Int> by lazy { search.editorActions() }
override val queryChangedIntent: Observable<CharSequence> by lazy { binding.search.textChanges() }
override val queryClearedIntent: Observable<*> by lazy { binding.cancel.clicks() }
override val queryEditorActionIntent: Observable<Int> by lazy { binding.search.editorActions() }
override val composeItemPressedIntent: Subject<ComposeItem> by lazy { contactsAdapter.clicks }
override val composeItemLongPressedIntent: Subject<ComposeItem> by lazy { contactsAdapter.longClicks }
override val phoneNumberSelectedIntent: Subject<Optional<Long>> by lazy { phoneNumberAdapter.selectedItemChanges }
@@ -83,11 +85,12 @@ class ContactsActivity : QkThemedActivity(), ContactsContract {
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.contacts_activity)
binding = ContactsActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
showBackButton(true)
viewModel.bindView(this)
contacts.adapter = contactsAdapter
binding.contacts.adapter = contactsAdapter
val callback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
@@ -98,7 +101,7 @@ class ContactsActivity : QkThemedActivity(), ContactsContract {
}
override fun render(state: ContactsState) {
cancel.isVisible = state.query.length > 1
binding.cancel.isVisible = state.query.length > 1
contactsAdapter.data = state.composeItems
@@ -112,17 +115,17 @@ class ContactsActivity : QkThemedActivity(), ContactsContract {
}
override fun clearQuery() {
search.text = null
binding.search.text = null
}
override fun openKeyboard() {
search.postDelayed({
search.showKeyboard()
binding.search.postDelayed({
binding.search.showKeyboard()
}, 200)
}
override fun finish(result: HashMap<String, String?>) {
search.hideKeyboard()
binding.search.hideKeyboard()
val intent = Intent().putExtra(CHIPS_KEY, result)
setResult(Activity.RESULT_OK, intent)
finish()
@@ -23,20 +23,21 @@ import com.bluelinelabs.conductor.Conductor
import com.bluelinelabs.conductor.Router
import com.bluelinelabs.conductor.RouterTransaction
import dagger.android.AndroidInjection
import dev.octoshrimpy.quik.R
import dev.octoshrimpy.quik.common.base.QkThemedActivity
import kotlinx.android.synthetic.main.container_activity.*
import dev.octoshrimpy.quik.databinding.ContainerActivityBinding
class ConversationInfoActivity : QkThemedActivity() {
private lateinit var binding: ContainerActivityBinding
private lateinit var router: Router
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.container_activity)
binding = ContainerActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
router = Conductor.attachRouter(this, container, savedInstanceState)
router = Conductor.attachRouter(this, binding.container, savedInstanceState)
if (!router.hasRootController()) {
val threadId = intent.extras?.getLong("threadId") ?: 0L
router.setRoot(RouterTransaction.with(ConversationInfoController(threadId)))
@@ -11,14 +11,14 @@ import dev.octoshrimpy.quik.common.base.QkViewHolder
import dev.octoshrimpy.quik.common.util.Colors
import dev.octoshrimpy.quik.common.util.extensions.setTint
import dev.octoshrimpy.quik.common.util.extensions.setVisible
import dev.octoshrimpy.quik.databinding.ConversationInfoSettingsBinding
import dev.octoshrimpy.quik.databinding.ConversationMediaListItemBinding
import dev.octoshrimpy.quik.databinding.ConversationRecipientListItemBinding
import dev.octoshrimpy.quik.extensions.isVideo
import dev.octoshrimpy.quik.feature.conversationinfo.ConversationInfoItem.*
import dev.octoshrimpy.quik.util.GlideApp
import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.Subject
import kotlinx.android.synthetic.main.conversation_info_settings.*
import kotlinx.android.synthetic.main.conversation_media_list_item.*
import kotlinx.android.synthetic.main.conversation_recipient_list_item.*
import javax.inject.Inject
class ConversationInfoAdapter @Inject constructor(
@@ -40,37 +40,46 @@ class ConversationInfoAdapter @Inject constructor(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
0 -> QkViewHolder(inflater.inflate(R.layout.conversation_recipient_list_item, parent, false)).apply {
itemView.setOnClickListener {
val item = getItem(adapterPosition) as? ConversationInfoRecipient
item?.value?.id?.run(recipientClicks::onNext)
}
0 -> {
val binding = ConversationRecipientListItemBinding.inflate(inflater, parent, false)
QkViewHolder(binding.root).apply {
itemView.setOnClickListener {
val item = getItem(adapterPosition) as? ConversationInfoRecipient
item?.value?.id?.run(recipientClicks::onNext)
}
itemView.setOnLongClickListener {
val item = getItem(adapterPosition) as? ConversationInfoRecipient
item?.value?.id?.run(recipientLongClicks::onNext)
true
}
itemView.setOnLongClickListener {
val item = getItem(adapterPosition) as? ConversationInfoRecipient
item?.value?.id?.run(recipientLongClicks::onNext)
true
}
theme.setOnClickListener {
val item = getItem(adapterPosition) as? ConversationInfoRecipient
item?.value?.id?.run(themeClicks::onNext)
binding.theme.setOnClickListener {
val item = getItem(adapterPosition) as? ConversationInfoRecipient
item?.value?.id?.run(themeClicks::onNext)
}
}
}
1 -> QkViewHolder(inflater.inflate(R.layout.conversation_info_settings, parent, false)).apply {
groupName.clicks().subscribe(nameClicks)
notifications.clicks().subscribe(notificationClicks)
markUnread.clicks().subscribe(markUnreadClicks)
archive.clicks().subscribe(archiveClicks)
block.clicks().subscribe(blockClicks)
delete.clicks().subscribe(deleteClicks)
1 -> {
val binding = ConversationInfoSettingsBinding.inflate(inflater, parent, false)
QkViewHolder(binding.root).apply {
binding.groupName.clicks().subscribe(nameClicks)
binding.notifications.clicks().subscribe(notificationClicks)
binding.markUnread.clicks().subscribe(markUnreadClicks)
binding.archive.clicks().subscribe(archiveClicks)
binding.block.clicks().subscribe(blockClicks)
binding.delete.clicks().subscribe(deleteClicks)
}
}
2 -> QkViewHolder(inflater.inflate(R.layout.conversation_media_list_item, parent, false)).apply {
itemView.setOnClickListener {
val item = getItem(adapterPosition) as? ConversationInfoMedia
item?.value?.id?.run(mediaClicks::onNext)
2 -> {
val binding = ConversationMediaListItemBinding.inflate(inflater, parent, false)
QkViewHolder(binding.root).apply {
itemView.setOnClickListener {
val item = getItem(adapterPosition) as? ConversationInfoMedia
item?.value?.id?.run(mediaClicks::onNext)
}
}
}
@@ -81,46 +90,49 @@ class ConversationInfoAdapter @Inject constructor(
override fun onBindViewHolder(holder: QkViewHolder, position: Int) {
when (val item = getItem(position)) {
is ConversationInfoRecipient -> {
val binding = ConversationRecipientListItemBinding.bind(holder.itemView)
val recipient = item.value
holder.avatar.setRecipient(recipient)
binding.avatar.setRecipient(recipient)
holder.name.text = recipient.contact?.name ?: recipient.address
binding.name.text = recipient.contact?.name ?: recipient.address
holder.address.text = recipient.address
holder.address.setVisible(recipient.contact != null)
binding.address.text = recipient.address
binding.address.setVisible(recipient.contact != null)
holder.add.setVisible(recipient.contact == null)
binding.add.setVisible(recipient.contact == null)
val theme = colors.theme(recipient)
holder.theme.setTint(theme.theme)
binding.theme.setTint(theme.theme)
}
is ConversationInfoSettings -> {
holder.groupName.summary = item.name
val binding = ConversationInfoSettingsBinding.bind(holder.itemView)
binding.groupName.summary = item.name
holder.notifications.isEnabled = !item.blocked
binding.notifications.isEnabled = !item.blocked
holder.archive.isEnabled = !item.blocked
holder.archive.title = context.getString(when (item.archived) {
binding.archive.isEnabled = !item.blocked
binding.archive.title = context.getString(when (item.archived) {
true -> R.string.info_unarchive
false -> R.string.info_archive
})
holder.block.title = context.getString(when (item.blocked) {
binding.block.title = context.getString(when (item.blocked) {
true -> R.string.info_unblock
false -> R.string.info_block
})
}
is ConversationInfoMedia -> {
val binding = ConversationMediaListItemBinding.bind(holder.itemView)
val part = item.value
GlideApp.with(context)
.load(part.getUri())
.fitCenter()
.into(holder.thumbnail)
.into(binding.thumbnail)
holder.video.isVisible = part.isVideo()
binding.video.isVisible = part.isVideo()
}
}
}
@@ -18,7 +18,9 @@
*/
package dev.octoshrimpy.quik.feature.conversationinfo
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.GridLayoutManager
import com.bluelinelabs.conductor.RouterTransaction
@@ -30,6 +32,7 @@ import dev.octoshrimpy.quik.common.QkChangeHandler
import dev.octoshrimpy.quik.common.base.QkController
import dev.octoshrimpy.quik.common.util.extensions.scrapViews
import dev.octoshrimpy.quik.common.widget.TextInputDialog
import dev.octoshrimpy.quik.databinding.ConversationInfoControllerBinding
import dev.octoshrimpy.quik.feature.blocking.BlockingDialog
import dev.octoshrimpy.quik.feature.conversationinfo.injection.ConversationInfoModule
import dev.octoshrimpy.quik.feature.themepicker.ThemePickerController
@@ -37,12 +40,14 @@ import dev.octoshrimpy.quik.injection.appComponent
import io.reactivex.Observable
import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.Subject
import kotlinx.android.synthetic.main.conversation_info_controller.*
import javax.inject.Inject
class ConversationInfoController(
val threadId: Long = 0
) : QkController<ConversationInfoView, ConversationInfoState, ConversationInfoPresenter>(), ConversationInfoView {
) : QkController<ConversationInfoControllerBinding, ConversationInfoView, ConversationInfoState, ConversationInfoPresenter>(), ConversationInfoView {
override fun inflateBinding(inflater: LayoutInflater, container: ViewGroup): ConversationInfoControllerBinding =
ConversationInfoControllerBinding.inflate(inflater, container, false)
@Inject override lateinit var presenter: ConversationInfoPresenter
@Inject lateinit var blockingDialog: BlockingDialog
@@ -62,14 +67,12 @@ class ConversationInfoController(
.conversationInfoModule(ConversationInfoModule(this))
.build()
.inject(this)
layoutRes = R.layout.conversation_info_controller
}
override fun onViewCreated() {
recyclerView.adapter = adapter
recyclerView.addItemDecoration(GridSpacingItemDecoration(adapter, activity!!))
recyclerView.layoutManager = GridLayoutManager(activity, 3).apply {
binding.recyclerView.adapter = adapter
binding.recyclerView.addItemDecoration(GridSpacingItemDecoration(adapter, activity!!))
binding.recyclerView.layoutManager = GridLayoutManager(activity, 3).apply {
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int = if (adapter.getItemViewType(position) == 2) 1 else 3
}
@@ -77,7 +80,7 @@ class ConversationInfoController(
themedActivity?.theme
?.autoDisposable(scope())
?.subscribe { recyclerView.scrapViews() }
?.subscribe { binding.recyclerView.scrapViews() }
}
override fun onAttach(view: View) {
@@ -27,18 +27,17 @@ import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import dev.octoshrimpy.quik.R
import dev.octoshrimpy.quik.common.Navigator
import dev.octoshrimpy.quik.common.base.QkBindingViewHolder
import dev.octoshrimpy.quik.common.base.QkRealmAdapter
import dev.octoshrimpy.quik.common.base.QkViewHolder
import dev.octoshrimpy.quik.common.util.Colors
import dev.octoshrimpy.quik.common.util.DateFormatter
import dev.octoshrimpy.quik.common.util.extensions.resolveThemeColor
import dev.octoshrimpy.quik.common.util.extensions.setTint
import dev.octoshrimpy.quik.databinding.ConversationListItemBinding
import dev.octoshrimpy.quik.model.Conversation
import dev.octoshrimpy.quik.repository.ScheduledMessageRepository
import dev.octoshrimpy.quik.util.PhoneNumberUtils
import io.reactivex.disposables.CompositeDisposable
import kotlinx.android.synthetic.main.conversation_list_item.*
import kotlinx.android.synthetic.main.conversation_list_item.view.*
import javax.inject.Inject
class ConversationsAdapter @Inject constructor(
@@ -48,7 +47,7 @@ class ConversationsAdapter @Inject constructor(
private val scheduledMessageRepo: ScheduledMessageRepository,
private val navigator: Navigator,
private val phoneNumberUtils: PhoneNumberUtils
) : QkRealmAdapter<Conversation, QkViewHolder>() {
) : QkRealmAdapter<Conversation, QkBindingViewHolder<ConversationListItemBinding>>() {
private val disposables = CompositeDisposable()
init {
@@ -56,44 +55,45 @@ class ConversationsAdapter @Inject constructor(
setHasStableIds(true)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkViewHolder {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkBindingViewHolder<ConversationListItemBinding> {
val layoutInflater = LayoutInflater.from(parent.context)
val view = layoutInflater.inflate(R.layout.conversation_list_item, parent, false)
val binding = ConversationListItemBinding.inflate(layoutInflater, parent, false)
if (viewType == 1) {
val textColorPrimary = parent.context.resolveThemeColor(android.R.attr.textColorPrimary)
view.title.setTypeface(view.title.typeface, Typeface.BOLD)
binding.title.setTypeface(binding.title.typeface, Typeface.BOLD)
view.snippet.setTypeface(view.snippet.typeface, Typeface.BOLD)
view.snippet.setTextColor(textColorPrimary)
view.snippet.maxLines = 5
binding.snippet.setTypeface(binding.snippet.typeface, Typeface.BOLD)
binding.snippet.setTextColor(textColorPrimary)
binding.snippet.maxLines = 5
view.unread.isVisible = true
binding.unread.isVisible = true
view.date.setTypeface(view.date.typeface, Typeface.BOLD)
view.date.setTextColor(textColorPrimary)
binding.date.setTypeface(binding.date.typeface, Typeface.BOLD)
binding.date.setTextColor(textColorPrimary)
}
return QkViewHolder(view).apply {
view.setOnClickListener {
return QkBindingViewHolder(binding).apply {
binding.root.setOnClickListener {
val conversation = getItem(adapterPosition) ?: return@setOnClickListener
when (toggleSelection(conversation.id, false)) {
true -> view.isActivated = isSelected(conversation.id)
true -> binding.root.isActivated = isSelected(conversation.id)
false -> navigator.showConversation(conversation.id)
}
}
view.setOnLongClickListener {
binding.root.setOnLongClickListener {
val conversation = getItem(adapterPosition) ?: return@setOnLongClickListener true
toggleSelection(conversation.id)
view.isActivated = isSelected(conversation.id)
binding.root.isActivated = isSelected(conversation.id)
true
}
}
}
override fun onBindViewHolder(holder: QkViewHolder, position: Int) {
override fun onBindViewHolder(holder: QkBindingViewHolder<ConversationListItemBinding>, position: Int) {
val conversation = getItem(position) ?: return
val binding = holder.binding
// If the last message wasn't incoming, then the colour doesn't really matter anyway
val lastMessage = conversation.lastMessage
@@ -105,22 +105,22 @@ class ConversationsAdapter @Inject constructor(
}
val theme = colors.theme(recipient).theme
holder.containerView.isActivated = isSelected(conversation.id)
holder.itemView.isActivated = isSelected(conversation.id)
holder.avatars.recipients = conversation.recipients
holder.title.collapseEnabled = conversation.recipients.size > 1
holder.title.text = buildSpannedString {
binding.avatars.recipients = conversation.recipients
binding.title.collapseEnabled = conversation.recipients.size > 1
binding.title.text = buildSpannedString {
append(conversation.getTitle())
}
holder.date.text = conversation.date.takeIf { it > 0 }?.let(dateFormatter::getConversationTimestamp)
holder.snippet.text = when {
binding.date.text = conversation.date.takeIf { it > 0 }?.let(dateFormatter::getConversationTimestamp)
binding.snippet.text = when {
conversation.draft.isNotEmpty() -> context.getString(R.string.main_sender_draft, conversation.draft)
conversation.me -> context.getString(R.string.main_sender_you, conversation.snippet)
else -> conversation.snippet
}
// Make the preview in italics if draft
if (conversation.draft.isNotEmpty()) holder.snippet.setTypeface(null, Typeface.ITALIC)
if (conversation.draft.isNotEmpty()) binding.snippet.setTypeface(null, Typeface.ITALIC)
// Get Scheduled Messages
val disposable = scheduledMessageRepo
@@ -128,12 +128,12 @@ class ConversationsAdapter @Inject constructor(
.asFlowable()
.toObservable()
.subscribe { messages ->
holder.scheduled.isVisible = messages.isNotEmpty()
binding.scheduled.isVisible = messages.isNotEmpty()
}
disposables.add(disposable)
holder.pinned.isVisible = conversation.pinned
holder.unread.setTint(theme)
binding.pinned.isVisible = conversation.pinned
binding.unread.setTint(theme)
}
override fun getItemId(position: Int): Long {
@@ -34,15 +34,17 @@ import dev.octoshrimpy.quik.R
import dev.octoshrimpy.quik.common.base.QkActivity
import dev.octoshrimpy.quik.common.util.DateFormatter
import dev.octoshrimpy.quik.common.util.extensions.setVisible
import dev.octoshrimpy.quik.databinding.GalleryActivityBinding
import dev.octoshrimpy.quik.model.MmsPart
import io.reactivex.Observable
import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.Subject
import kotlinx.android.synthetic.main.gallery_activity.*
import javax.inject.Inject
class GalleryActivity : QkActivity(), GalleryView {
private lateinit var binding: GalleryActivityBinding
@Inject lateinit var dateFormatter: DateFormatter
@Inject lateinit var viewModelFactory: ViewModelProvider.Factory
@Inject lateinit var pagerAdapter: GalleryPagerAdapter
@@ -57,12 +59,13 @@ class GalleryActivity : QkActivity(), GalleryView {
delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_YES
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.gallery_activity)
binding = GalleryActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
showBackButton(true)
viewModel.bindView(this)
pager.adapter = pagerAdapter
pager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
binding.pager.adapter = pagerAdapter
binding.pager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
this@GalleryActivity.onPageSelected(position)
}
@@ -74,7 +77,7 @@ class GalleryActivity : QkActivity(), GalleryView {
?.indexOfFirst { part -> part.id == partId }
?.let { index ->
onPageSelected(index)
pager.setCurrentItem(index, false)
binding.pager.setCurrentItem(index, false)
pagerAdapter.unregisterAdapterDataObserver(this)
}
}
@@ -82,15 +85,15 @@ class GalleryActivity : QkActivity(), GalleryView {
}
fun onPageSelected(position: Int) {
toolbarSubtitle.text = pagerAdapter.getItem(position)?.messages?.firstOrNull()?.date
binding.toolbarSubtitle.text = pagerAdapter.getItem(position)?.messages?.firstOrNull()?.date
?.let(dateFormatter::getDetailedTimestamp)
toolbarSubtitle.isVisible = toolbarTitle.text.isNotBlank()
binding.toolbarSubtitle.isVisible = binding.toolbarTitle.text.isNotBlank()
pagerAdapter.getItem(position)?.run(pageChangedSubject::onNext)
}
override fun render(state: GalleryState) {
toolbar.setVisible(state.navigationVisible)
toolbar?.setVisible(state.navigationVisible)
title = state.title
pagerAdapter.updateData(state.parts)
@@ -33,15 +33,14 @@ import com.google.android.mms.ContentType
import dev.octoshrimpy.quik.R
import dev.octoshrimpy.quik.common.base.QkRealmAdapter
import dev.octoshrimpy.quik.common.base.QkViewHolder
import dev.octoshrimpy.quik.databinding.GalleryImagePageBinding
import dev.octoshrimpy.quik.databinding.GalleryVideoPageBinding
import dev.octoshrimpy.quik.extensions.isImage
import dev.octoshrimpy.quik.extensions.isVideo
import dev.octoshrimpy.quik.model.MmsPart
import dev.octoshrimpy.quik.util.GlideApp
import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.Subject
import kotlinx.android.synthetic.main.gallery_image_page.*
import kotlinx.android.synthetic.main.gallery_image_page.view.*
import kotlinx.android.synthetic.main.gallery_video_page.*
import java.util.*
import javax.inject.Inject
@@ -61,25 +60,26 @@ class GalleryPagerAdapter @Inject constructor(private val context: Context) : Qk
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkViewHolder {
val inflater = LayoutInflater.from(parent.context)
return QkViewHolder(when (viewType) {
VIEW_TYPE_IMAGE -> inflater.inflate(R.layout.gallery_image_page, parent, false).apply {
VIEW_TYPE_IMAGE -> {
val binding = GalleryImagePageBinding.inflate(inflater, parent, false)
// When calling the public setter, it doesn't allow the midscale to be the same as the
// maxscale or the minscale. We don't want 3 levels and we don't want to modify the library
// so let's celebrate the invention of reflection!
image.attacher.run {
binding.image.attacher.run {
javaClass.getDeclaredField("mMinScale").run {
isAccessible = true
setFloat(image.attacher, 1f)
setFloat(binding.image.attacher, 1f)
}
javaClass.getDeclaredField("mMidScale").run {
isAccessible = true
setFloat(image.attacher, 1f)
setFloat(binding.image.attacher, 1f)
}
javaClass.getDeclaredField("mMaxScale").run {
isAccessible = true
setFloat(image.attacher, 3f)
setFloat(binding.image.attacher, 3f)
}
}
binding.root
}
VIEW_TYPE_VIDEO -> inflater.inflate(R.layout.gallery_video_page, parent, false)
@@ -93,25 +93,27 @@ class GalleryPagerAdapter @Inject constructor(private val context: Context) : Qk
val part = getItem(position) ?: return
when (getItemViewType(position)) {
VIEW_TYPE_IMAGE -> {
val binding = GalleryImagePageBinding.bind(holder.itemView)
// We need to explicitly request a gif from glide for animations to work
when (part.getUri().let(contentResolver::getType)) {
ContentType.IMAGE_GIF -> GlideApp.with(context)
.asGif()
.load(part.getUri())
.into(holder.image)
.into(binding.image)
else -> GlideApp.with(context)
.asBitmap()
.load(part.getUri())
.into(holder.image)
.into(binding.image)
}
}
VIEW_TYPE_VIDEO -> {
val binding = GalleryVideoPageBinding.bind(holder.itemView)
val videoTrackSelectionFactory = AdaptiveTrackSelection.Factory(null)
val trackSelector = DefaultTrackSelector(videoTrackSelectionFactory)
val exoPlayer = ExoPlayerFactory.newSimpleInstance(context, trackSelector)
holder.video.player = exoPlayer
binding.video.player = exoPlayer
exoPlayers.add(exoPlayer)
val dataSourceFactory = DefaultDataSourceFactory(context, Util.getUserAgent(context, "QUIK"))
@@ -57,6 +57,9 @@ import dev.octoshrimpy.quik.common.util.extensions.setTint
import dev.octoshrimpy.quik.common.util.extensions.setVisible
import dev.octoshrimpy.quik.common.widget.TextInputDialog
import dev.octoshrimpy.quik.feature.blocking.BlockingDialog
import dev.octoshrimpy.quik.databinding.MainActivityBinding
import dev.octoshrimpy.quik.databinding.MainPermissionHintBinding
import dev.octoshrimpy.quik.databinding.MainSyncingBinding
import dev.octoshrimpy.quik.feature.changelog.ChangelogDialog
import dev.octoshrimpy.quik.feature.conversations.ConversationItemTouchCallback
import dev.octoshrimpy.quik.feature.conversations.ConversationsAdapter
@@ -66,10 +69,6 @@ import io.reactivex.Observable
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.Subject
import kotlinx.android.synthetic.main.drawer_view.*
import kotlinx.android.synthetic.main.main_activity.*
import kotlinx.android.synthetic.main.main_permission_hint.*
import kotlinx.android.synthetic.main.main_syncing.*
import javax.inject.Inject
class MainActivity : QkThemedActivity(), MainView {
@@ -83,32 +82,36 @@ class MainActivity : QkThemedActivity(), MainView {
@Inject lateinit var itemTouchCallback: ConversationItemTouchCallback
@Inject lateinit var viewModelFactory: ViewModelProvider.Factory
private lateinit var binding: MainActivityBinding
private lateinit var snackbarBinding: MainPermissionHintBinding
private lateinit var syncingBinding: MainSyncingBinding
override val onNewIntentIntent: Subject<Intent> = PublishSubject.create()
override val activityResumedIntent: Subject<Boolean> = PublishSubject.create()
override val queryChangedIntent by lazy { toolbarSearch.textChanges() }
override val composeIntent by lazy { compose.clicks() }
override val queryChangedIntent by lazy { binding.toolbarSearch.textChanges() }
override val composeIntent by lazy { binding.compose.clicks() }
override val drawerToggledIntent: Observable<Boolean> by lazy {
drawerLayout.drawerOpen(Gravity.START)
binding.drawerLayout.drawerOpen(Gravity.START)
}
override val homeIntent: Subject<Unit> = PublishSubject.create()
override val navigationIntent: Observable<NavItem> by lazy {
Observable.merge(listOf(
backPressedSubject,
inbox.clicks().map { NavItem.INBOX },
archived.clicks().map { NavItem.ARCHIVED },
backup.clicks().map { NavItem.BACKUP },
scheduled.clicks().map { NavItem.SCHEDULED },
blocking.clicks().map { NavItem.BLOCKING },
messageUtils.clicks().map { NavItem.MESSAGE_UTILS },
settings.clicks().map { NavItem.SETTINGS },
binding.drawer.inbox.clicks().map { NavItem.INBOX },
binding.drawer.archived.clicks().map { NavItem.ARCHIVED },
binding.drawer.backup.clicks().map { NavItem.BACKUP },
binding.drawer.scheduled.clicks().map { NavItem.SCHEDULED },
binding.drawer.blocking.clicks().map { NavItem.BLOCKING },
binding.drawer.messageUtils.clicks().map { NavItem.MESSAGE_UTILS },
binding.drawer.settings.clicks().map { NavItem.SETTINGS },
// plus.clicks().map { NavItem.PLUS },
// help.clicks().map { NavItem.HELP },
invite.clicks().map { NavItem.INVITE }))
binding.drawer.invite.clicks().map { NavItem.INVITE }))
}
override val optionsItemIntent: Subject<Int> = PublishSubject.create()
// override val plusBannerIntent by lazy { plusBanner.clicks() }
override val dismissRatingIntent by lazy { rateDismiss.clicks() }
override val rateIntent by lazy { rateOkay.clicks() }
override val dismissRatingIntent by lazy { binding.drawer.rateDismiss.clicks() }
override val rateIntent by lazy { binding.drawer.rateOkay.clicks() }
override val conversationsSelectedIntent by lazy { conversationsAdapter.selectionChanges }
override val confirmDeleteIntent: Subject<List<Long>> = PublishSubject.create()
override val renameConversationIntent: Subject<String> = PublishSubject.create()
@@ -123,52 +126,49 @@ class MainActivity : QkThemedActivity(), MainView {
private val toggle by lazy {
ActionBarDrawerToggle(
this,
drawerLayout,
toolbar,
binding.drawerLayout,
binding.toolbar,
R.string.main_drawer_open_cd,
0
)
}
private val itemTouchHelper by lazy { ItemTouchHelper(itemTouchCallback) }
private val progressAnimator by lazy {
ObjectAnimator.ofInt(syncingProgress, "progress", 0, 0)
ObjectAnimator.ofInt(syncingBinding.syncingProgress, "progress", 0, 0)
}
private val changelogDialog by lazy { ChangelogDialog(this) }
private val snackbar by lazy { findViewById<View>(R.id.snackbar) }
private val syncing by lazy { findViewById<View>(R.id.syncing) }
private val backPressedSubject: Subject<NavItem> = PublishSubject.create()
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
binding = MainActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
viewModel.bindView(this)
onNewIntentIntent.onNext(intent)
(snackbar as? ViewStub)?.setOnInflateListener { _, _ ->
snackbarButton.clicks()
.autoDisposable(scope(Lifecycle.Event.ON_DESTROY))
.subscribe(snackbarButtonIntent)
snackbarBinding = MainPermissionHintBinding.bind(binding.snackbar.inflate()).also {
it.snackbarButton.clicks()
.autoDisposable(scope(Lifecycle.Event.ON_DESTROY))
.subscribe(snackbarButtonIntent)
}
(syncing as? ViewStub)?.setOnInflateListener { _, _ ->
syncingProgress?.let {
it.progressTintList = ColorStateList.valueOf(theme.blockingFirst().theme)
it.indeterminateTintList = ColorStateList.valueOf(theme.blockingFirst().theme)
}
syncingBinding = MainSyncingBinding.bind(binding.syncing.inflate()).also {
it.syncingProgress.progressTintList = ColorStateList.valueOf(theme.blockingFirst().theme)
it.syncingProgress.indeterminateTintList = ColorStateList.valueOf(theme.blockingFirst().theme)
}
toggle.syncState()
toolbar.setNavigationOnClickListener {
binding.toolbar.setNavigationOnClickListener {
dismissKeyboard()
homeIntent.onNext(Unit)
}
itemTouchCallback.adapter = conversationsAdapter
conversationsAdapter.autoScrollToStart(recyclerView)
conversationsAdapter.autoScrollToStart(binding.recyclerView)
// Don't allow clicks to pass through the drawer layout
drawer.clicks().autoDisposable(scope()).subscribe()
binding.drawer.root.clicks().autoDisposable(scope()).subscribe()
// Set the theme color tint to the recyclerView, progressbar, and FAB
theme
@@ -183,23 +183,23 @@ class MainActivity : QkThemedActivity(), MainView {
resolveThemeColor(android.R.attr.textColorSecondary)
))
.let { tintList ->
inboxIcon.imageTintList = tintList
archivedIcon.imageTintList = tintList
binding.drawer.inboxIcon.imageTintList = tintList
binding.drawer.archivedIcon.imageTintList = tintList
}
// Miscellaneous views
listOf(plusBadge1, plusBadge2).forEach { badge ->
listOf(binding.drawer.plusBadge1, binding.drawer.plusBadge2).forEach { badge ->
badge.setBackgroundTint(theme.theme)
badge.setTextColor(theme.textPrimary)
}
syncingProgress?.progressTintList = ColorStateList.valueOf(theme.theme)
syncingProgress?.indeterminateTintList = ColorStateList.valueOf(theme.theme)
plusIcon.setTint(theme.theme)
rateIcon.setTint(theme.theme)
compose.setBackgroundTint(theme.theme)
syncingBinding.syncingProgress.progressTintList = ColorStateList.valueOf(theme.theme)
syncingBinding.syncingProgress.indeterminateTintList = ColorStateList.valueOf(theme.theme)
binding.drawer.plusIcon.setTint(theme.theme)
binding.drawer.rateIcon.setTint(theme.theme)
binding.compose.setBackgroundTint(theme.theme)
// Set the FAB compose icon color
compose.setTint(theme.textPrimary)
binding.compose.setTint(theme.textPrimary)
}
}
@@ -239,13 +239,13 @@ class MainActivity : QkThemedActivity(), MainView {
else -> 0
}
toolbarSearch.setVisible(state.page is Inbox &&
binding.toolbarSearch.setVisible(state.page is Inbox &&
state.page.selected == 0 ||
state.page is Searching
)
toolbarTitle.setVisible(toolbarSearch.visibility != View.VISIBLE)
binding.toolbarTitle.setVisible(binding.toolbarSearch.visibility != View.VISIBLE)
toolbar.menu.apply {
binding.toolbar.menu.apply {
findItem(R.id.select_all)?.isVisible =
(conversationsAdapter.itemCount > 1) && selectedConversations != 0
findItem(R.id.archive)?.isVisible =
@@ -264,36 +264,36 @@ class MainActivity : QkThemedActivity(), MainView {
findItem(R.id.rename)?.isVisible = selectedConversations == 1
}
listOf(plusBadge1, plusBadge2).forEach { badge ->
listOf(binding.drawer.plusBadge1, binding.drawer.plusBadge2).forEach { badge ->
badge.isVisible = drawerBadgesExperiment.variant && !state.upgraded
}
// plus.isVisible = state.upgraded
plusBanner.isVisible = !state.upgraded
rateLayout.setVisible(state.showRating)
binding.drawer.plusBanner.isVisible = !state.upgraded
binding.drawer.rateLayout.setVisible(state.showRating)
compose.setVisible(state.page is Inbox || state.page is Archived)
conversationsAdapter.emptyView = empty.takeIf {
binding.compose.setVisible(state.page is Inbox || state.page is Archived)
conversationsAdapter.emptyView = binding.empty.takeIf {
state.page is Inbox || state.page is Archived
}
searchAdapter.emptyView = empty.takeIf { state.page is Searching }
searchAdapter.emptyView = binding.empty.takeIf { state.page is Searching }
when (state.page) {
is Inbox -> {
showBackButton(state.page.selected > 0)
title = getString(R.string.main_title_selected, state.page.selected)
if (recyclerView.adapter !== conversationsAdapter)
recyclerView.adapter = conversationsAdapter
if (binding.recyclerView.adapter !== conversationsAdapter)
binding.recyclerView.adapter = conversationsAdapter
conversationsAdapter.updateData(state.page.data)
itemTouchHelper.attachToRecyclerView(recyclerView)
empty.setText(R.string.inbox_empty_text)
itemTouchHelper.attachToRecyclerView(binding.recyclerView)
binding.empty.setText(R.string.inbox_empty_text)
}
is Searching -> {
showBackButton(true)
if (recyclerView.adapter !== searchAdapter) recyclerView.adapter = searchAdapter
if (binding.recyclerView.adapter !== searchAdapter) binding.recyclerView.adapter = searchAdapter
searchAdapter.data = state.page.data ?: listOf()
itemTouchHelper.attachToRecyclerView(null)
empty.setText(R.string.inbox_search_empty_text)
binding.empty.setText(R.string.inbox_search_empty_text)
}
is Archived -> {
@@ -302,78 +302,78 @@ class MainActivity : QkThemedActivity(), MainView {
true -> getString(R.string.main_title_selected, state.page.selected)
false -> getString(R.string.title_archived)
}
if (recyclerView.adapter !== conversationsAdapter)
recyclerView.adapter = conversationsAdapter
if (binding.recyclerView.adapter !== conversationsAdapter)
binding.recyclerView.adapter = conversationsAdapter
conversationsAdapter.updateData(state.page.data)
itemTouchHelper.attachToRecyclerView(null)
empty.setText(R.string.archived_empty_text)
binding.empty.setText(R.string.archived_empty_text)
}
else -> {}
}
inbox.isActivated = state.page is Inbox
archived.isActivated = state.page is Archived
binding.drawer.inbox.isActivated = state.page is Inbox
binding.drawer.archived.isActivated = state.page is Archived
if (drawerLayout.isDrawerOpen(GravityCompat.START) && !state.drawerOpen)
drawerLayout.closeDrawer(GravityCompat.START)
else if (!drawerLayout.isDrawerVisible(GravityCompat.START) && state.drawerOpen)
drawerLayout.openDrawer(GravityCompat.START)
if (binding.drawerLayout.isDrawerOpen(GravityCompat.START) && !state.drawerOpen)
binding.drawerLayout.closeDrawer(GravityCompat.START)
else if (!binding.drawerLayout.isDrawerVisible(GravityCompat.START) && state.drawerOpen)
binding.drawerLayout.openDrawer(GravityCompat.START)
when (state.syncing) {
is SyncRepository.SyncProgress.Idle -> {
syncing.isVisible = false
snackbar.isVisible = (!state.defaultSms ||
syncingBinding.root.isVisible = false
snackbarBinding.root.isVisible = (!state.defaultSms ||
!state.smsPermission ||
!state.contactPermission ||
!state.notificationPermission)
}
is SyncRepository.SyncProgress.Running -> {
syncing.isVisible = true
syncingProgress.max = state.syncing.max
syncingBinding.root.isVisible = true
syncingBinding.syncingProgress.max = state.syncing.max
progressAnimator.apply {
setIntValues(syncingProgress.progress, state.syncing.progress)
setIntValues(syncingBinding.syncingProgress.progress, state.syncing.progress)
}.start()
syncingProgress.isIndeterminate = state.syncing.indeterminate
snackbar.isVisible = false
syncingBinding.syncingProgress.isIndeterminate = state.syncing.indeterminate
snackbarBinding.root.isVisible = false
}
is SyncRepository.SyncProgress.ParsingEmojis -> {
syncing.isVisible = true
syncingLabel.setText(getString(R.string.main_sync_emojis))
syncingProgress.max = state.syncing.max
syncingBinding.root.isVisible = true
syncingBinding.syncingLabel.setText(getString(R.string.main_sync_emojis))
syncingBinding.syncingProgress.max = state.syncing.max
progressAnimator.apply {
setIntValues(syncingProgress.progress, state.syncing.progress)
setIntValues(syncingBinding.syncingProgress.progress, state.syncing.progress)
}.start()
syncingProgress.isIndeterminate = state.syncing.indeterminate
snackbar.isVisible = false
syncingBinding.syncingProgress.isIndeterminate = state.syncing.indeterminate
snackbarBinding.root.isVisible = false
}
}
when {
!state.defaultSms -> {
snackbarTitle?.setText(R.string.main_default_sms_title)
snackbarMessage?.setText(R.string.main_default_sms_message)
snackbarButton?.setText(R.string.main_default_sms_change)
snackbarBinding.snackbarTitle.setText(R.string.main_default_sms_title)
snackbarBinding.snackbarMessage.setText(R.string.main_default_sms_message)
snackbarBinding.snackbarButton.setText(R.string.main_default_sms_change)
}
!state.smsPermission -> {
snackbarTitle?.setText(R.string.main_permission_required)
snackbarMessage?.setText(R.string.main_permission_sms)
snackbarButton?.setText(R.string.main_permission_allow)
snackbarBinding.snackbarTitle.setText(R.string.main_permission_required)
snackbarBinding.snackbarMessage.setText(R.string.main_permission_sms)
snackbarBinding.snackbarButton.setText(R.string.main_permission_allow)
}
!state.contactPermission -> {
snackbarTitle?.setText(R.string.main_permission_required)
snackbarMessage?.setText(R.string.main_permission_contacts)
snackbarButton?.setText(R.string.main_permission_allow)
snackbarBinding.snackbarTitle.setText(R.string.main_permission_required)
snackbarBinding.snackbarMessage.setText(R.string.main_permission_contacts)
snackbarBinding.snackbarButton.setText(R.string.main_permission_allow)
}
!state.notificationPermission -> {
snackbarTitle?.setText(R.string.main_permission_required)
snackbarMessage?.setText(R.string.main_permission_notifications)
snackbarButton?.setText(R.string.main_permission_allow)
snackbarBinding.snackbarTitle.setText(R.string.main_permission_required)
snackbarBinding.snackbarMessage.setText(R.string.main_permission_notifications)
snackbarBinding.snackbarButton.setText(R.string.main_permission_allow)
}
}
}
@@ -389,7 +389,7 @@ class MainActivity : QkThemedActivity(), MainView {
override fun showBackButton(show: Boolean) =
toggle.let {
it.onDrawerSlide(drawer, if (show) 1f else 0f)
it.onDrawerSlide(binding.drawer.root, if (show) 1f else 0f)
it.drawerArrowDrawable.color = when (show) {
true -> resolveThemeColor(android.R.attr.textColorSecondary)
false -> resolveThemeColor(android.R.attr.textColorPrimary)
@@ -414,14 +414,14 @@ class MainActivity : QkThemedActivity(), MainView {
override fun clearSearch() {
dismissKeyboard()
toolbarSearch.text = null
binding.toolbarSearch.text = null
}
override fun clearSelection() = conversationsAdapter.clearSelection()
override fun toggleSelectAll() = conversationsAdapter.toggleSelectAll()
override fun themeChanged() = recyclerView.scrapViews()
override fun themeChanged() = binding.recyclerView.scrapViews()
override fun showBlockingDialog(conversations: List<Long>, block: Boolean) {
blockingDialog.show(this, conversations, block)
@@ -456,7 +456,7 @@ class MainActivity : QkThemedActivity(), MainView {
override fun showArchivedSnackbar(countConversationsArchived: Int, isArchiving: Boolean) =
Snackbar.make(
drawerLayout,
binding.drawerLayout,
if (isArchiving) {
resources.getQuantityString(R.plurals.toast_archived, countConversationsArchived, countConversationsArchived)
} else {
@@ -484,9 +484,9 @@ class MainActivity : QkThemedActivity(), MainView {
override fun drawerToggled(opened: Boolean) {
if (opened) {
dismissKeyboard()
if (!inbox.isInTouchMode)
inbox.requestFocus()
if (!binding.drawer.inbox.isInTouchMode)
binding.drawer.inbox.requestFocus()
} else
toolbarSearch.requestFocus()
binding.toolbarSearch.requestFocus()
}
}
@@ -11,7 +11,6 @@ import com.jakewharton.rxbinding2.view.clicks
import dev.octoshrimpy.quik.R
import dev.octoshrimpy.quik.common.base.QkController
import dev.octoshrimpy.quik.common.util.extensions.animateLayoutChanges
import dev.octoshrimpy.quik.common.widget.QkSwitch
import dev.octoshrimpy.quik.databinding.MessageUtilsControllerBinding
import dev.octoshrimpy.quik.feature.settings.autodelete.AutoDeleteDialog
import dev.octoshrimpy.quik.injection.appComponent
@@ -26,32 +25,30 @@ import kotlinx.coroutines.withContext
import javax.inject.Inject
import kotlin.coroutines.resume
class MessageUtilsController : QkController<MessageUtilsView, MessageUtilsState, MessageUtilsPresenter>(), MessageUtilsView {
class MessageUtilsController : QkController<MessageUtilsControllerBinding, MessageUtilsView, MessageUtilsState, MessageUtilsPresenter>(), MessageUtilsView {
override fun inflateBinding(inflater: LayoutInflater, container: ViewGroup): MessageUtilsControllerBinding =
MessageUtilsControllerBinding.inflate(inflater, container, false)
@Inject lateinit var context: Context
@Inject override lateinit var presenter: MessageUtilsPresenter
private val autoDeleteDialog: AutoDeleteDialog by lazy {
AutoDeleteDialog(activity!!, autoDeleteSubject::onNext)
}
private val autoDeleteSubject: Subject<Int> = PublishSubject.create()
private var binding: MessageUtilsControllerBinding? = null
override val autoDeduplicateClickIntent by lazy { binding!!.autoDeduplicate.clicks() }
override val deduplicateClickIntent by lazy { binding!!.deduplicateMessages.clicks() }
override val autoDeleteClickIntent by lazy { binding!!.autoDelete.clicks() }
override val autoDeduplicateClickIntent by lazy { binding.autoDeduplicate.clicks() }
override val deduplicateClickIntent by lazy { binding.deduplicateMessages.clicks() }
override val autoDeleteClickIntent by lazy { binding.autoDelete.clicks() }
init {
appComponent.inject(this)
retainViewMode = RetainViewMode.RETAIN_DETACH
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = MessageUtilsControllerBinding.inflate(inflater, container, false)
return binding!!.root
}
override fun onViewCreated() {
super.onViewCreated()
(binding?.root as? ViewGroup)?.postDelayed({
binding?.parent?.animateLayoutChanges = true
binding.root.postDelayed({
binding.parent.animateLayoutChanges = true
}, 100)
}
@@ -62,17 +59,10 @@ class MessageUtilsController : QkController<MessageUtilsView, MessageUtilsState,
presenter.bindIntents(this)
}
override fun onDestroyView(view: View) {
super.onDestroyView(view)
binding = null
}
override fun render(state: MessageUtilsState) {
binding?.autoDeduplicate
?.findViewById<QkSwitch>(R.id.checkbox)
?.isChecked = state.autoDeduplicateMessages
binding.autoDeduplicate.checkbox?.isChecked = state.autoDeduplicateMessages
val deduplicationProgress = binding!!.deduplicationProgress
val deduplicationProgress = binding.deduplicationProgress
val progressAnimator = ObjectAnimator.ofInt(deduplicationProgress, "progress", 0, 0)
when (state.deduplicationProgress) {
@@ -114,8 +104,8 @@ class MessageUtilsController : QkController<MessageUtilsView, MessageUtilsState,
}
override fun handleDeduplicationResult(resIdString: Int) {
binding?.deduplicationProgressText?.isVisible = true
binding?.deduplicationProgressText?.setText(resIdString)
binding.deduplicationProgressText.isVisible = true
binding.deduplicationProgressText.setText(resIdString)
}
override fun autoDeleteChanged(): Observable<Int> = autoDeleteSubject
@@ -37,15 +37,16 @@ import dev.octoshrimpy.quik.common.base.QkThemedActivity
import dev.octoshrimpy.quik.common.util.extensions.animateLayoutChanges
import dev.octoshrimpy.quik.common.util.extensions.setVisible
import dev.octoshrimpy.quik.common.widget.PreferenceView
import dev.octoshrimpy.quik.databinding.NotificationPrefsActivityBinding
import io.reactivex.Observable
import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.Subject
import kotlinx.android.synthetic.main.notification_prefs_activity.*
import kotlinx.android.synthetic.main.settings_switch_widget.view.*
import javax.inject.Inject
class NotificationPrefsActivity : QkThemedActivity(), NotificationPrefsView {
private lateinit var binding: NotificationPrefsActivityBinding
@Inject lateinit var previewModeDialog: QkDialog
@Inject lateinit var actionsDialog: QkDialog
@Inject lateinit var viewModelFactory: ViewModelProvider.Factory
@@ -62,27 +63,28 @@ class NotificationPrefsActivity : QkThemedActivity(), NotificationPrefsView {
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.notification_prefs_activity)
binding = NotificationPrefsActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
setTitle(R.string.title_notification_prefs)
showBackButton(true)
viewModel.bindView(this)
preferences.postDelayed({ preferences?.animateLayoutChanges = true }, 100)
binding.preferences.postDelayed({ binding.preferences.animateLayoutChanges = true }, 100)
val hasOreo = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
notificationsO.setVisible(hasOreo)
notifications.setVisible(!hasOreo)
vibration.setVisible(!hasOreo)
ringtone.setVisible(!hasOreo)
binding.notificationsO.setVisible(hasOreo)
binding.notifications.setVisible(!hasOreo)
binding.vibration.setVisible(!hasOreo)
binding.ringtone.setVisible(!hasOreo)
previewModeDialog.setTitle(R.string.settings_notification_previews_title)
previewModeDialog.adapter.setData(R.array.notification_preview_options)
actionsDialog.adapter.setData(R.array.notification_actions)
// Listen to clicks for all of the preferences
(0 until preferences.childCount)
.map { index -> preferences.getChildAt(index) }
(0 until binding.preferences.childCount)
.map { index -> binding.preferences.getChildAt(index) }
.mapNotNull { view -> view as? PreferenceView }
.map { preference -> preference.clicks().map { preference } }
.let { Observable.merge(it) }
@@ -95,31 +97,31 @@ class NotificationPrefsActivity : QkThemedActivity(), NotificationPrefsView {
title = state.conversationTitle
}
notifications.checkbox.isChecked = state.notificationsEnabled
previews.summary = state.previewSummary
binding.notifications.checkbox?.isChecked = state.notificationsEnabled
binding.previews.summary = state.previewSummary
previewModeDialog.adapter.selectedItem = state.previewId
wake.checkbox.isChecked = state.wakeEnabled
silentNotContact.checkbox.isChecked = state.silentNotContact
silentNotContact.isVisible = state.threadId == 0L
vibration.checkbox.isChecked = state.vibrationEnabled
ringtone.summary = state.ringtoneName
binding.wake.checkbox?.isChecked = state.wakeEnabled
binding.silentNotContact.checkbox?.isChecked = state.silentNotContact
binding.silentNotContact.isVisible = state.threadId == 0L
binding.vibration.checkbox?.isChecked = state.vibrationEnabled
binding.ringtone.summary = state.ringtoneName
actionsDivider.isVisible = state.threadId == 0L
actionsTitle.isVisible = state.threadId == 0L
action1.isVisible = state.threadId == 0L
action1.summary = state.action1Summary
action2.isVisible = state.threadId == 0L
action2.summary = state.action2Summary
action3.isVisible = state.threadId == 0L
action3.summary = state.action3Summary
binding.actionsDivider.isVisible = state.threadId == 0L
binding.actionsTitle.isVisible = state.threadId == 0L
binding.action1.isVisible = state.threadId == 0L
binding.action1.summary = state.action1Summary
binding.action2.isVisible = state.threadId == 0L
binding.action2.summary = state.action2Summary
binding.action3.isVisible = state.threadId == 0L
binding.action3.summary = state.action3Summary
qkreplyDivider.isVisible = state.threadId == 0L
qkreplyTitle.isVisible = state.threadId == 0L
qkreply.checkbox.isChecked = state.qkReplyEnabled
qkreply.isVisible = state.threadId == 0L
qkreplyTapDismiss.isVisible = state.threadId == 0L
qkreplyTapDismiss.isEnabled = state.qkReplyEnabled
qkreplyTapDismiss.checkbox.isChecked = state.qkReplyTapDismiss
binding.qkreplyDivider.isVisible = state.threadId == 0L
binding.qkreplyTitle.isVisible = state.threadId == 0L
binding.qkreply.checkbox?.isChecked = state.qkReplyEnabled
binding.qkreply.isVisible = state.threadId == 0L
binding.qkreplyTapDismiss.isVisible = state.threadId == 0L
binding.qkreplyTapDismiss.isEnabled = state.qkReplyEnabled
binding.qkreplyTapDismiss.checkbox?.isChecked = state.qkReplyTapDismiss
}
override fun showPreviewModeDialog() = previewModeDialog.show(this)
@@ -83,7 +83,7 @@ class PlusActivity : QkThemedActivity(), PlusView {
// Make the list titles bold
binding.linearLayout.children
.mapNotNull { it as? PreferenceView }
.map { it.titleTextView }
.map { it.titleView }
.forEach { it.setTypeface(it.typeface, Typeface.BOLD) }
val textPrimary = resolveThemeColor(android.R.attr.textColorPrimary)
@@ -45,22 +45,23 @@ import dev.octoshrimpy.quik.common.util.extensions.autoScrollToStart
import dev.octoshrimpy.quik.common.util.extensions.setVisible
import dev.octoshrimpy.quik.common.util.extensions.showKeyboard
import dev.octoshrimpy.quik.common.widget.QkEditText
import dev.octoshrimpy.quik.databinding.QkreplyActivityBinding
import dev.octoshrimpy.quik.feature.compose.MessagesAdapter
import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.Subject
import kotlinx.android.synthetic.main.compose_activity.message
import kotlinx.android.synthetic.main.qkreply_activity.*
import javax.inject.Inject
class QkReplyActivity : QkThemedActivity(), QkReplyView {
private lateinit var binding: QkreplyActivityBinding
@Inject lateinit var adapter: MessagesAdapter
@Inject lateinit var viewModelFactory: ViewModelProvider.Factory
override val menuItemIntent: Subject<Int> = PublishSubject.create()
override val textChangedIntent by lazy { message.textChanges() }
override val changeSimIntent by lazy { sim.clicks() }
override val sendIntent by lazy { send.clicks() }
override val textChangedIntent by lazy { binding.message.textChanges() }
override val changeSimIntent by lazy { binding.sim.clicks() }
override val sendIntent by lazy { binding.send.clicks() }
private val viewModel by lazy { ViewModelProviders.of(this, viewModelFactory)[QkReplyViewModel::class.java] }
@@ -90,21 +91,22 @@ class QkReplyActivity : QkThemedActivity(), QkReplyView {
super.onCreate(savedInstanceState)
setFinishOnTouchOutside(prefs.qkreplyTapDismiss.get())
setContentView(R.layout.qkreply_activity)
binding = QkreplyActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
window.setBackgroundDrawable(null)
window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
viewModel.bindView(this)
toolbar.clipToOutline = true
binding.toolbar.clipToOutline = true
messages.adapter = adapter
messages.adapter?.autoScrollToStart(messages)
messages.adapter?.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onChanged() = messages.scrollToPosition(adapter.itemCount - 1)
binding.messages.adapter = adapter
binding.messages.adapter?.autoScrollToStart(binding.messages)
binding.messages.adapter?.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onChanged() = binding.messages.scrollToPosition(adapter.itemCount - 1)
})
message.setOnTouchListener(object : OnTouchListener {
binding.message.setOnTouchListener(object : OnTouchListener {
private val gestureDetector =
GestureDetector(this@QkReplyActivity, object : SimpleOnGestureListener() {
override fun onDoubleTap(e: MotionEvent): Boolean {
@@ -119,7 +121,7 @@ class QkReplyActivity : QkThemedActivity(), QkReplyView {
}
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
message.showKeyboard()
binding.message.showKeyboard()
return true
}
@@ -143,24 +145,24 @@ class QkReplyActivity : QkThemedActivity(), QkReplyView {
title = state.title
toolbar.menu.findItem(R.id.expand)?.isVisible = !state.expanded
toolbar.menu.findItem(R.id.collapse)?.isVisible = state.expanded
binding.toolbar.menu.findItem(R.id.expand)?.isVisible = !state.expanded
binding.toolbar.menu.findItem(R.id.collapse)?.isVisible = state.expanded
adapter.data = state.data
counter.text = state.remaining
counter.setVisible(counter.text.isNotBlank())
binding.counter.text = state.remaining
binding.counter.setVisible(binding.counter.text.isNotBlank())
sim.setVisible(state.subscription != null)
sim.contentDescription = getString(R.string.compose_sim_cd, state.subscription?.displayName)
simIndex.text = "${state.subscription?.simSlotIndex?.plus(1)}"
binding.sim.setVisible(state.subscription != null)
binding.sim.contentDescription = getString(R.string.compose_sim_cd, state.subscription?.displayName)
binding.simIndex.text = "${state.subscription?.simSlotIndex?.plus(1)}"
send.isEnabled = state.canSend
send.imageAlpha = if (state.canSend) 255 else 128
binding.send.isEnabled = state.canSend
binding.send.imageAlpha = if (state.canSend) 255 else 128
}
override fun setDraft(draft: String) {
message.setText(draft)
binding.message.setText(draft)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
@@ -31,20 +31,21 @@ import dev.octoshrimpy.quik.R
import dev.octoshrimpy.quik.common.base.QkThemedActivity
import dev.octoshrimpy.quik.common.util.extensions.setBackgroundTint
import dev.octoshrimpy.quik.common.util.extensions.setTint
import dev.octoshrimpy.quik.databinding.ScheduledActivityBinding
import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.Subject
import kotlinx.android.synthetic.main.main_activity.toolbar
import kotlinx.android.synthetic.main.scheduled_activity.*
import javax.inject.Inject
class ScheduledActivity : QkThemedActivity(), ScheduledView {
private lateinit var binding: ScheduledActivityBinding
@Inject lateinit var scheduledMessageAdapter: ScheduledMessageAdapter
@Inject lateinit var viewModelFactory: ViewModelProvider.Factory
override val composeIntent by lazy { compose.clicks() }
override val upgradeIntent by lazy { upgrade.clicks() }
override val composeIntent by lazy { binding.compose.clicks() }
override val upgradeIntent by lazy { binding.upgrade.clicks() }
override val messagesSelectedIntent by lazy { scheduledMessageAdapter.selectionChanges }
override val optionsItemIntent: Subject<Int> = PublishSubject.create()
override val deleteScheduledMessages: Subject<List<Long>> = PublishSubject.create()
@@ -59,22 +60,23 @@ class ScheduledActivity : QkThemedActivity(), ScheduledView {
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.scheduled_activity)
binding = ScheduledActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
setTitle(R.string.scheduled_title)
showBackButton(true)
viewModel.bindView(this)
scheduledMessageAdapter.emptyView = empty
messages.adapter = scheduledMessageAdapter
scheduledMessageAdapter.emptyView = binding.empty
binding.messages.adapter = scheduledMessageAdapter
colors.theme().let { theme ->
sampleMessage.setBackgroundTint(theme.theme)
sampleMessage.setTextColor(theme.textPrimary)
compose.setTint(theme.textPrimary)
compose.setBackgroundTint(theme.theme)
upgrade.setBackgroundTint(theme.theme)
upgradeIcon.setTint(theme.textPrimary)
upgradeLabel.setTextColor(theme.textPrimary)
binding.sampleMessage.setBackgroundTint(theme.theme)
binding.sampleMessage.setTextColor(theme.textPrimary)
binding.compose.setTint(theme.textPrimary)
binding.compose.setBackgroundTint(theme.theme)
binding.upgrade.setBackgroundTint(theme.theme)
binding.upgradeIcon.setTint(theme.textPrimary)
binding.upgradeLabel.setTextColor(theme.textPrimary)
}
}
@@ -88,20 +90,20 @@ class ScheduledActivity : QkThemedActivity(), ScheduledView {
})
// show/hide menu items
toolbar.menu.findItem(R.id.select_all)?.isVisible =
toolbar?.menu?.findItem(R.id.select_all)?.isVisible =
((scheduledMessageAdapter.itemCount > 1) && (state.selectedMessages != 0))
toolbar.menu.findItem(R.id.delete)?.isVisible =
toolbar?.menu?.findItem(R.id.delete)?.isVisible =
((scheduledMessageAdapter.itemCount != 0) && (state.selectedMessages != 0))
toolbar.menu.findItem(R.id.copy)?.isVisible =
toolbar?.menu?.findItem(R.id.copy)?.isVisible =
((scheduledMessageAdapter.itemCount != 0) && (state.selectedMessages != 0))
toolbar.menu.findItem(R.id.send_now)?.isVisible =
toolbar?.menu?.findItem(R.id.send_now)?.isVisible =
((scheduledMessageAdapter.itemCount != 0) && (state.selectedMessages != 0))
toolbar.menu.findItem(R.id.edit_message)?.isVisible =
toolbar?.menu?.findItem(R.id.edit_message)?.isVisible =
((scheduledMessageAdapter.itemCount != 0) && (state.selectedMessages == 1))
// show compose button
compose.isVisible = state.upgraded && (state.conversationId == null)
upgrade.isVisible = !state.upgraded
binding.compose.isVisible = state.upgraded && (state.conversationId == null)
binding.upgrade.isVisible = !state.upgraded
}
override fun onBackPressed() = backPressedIntent.onNext(Unit)
@@ -23,20 +23,21 @@ import com.bluelinelabs.conductor.Conductor
import com.bluelinelabs.conductor.Router
import com.bluelinelabs.conductor.RouterTransaction
import dagger.android.AndroidInjection
import dev.octoshrimpy.quik.R
import dev.octoshrimpy.quik.common.base.QkThemedActivity
import kotlinx.android.synthetic.main.container_activity.*
import dev.octoshrimpy.quik.databinding.ContainerActivityBinding
class SettingsActivity : QkThemedActivity() {
private lateinit var binding: ContainerActivityBinding
private lateinit var router: Router
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.container_activity)
binding = ContainerActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
router = Conductor.attachRouter(this, container, savedInstanceState)
router = Conductor.attachRouter(this, binding.container, savedInstanceState)
if (!router.hasRootController()) {
router.setRoot(RouterTransaction.with(SettingsController()))
}
@@ -23,7 +23,9 @@ import android.app.TimePickerDialog
import android.content.Context
import android.os.Build
import android.text.format.DateFormat
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import com.bluelinelabs.conductor.RouterTransaction
import com.google.android.material.snackbar.Snackbar
@@ -43,6 +45,7 @@ import dev.octoshrimpy.quik.common.util.extensions.setBackgroundTint
import dev.octoshrimpy.quik.common.util.extensions.setVisible
import dev.octoshrimpy.quik.common.widget.PreferenceView
import dev.octoshrimpy.quik.common.widget.TextInputDialog
import dev.octoshrimpy.quik.databinding.SettingsControllerBinding
import dev.octoshrimpy.quik.feature.settings.about.AboutController
import dev.octoshrimpy.quik.feature.settings.swipe.SwipeActionsController
import dev.octoshrimpy.quik.feature.themepicker.ThemePickerController
@@ -52,13 +55,12 @@ import dev.octoshrimpy.quik.util.Preferences
import io.reactivex.Observable
import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.Subject
import kotlinx.android.synthetic.main.settings_controller.*
import kotlinx.android.synthetic.main.settings_controller.view.*
import kotlinx.android.synthetic.main.settings_switch_widget.view.*
import kotlinx.android.synthetic.main.settings_theme_widget.*
import javax.inject.Inject
class SettingsController : QkController<SettingsView, SettingsState, SettingsPresenter>(), SettingsView {
class SettingsController : QkController<SettingsControllerBinding, SettingsView, SettingsState, SettingsPresenter>(), SettingsView {
override fun inflateBinding(inflater: LayoutInflater, container: ViewGroup): SettingsControllerBinding =
SettingsControllerBinding.inflate(inflater, container, false)
@Inject lateinit var context: Context
@Inject lateinit var colors: Colors
@@ -79,12 +81,11 @@ class SettingsController : QkController<SettingsView, SettingsState, SettingsPre
private val endTimeSelectedSubject: Subject<Pair<Int, Int>> = PublishSubject.create()
private val signatureSubject: Subject<String> = PublishSubject.create()
private val progressAnimator by lazy { ObjectAnimator.ofInt(syncingProgress, "progress", 0, 0) }
private val progressAnimator by lazy { ObjectAnimator.ofInt(binding.syncingProgress, "progress", 0, 0) }
init {
appComponent.inject(this)
retainViewMode = RetainViewMode.RETAIN_DETACH
layoutRes = R.layout.settings_controller
colors.themeObservable()
.autoDisposable(scope())
@@ -92,7 +93,7 @@ class SettingsController : QkController<SettingsView, SettingsState, SettingsPre
}
override fun onViewCreated() {
preferences.postDelayed({ preferences?.animateLayoutChanges = true }, 100)
binding.preferences.postDelayed({ binding.preferences.animateLayoutChanges = true }, 100)
when (Build.VERSION.SDK_INT >= 29) {
true -> nightModeDialog.adapter.setData(R.array.night_modes)
@@ -105,7 +106,7 @@ class SettingsController : QkController<SettingsView, SettingsState, SettingsPre
mmsSizeDialog.adapter.setData(R.array.mms_sizes, R.array.mms_sizes_ids)
messageLinkHandlingDialog.adapter.setData(R.array.messageLinkHandlings, R.array.messageLinkHandling_ids)
about.summary = context.getString(R.string.settings_version, BuildConfig.VERSION_NAME)
binding.about.summary = context.getString(R.string.settings_version, BuildConfig.VERSION_NAME)
}
override fun onAttach(view: View) {
@@ -115,13 +116,13 @@ class SettingsController : QkController<SettingsView, SettingsState, SettingsPre
showBackButton(true)
}
override fun preferenceClicks(): Observable<PreferenceView> = (0 until preferences.childCount)
.map { index -> preferences.getChildAt(index) }
override fun preferenceClicks(): Observable<PreferenceView> = (0 until binding.preferences.childCount)
.map { index -> binding.preferences.getChildAt(index) }
.mapNotNull { view -> view as? PreferenceView }
.map { preference -> preference.clicks().map { preference } }
.let { preferences -> Observable.merge(preferences) }
override fun aboutLongClicks(): Observable<*> = about.longClicks()
override fun aboutLongClicks(): Observable<*> = binding.about.longClicks()
override fun viewQksmsPlusClicks(): Observable<*> = viewQksmsPlusSubject
@@ -142,73 +143,73 @@ class SettingsController : QkController<SettingsView, SettingsState, SettingsPre
override fun messageLinkHandlingSelected(): Observable<Int> = messageLinkHandlingDialog.adapter.menuItemClicks
override fun render(state: SettingsState) {
themePreview.setBackgroundTint(state.theme)
night.summary = state.nightModeSummary
binding.theme.findViewById<View>(R.id.themePreview)?.setBackgroundTint(state.theme)
binding.night.summary = state.nightModeSummary
nightModeDialog.adapter.selectedItem = state.nightModeId
nightStart.setVisible(state.nightModeId == Preferences.NIGHT_MODE_AUTO)
nightStart.summary = state.nightStart
nightEnd.setVisible(state.nightModeId == Preferences.NIGHT_MODE_AUTO)
nightEnd.summary = state.nightEnd
binding.nightStart.setVisible(state.nightModeId == Preferences.NIGHT_MODE_AUTO)
binding.nightStart.summary = state.nightStart
binding.nightEnd.setVisible(state.nightModeId == Preferences.NIGHT_MODE_AUTO)
binding.nightEnd.summary = state.nightEnd
black.setVisible(state.nightModeId != Preferences.NIGHT_MODE_OFF)
black.checkbox.isChecked = state.black
binding.black.setVisible(state.nightModeId != Preferences.NIGHT_MODE_OFF)
binding.black.checkbox?.isChecked = state.black
autoEmoji.checkbox.isChecked = state.autoEmojiEnabled
binding.autoEmoji.checkbox?.isChecked = state.autoEmojiEnabled
delayed.summary = state.sendDelaySummary
binding.delayed.summary = state.sendDelaySummary
sendDelayDialog.adapter.selectedItem = state.sendDelayId
delivery.checkbox.isChecked = state.deliveryEnabled
binding.delivery.checkbox?.isChecked = state.deliveryEnabled
unreadAtTop.checkbox.isChecked = state.unreadAtTopEnabled
binding.unreadAtTop.checkbox?.isChecked = state.unreadAtTopEnabled
signature.summary = state.signature.takeIf { it.isNotBlank() }
binding.signature.summary = state.signature.takeIf { it.isNotBlank() }
?: context.getString(R.string.settings_signature_summary)
textSize.summary = state.textSizeSummary
binding.textSize.summary = state.textSizeSummary
textSizeDialog.adapter.selectedItem = state.textSizeId
autoColor.checkbox.isChecked = state.autoColor
binding.autoColor.checkbox?.isChecked = state.autoColor
systemFont.checkbox.isChecked = state.systemFontEnabled
binding.systemFont.checkbox?.isChecked = state.systemFontEnabled
showStt.checkbox.isChecked = state.showStt
binding.showStt.checkbox?.isChecked = state.showStt
unicode.checkbox.isChecked = state.stripUnicodeEnabled
mobileOnly.checkbox.isChecked = state.mobileOnly
binding.unicode.checkbox?.isChecked = state.stripUnicodeEnabled
binding.mobileOnly.checkbox?.isChecked = state.mobileOnly
longAsMms.checkbox.isChecked = state.longAsMms
binding.longAsMms.checkbox?.isChecked = state.longAsMms
mmsSize.summary = state.maxMmsSizeSummary
binding.mmsSize.summary = state.maxMmsSizeSummary
mmsSizeDialog.adapter.selectedItem = state.maxMmsSizeId
messsageLinkHandling.summary = state.messageLinkHandlingSummary
binding.messsageLinkHandling.summary = state.messageLinkHandlingSummary
messageLinkHandlingDialog.adapter.selectedItem = state.messageLinkHandlingId
disableScreenshots.checkbox.isChecked = state.disableScreenshotsEnabled
binding.disableScreenshots.checkbox?.isChecked = state.disableScreenshotsEnabled
when (state.syncProgress) {
is SyncRepository.SyncProgress.Idle -> syncingProgress.isVisible = false
is SyncRepository.SyncProgress.Idle -> binding.syncingProgress.isVisible = false
is SyncRepository.SyncProgress.Running -> {
syncingProgress.isVisible = true
syncingProgress.max = state.syncProgress.max
progressAnimator.apply { setIntValues(syncingProgress.progress, state.syncProgress.progress) }.start()
syncingProgress.isIndeterminate = state.syncProgress.indeterminate
binding.syncingProgress.isVisible = true
binding.syncingProgress.max = state.syncProgress.max
progressAnimator.apply { setIntValues(binding.syncingProgress.progress, state.syncProgress.progress) }.start()
binding.syncingProgress.isIndeterminate = state.syncProgress.indeterminate
}
is SyncRepository.SyncProgress.ParsingEmojis -> {
syncingProgress.isVisible = true
syncingProgress.max = state.syncProgress.max
progressAnimator.apply { setIntValues(syncingProgress.progress, state.syncProgress.progress) }.start()
syncingProgress.isIndeterminate = state.syncProgress.indeterminate
binding.syncingProgress.isVisible = true
binding.syncingProgress.max = state.syncProgress.max
progressAnimator.apply { setIntValues(binding.syncingProgress.progress, state.syncProgress.progress) }.start()
binding.syncingProgress.isIndeterminate = state.syncProgress.indeterminate
}
}
}
override fun showQksmsPlusSnackbar() {
view?.run {
Snackbar.make(contentView, R.string.toast_qksms_plus, Snackbar.LENGTH_LONG).run {
Snackbar.make(binding.contentView, R.string.toast_qksms_plus, Snackbar.LENGTH_LONG).run {
setAction(R.string.button_more) { viewQksmsPlusSubject.onNext(Unit) }
setActionTextColor(colors.theme().theme)
show()
@@ -259,4 +260,4 @@ class SettingsController : QkController<SettingsView, SettingsState, SettingsPre
.popChangeHandler(QkChangeHandler()))
}
}
}
@@ -18,28 +18,32 @@
*/
package dev.octoshrimpy.quik.feature.settings.about
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.jakewharton.rxbinding2.view.clicks
import dev.octoshrimpy.quik.BuildConfig
import dev.octoshrimpy.quik.R
import dev.octoshrimpy.quik.common.base.QkController
import dev.octoshrimpy.quik.common.widget.PreferenceView
import dev.octoshrimpy.quik.databinding.AboutControllerBinding
import dev.octoshrimpy.quik.injection.appComponent
import io.reactivex.Observable
import kotlinx.android.synthetic.main.about_controller.*
import javax.inject.Inject
class AboutController : QkController<AboutView, Unit, AboutPresenter>(), AboutView {
class AboutController : QkController<AboutControllerBinding, AboutView, Unit, AboutPresenter>(), AboutView {
override fun inflateBinding(inflater: LayoutInflater, container: ViewGroup): AboutControllerBinding =
AboutControllerBinding.inflate(inflater, container, false)
@Inject override lateinit var presenter: AboutPresenter
init {
appComponent.inject(this)
layoutRes = R.layout.about_controller
}
override fun onViewCreated() {
version.summary = BuildConfig.VERSION_NAME
binding.version.summary = BuildConfig.VERSION_NAME
}
override fun onAttach(view: View) {
@@ -49,8 +53,8 @@ class AboutController : QkController<AboutView, Unit, AboutPresenter>(), AboutVi
showBackButton(true)
}
override fun preferenceClicks(): Observable<PreferenceView> = (0 until preferences.childCount)
.map { index -> preferences.getChildAt(index) }
override fun preferenceClicks(): Observable<PreferenceView> = (0 until binding.preferences.childCount)
.map { index -> binding.preferences.getChildAt(index) }
.mapNotNull { view -> view as? PreferenceView }
.map { preference -> preference.clicks().map { preference } }
.let { preferences -> Observable.merge(preferences) }
@@ -18,7 +18,9 @@
*/
package dev.octoshrimpy.quik.feature.settings.swipe
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import com.jakewharton.rxbinding2.view.clicks
import com.uber.autodispose.android.lifecycle.scope
@@ -30,14 +32,17 @@ import dev.octoshrimpy.quik.common.util.Colors
import dev.octoshrimpy.quik.common.util.extensions.animateLayoutChanges
import dev.octoshrimpy.quik.common.util.extensions.setBackgroundTint
import dev.octoshrimpy.quik.common.util.extensions.setTint
import dev.octoshrimpy.quik.databinding.SwipeActionsControllerBinding
import dev.octoshrimpy.quik.injection.appComponent
import io.reactivex.Observable
import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.Subject
import kotlinx.android.synthetic.main.swipe_actions_controller.*
import javax.inject.Inject
class SwipeActionsController : QkController<SwipeActionsView, SwipeActionsState, SwipeActionsPresenter>(), SwipeActionsView {
class SwipeActionsController : QkController<SwipeActionsControllerBinding, SwipeActionsView, SwipeActionsState, SwipeActionsPresenter>(), SwipeActionsView {
override fun inflateBinding(inflater: LayoutInflater, container: ViewGroup): SwipeActionsControllerBinding =
SwipeActionsControllerBinding.inflate(inflater, container, false)
@Inject override lateinit var presenter: SwipeActionsPresenter
@Inject lateinit var actionsDialog: QkDialog
@@ -50,25 +55,24 @@ class SwipeActionsController : QkController<SwipeActionsView, SwipeActionsState,
init {
appComponent.inject(this)
layoutRes = R.layout.swipe_actions_controller
actionsDialog.adapter.setData(R.array.settings_swipe_actions)
}
override fun onViewCreated() {
colors.theme().let { theme ->
rightIcon.setBackgroundTint(theme.theme)
rightIcon.setTint(theme.textPrimary)
leftIcon.setBackgroundTint(theme.theme)
leftIcon.setTint(theme.textPrimary)
binding.rightIcon.setBackgroundTint(theme.theme)
binding.rightIcon.setTint(theme.textPrimary)
binding.leftIcon.setBackgroundTint(theme.theme)
binding.leftIcon.setTint(theme.textPrimary)
}
right.postDelayed({ right?.animateLayoutChanges = true }, 100)
left.postDelayed({ left?.animateLayoutChanges = true }, 100)
binding.right.postDelayed({ binding.right.animateLayoutChanges = true }, 100)
binding.left.postDelayed({ binding.left.animateLayoutChanges = true }, 100)
Observable.merge(
right.clicks().map { SwipeActionsView.Action.RIGHT },
left.clicks().map { SwipeActionsView.Action.LEFT })
binding.right.clicks().map { SwipeActionsView.Action.RIGHT },
binding.left.clicks().map { SwipeActionsView.Action.LEFT })
.autoDisposable(scope())
.subscribe(actionClicks)
}
@@ -90,13 +94,13 @@ class SwipeActionsController : QkController<SwipeActionsView, SwipeActionsState,
}
override fun render(state: SwipeActionsState) {
rightIcon.isVisible = state.rightIcon != 0
rightIcon.setImageResource(state.rightIcon)
rightLabel.text = state.rightLabel
binding.rightIcon.isVisible = state.rightIcon != 0
binding.rightIcon.setImageResource(state.rightIcon)
binding.rightLabel.text = state.rightLabel
leftIcon.isVisible = state.leftIcon != 0
leftIcon.setImageResource(state.leftIcon)
leftLabel.text = state.leftLabel
binding.leftIcon.isVisible = state.leftIcon != 0
binding.leftIcon.setImageResource(state.leftIcon)
binding.leftLabel.text = state.leftLabel
}
}
@@ -22,16 +22,15 @@ import android.content.Context
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import androidx.constraintlayout.widget.ConstraintLayout
import dev.octoshrimpy.quik.R
import dev.octoshrimpy.quik.common.util.extensions.setBackgroundTint
import dev.octoshrimpy.quik.common.util.extensions.setTint
import dev.octoshrimpy.quik.common.util.extensions.within
import dev.octoshrimpy.quik.databinding.HsvPickerViewBinding
import io.reactivex.subjects.BehaviorSubject
import io.reactivex.subjects.Subject
import kotlinx.android.synthetic.main.hsv_picker_view.view.*
class HSVPickerView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
@@ -51,13 +50,15 @@ class HSVPickerView @JvmOverloads constructor(
updateHue()
}
private val binding: HsvPickerViewBinding
init {
View.inflate(context, R.layout.hsv_picker_view, this)
binding = HsvPickerViewBinding.inflate(LayoutInflater.from(context), this)
var swatchX = 0f
var swatchY = 0f
saturation.setOnTouchListener { _, event ->
binding.saturation.setOnTouchListener { _, event ->
setupBounds()
when (event.action) {
MotionEvent.ACTION_DOWN -> {
@@ -68,8 +69,8 @@ class HSVPickerView @JvmOverloads constructor(
MotionEvent.ACTION_MOVE -> {
// Calculate the new x/y position
swatch.x = (event.rawX + swatchX + min).within(min, max)
swatch.y = (event.rawY + swatchY + min).within(min, max)
binding.swatch.x = (event.rawX + swatchX + min).within(min, max)
binding.swatch.y = (event.rawY + swatchY + min).within(min, max)
updateSelectedColor()
}
@@ -85,7 +86,7 @@ class HSVPickerView @JvmOverloads constructor(
var hueThumbX = 0f
hueGroup.setOnTouchListener { _, event ->
binding.hueGroup.setOnTouchListener { _, event ->
setupBounds()
when (event.action) {
MotionEvent.ACTION_DOWN -> {
@@ -96,8 +97,8 @@ class HSVPickerView @JvmOverloads constructor(
MotionEvent.ACTION_MOVE -> {
val x = (event.rawX + hueThumbX + min).within(min, max)
hueThumb.x = x
hue = (hueThumb.x - min) / (max - min) * 360
binding.hueThumb.x = x
hue = (binding.hueThumb.x - min) / (max - min) * 360
updateSelectedColor()
}
@@ -111,14 +112,14 @@ class HSVPickerView @JvmOverloads constructor(
true
}
hueTrack.clipToOutline = true
hueTrack.setImageDrawable(GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, hues))
binding.hueTrack.clipToOutline = true
binding.hueTrack.setImageDrawable(GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, hues))
}
private fun setupBounds() {
if (min == 0f || max == 0f) {
min = saturation.x - swatch.width / 2
max = min + saturation.width
min = binding.saturation.x - binding.swatch.width / 2
max = min + binding.saturation.width
}
}
@@ -126,10 +127,10 @@ class HSVPickerView @JvmOverloads constructor(
setupBounds()
val range = max - min
val hsv = floatArrayOf(hue, (swatch.x - min) / range, 1 - (swatch.y - min) / range)
val hsv = floatArrayOf(hue, (binding.swatch.x - min) / range, 1 - (binding.swatch.y - min) / range)
val color = Color.HSVToColor(hsv)
swatch.setTint(color)
binding.swatch.setTint(color)
selectedColor.onNext(color)
}
@@ -145,9 +146,9 @@ class HSVPickerView @JvmOverloads constructor(
setupBounds()
val range = max - min
hueThumb.x = range * hsv[0] / 360 + min
swatch.x = range * hsv[1] + min
swatch.y = range * (1 - hsv[2]) + min
binding.hueThumb.x = range * hsv[0] / 360 + min
binding.swatch.x = range * hsv[1] + min
binding.swatch.y = range * (1 - hsv[2]) + min
updateSelectedColor()
}
@@ -156,7 +157,7 @@ class HSVPickerView @JvmOverloads constructor(
private fun updateHue() {
val hsv = floatArrayOf(hue, 1f, 1f)
val tint = Color.HSVToColor(hsv)
saturation.setBackgroundTint(tint)
binding.saturation.setBackgroundTint(tint)
}
}
@@ -25,25 +25,23 @@ import android.view.ViewGroup
import com.google.android.flexbox.FlexDirection
import com.google.android.flexbox.FlexWrap
import com.google.android.flexbox.FlexboxLayout
import dev.octoshrimpy.quik.R
import dev.octoshrimpy.quik.common.base.QkAdapter
import dev.octoshrimpy.quik.common.base.QkViewHolder
import dev.octoshrimpy.quik.common.base.QkBindingViewHolder
import dev.octoshrimpy.quik.common.util.Colors
import dev.octoshrimpy.quik.common.util.extensions.dpToPx
import dev.octoshrimpy.quik.common.util.extensions.setBackgroundTint
import dev.octoshrimpy.quik.common.util.extensions.setTint
import dev.octoshrimpy.quik.common.util.extensions.setVisible
import dev.octoshrimpy.quik.databinding.ThemeListItemBinding
import dev.octoshrimpy.quik.databinding.ThemePaletteListItemBinding
import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.Subject
import kotlinx.android.synthetic.main.theme_list_item.view.*
import kotlinx.android.synthetic.main.theme_palette_list_item.*
import kotlinx.android.synthetic.main.theme_palette_list_item.view.*
import javax.inject.Inject
class ThemeAdapter @Inject constructor(
private val context: Context,
private val colors: Colors
) : QkAdapter<List<Int>, QkViewHolder>() {
) : QkAdapter<List<Int>, QkBindingViewHolder<ThemePaletteListItemBinding>>() {
val colorSelected: Subject<Int> = PublishSubject.create()
@@ -61,16 +59,17 @@ class ThemeAdapter @Inject constructor(
private var iconTint = 0
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.theme_palette_list_item, parent, false)
view.palette.flexWrap = FlexWrap.WRAP
view.palette.flexDirection = FlexDirection.ROW
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkBindingViewHolder<ThemePaletteListItemBinding> {
val binding = ThemePaletteListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
binding.palette.flexWrap = FlexWrap.WRAP
binding.palette.flexDirection = FlexDirection.ROW
return QkViewHolder(view)
return QkBindingViewHolder(binding)
}
override fun onBindViewHolder(holder: QkViewHolder, position: Int) {
override fun onBindViewHolder(holder: QkBindingViewHolder<ThemePaletteListItemBinding>, position: Int) {
val palette = getItem(position)
val binding = holder.binding
val screenWidth = Resources.getSystem().displayMetrics.widthPixels
val minPadding = (16 * 6).dpToPx(context)
@@ -81,33 +80,34 @@ class ThemeAdapter @Inject constructor(
}
val swatchPadding = (screenWidth - size * 5) / 12
holder.palette.removeAllViews()
holder.palette.setPadding(swatchPadding, swatchPadding, swatchPadding, swatchPadding)
binding.palette.removeAllViews()
binding.palette.setPadding(swatchPadding, swatchPadding, swatchPadding, swatchPadding)
(palette.subList(0, 5) + palette.subList(5, 10).reversed())
.mapIndexed { index, color ->
LayoutInflater.from(context).inflate(R.layout.theme_list_item, holder.palette, false).apply {
val itemBinding = ThemeListItemBinding.inflate(LayoutInflater.from(context), binding.palette, false)
// Send clicks to the selected subject
setOnClickListener { colorSelected.onNext(color) }
// Send clicks to the selected subject
itemBinding.root.setOnClickListener { colorSelected.onNext(color) }
// Apply the color to the view
theme.setBackgroundTint(color)
// Apply the color to the view
itemBinding.theme.setBackgroundTint(color)
// Control the check visibility and tint
check.setVisible(color == selectedColor)
check.setTint(iconTint)
// Control the check visibility and tint
itemBinding.check.setVisible(color == selectedColor)
itemBinding.check.setTint(iconTint)
// Update the size so that the spacing is perfectly even
layoutParams = (layoutParams as FlexboxLayout.LayoutParams).apply {
height = size
width = size
isWrapBefore = index % 5 == 0
setMargins(swatchPadding, swatchPadding, swatchPadding, swatchPadding)
}
// Update the size so that the spacing is perfectly even
itemBinding.root.layoutParams = (itemBinding.root.layoutParams as FlexboxLayout.LayoutParams).apply {
height = size
width = size
isWrapBefore = index % 5 == 0
setMargins(swatchPadding, swatchPadding, swatchPadding, swatchPadding)
}
itemBinding.root
}
.forEach { theme -> holder.palette.addView(theme) }
.forEach { theme -> binding.palette.addView(theme) }
}
}
@@ -19,7 +19,9 @@
package dev.octoshrimpy.quik.feature.themepicker
import android.animation.ObjectAnimator
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.snackbar.Snackbar
import com.jakewharton.rxbinding2.view.clicks
@@ -29,18 +31,20 @@ import dev.octoshrimpy.quik.common.util.Colors
import dev.octoshrimpy.quik.common.util.extensions.dpToPx
import dev.octoshrimpy.quik.common.util.extensions.setBackgroundTint
import dev.octoshrimpy.quik.common.util.extensions.setVisible
import dev.octoshrimpy.quik.databinding.ThemePickerControllerBinding
import dev.octoshrimpy.quik.feature.themepicker.injection.ThemePickerModule
import dev.octoshrimpy.quik.injection.appComponent
import io.reactivex.Observable
import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.Subject
import kotlinx.android.synthetic.main.theme_picker_controller.*
import kotlinx.android.synthetic.main.theme_picker_hsv.*
import javax.inject.Inject
class ThemePickerController(
val recipientId: Long = 0L
) : QkController<ThemePickerView, ThemePickerState, ThemePickerPresenter>(), ThemePickerView {
) : QkController<ThemePickerControllerBinding, ThemePickerView, ThemePickerState, ThemePickerPresenter>(), ThemePickerView {
override fun inflateBinding(inflater: LayoutInflater, container: ViewGroup): ThemePickerControllerBinding =
ThemePickerControllerBinding.inflate(inflater, container, false)
@Inject override lateinit var presenter: ThemePickerPresenter
@@ -56,19 +60,17 @@ class ThemePickerController(
.themePickerModule(ThemePickerModule(this))
.build()
.inject(this)
layoutRes = R.layout.theme_picker_controller
}
override fun onViewCreated() {
pager.offscreenPageLimit = 1
pager.adapter = themePagerAdapter
tabs.pager = pager
binding.pager.offscreenPageLimit = 1
binding.pager.adapter = themePagerAdapter
binding.tabs.pager = binding.pager
themeAdapter.data = colors.materialColors
materialColors.layoutManager = LinearLayoutManager(activity)
materialColors.adapter = themeAdapter
binding.materialColors.layoutManager = LinearLayoutManager(activity)
binding.materialColors.adapter = themeAdapter
}
override fun onAttach(view: View) {
@@ -91,7 +93,7 @@ class ThemePickerController(
}
override fun showQksmsPlusSnackbar() {
Snackbar.make(contentView, R.string.toast_qksms_plus, Snackbar.LENGTH_LONG).run {
Snackbar.make(binding.contentView, R.string.toast_qksms_plus, Snackbar.LENGTH_LONG).run {
setAction(R.string.button_more) { viewQksmsPlusSubject.onNext(Unit) }
setActionTextColor(colors.theme().theme)
show()
@@ -100,27 +102,27 @@ class ThemePickerController(
override fun themeSelected(): Observable<Int> = themeAdapter.colorSelected
override fun hsvThemeSelected(): Observable<Int> = picker.selectedColor
override fun hsvThemeSelected(): Observable<Int> = binding.hsvPicker.picker.selectedColor
override fun clearHsvThemeClicks(): Observable<*> = clear.clicks()
override fun clearHsvThemeClicks(): Observable<*> = binding.hsvPicker.clear.clicks()
override fun applyHsvThemeClicks(): Observable<*> = apply.clicks()
override fun applyHsvThemeClicks(): Observable<*> = binding.hsvPicker.apply.clicks()
override fun viewQksmsPlusClicks(): Observable<*> = viewQksmsPlusSubject
override fun render(state: ThemePickerState) {
tabs.setRecipientId(state.recipientId)
binding.tabs.setRecipientId(state.recipientId)
hex.setText(Integer.toHexString(state.newColor).takeLast(6))
binding.hsvPicker.hex.setText(Integer.toHexString(state.newColor).takeLast(6))
applyGroup.setVisible(state.applyThemeVisible)
apply.setBackgroundTint(state.newColor)
apply.setTextColor(state.newTextColor)
binding.hsvPicker.applyGroup.setVisible(state.applyThemeVisible)
binding.hsvPicker.apply.setBackgroundTint(state.newColor)
binding.hsvPicker.apply.setTextColor(state.newTextColor)
}
override fun setCurrentTheme(color: Int) {
picker.setColor(color)
binding.hsvPicker.picker.setColor(color)
themeAdapter.selectedColor = color
}
}
}