Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ea2edbb8 authored by Moez Bhatti's avatar Moez Bhatti
Browse files

Rewrite ConversationInfo screen

Fixes #1413
parent f8a00d73
Loading
Loading
Loading
Loading
+153 −0
Original line number Diff line number Diff line
package com.moez.QKSMS.feature.conversationinfo

import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.view.isVisible
import com.jakewharton.rxbinding2.view.clicks
import com.moez.QKSMS.R
import com.moez.QKSMS.common.base.QkAdapter
import com.moez.QKSMS.common.base.QkViewHolder
import com.moez.QKSMS.common.util.Colors
import com.moez.QKSMS.common.util.extensions.setTint
import com.moez.QKSMS.common.util.extensions.setVisible
import com.moez.QKSMS.extensions.isVideo
import com.moez.QKSMS.feature.conversationinfo.ConversationInfoItem.*
import com.moez.QKSMS.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(
    private val context: Context,
    private val colors: Colors
) : QkAdapter<ConversationInfoItem>() {

    val recipientClicks: Subject<Long> = PublishSubject.create()
    val recipientLongClicks: Subject<Long> = PublishSubject.create()
    val themeClicks: Subject<Long> = PublishSubject.create()
    val nameClicks: Subject<Unit> = PublishSubject.create()
    val notificationClicks: Subject<Unit> = PublishSubject.create()
    val archiveClicks: Subject<Unit> = PublishSubject.create()
    val blockClicks: Subject<Unit> = PublishSubject.create()
    val deleteClicks: Subject<Unit> = PublishSubject.create()
    val mediaClicks: Subject<Long> = PublishSubject.create()

    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)
                }

                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)
                }
            }

            1 -> QkViewHolder(inflater.inflate(R.layout.conversation_info_settings, parent, false)).apply {
                groupName.clicks().subscribe(nameClicks)
                notifications.clicks().subscribe(notificationClicks)
                archive.clicks().subscribe(archiveClicks)
                block.clicks().subscribe(blockClicks)
                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)
                }
            }

            else -> throw IllegalStateException()
        }
    }

    override fun onBindViewHolder(holder: QkViewHolder, position: Int) {
        when (val item = getItem(position)) {
            is ConversationInfoRecipient -> {
                val recipient = item.value
                holder.avatar.setRecipient(recipient)

                holder.name.text = recipient.contact?.name ?: recipient.address

                holder.address.text = recipient.address
                holder.address.setVisible(recipient.contact != null)

                holder.add.setVisible(recipient.contact == null)

                val theme = colors.theme(recipient)
                holder.theme.setTint(theme.theme)
            }

            is ConversationInfoSettings -> {
                holder.groupName.isVisible = item.recipients.size > 1
                holder.groupName.summary = item.name

                holder.notifications.isEnabled = !item.blocked

                holder.archive.isEnabled = !item.blocked
                holder.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) {
                    true -> R.string.info_unblock
                    false -> R.string.info_block
                })
            }

            is ConversationInfoMedia -> {
                val part = item.value

                GlideApp.with(context)
                        .load(part.getUri())
                        .fitCenter()
                        .into(holder.thumbnail)

                holder.video.isVisible = part.isVideo()
            }
        }
    }

    override fun getItemViewType(position: Int): Int {
        return when (data[position]) {
            is ConversationInfoRecipient -> 0
            is ConversationInfoSettings -> 1
            is ConversationInfoMedia -> 2
        }
    }

    override fun areItemsTheSame(old: ConversationInfoItem, new: ConversationInfoItem): Boolean {
        return when {
            old is ConversationInfoRecipient && new is ConversationInfoRecipient -> {
               old.value.id == new.value.id
            }

            old is ConversationInfoSettings && new is ConversationInfoSettings -> {
                true
            }

            old is ConversationInfoMedia && new is ConversationInfoMedia -> {
                old.value.id == new.value.id
            }

            else -> false
        }
    }

}
+23 −52
Original line number Diff line number Diff line
@@ -20,15 +20,13 @@ package com.moez.QKSMS.feature.conversationinfo

import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.GridLayoutManager
import com.bluelinelabs.conductor.RouterTransaction
import com.jakewharton.rxbinding2.view.clicks
import com.moez.QKSMS.R
import com.moez.QKSMS.common.Navigator
import com.moez.QKSMS.common.QkChangeHandler
import com.moez.QKSMS.common.base.QkController
import com.moez.QKSMS.common.util.extensions.animateLayoutChanges
import com.moez.QKSMS.common.util.extensions.scrapViews
import com.moez.QKSMS.common.util.extensions.setVisible
import com.moez.QKSMS.common.widget.FieldDialog
import com.moez.QKSMS.feature.blocking.BlockingDialog
import com.moez.QKSMS.feature.conversationinfo.injection.ConversationInfoModule
@@ -49,9 +47,7 @@ class ConversationInfoController(
    @Inject override lateinit var presenter: ConversationInfoPresenter
    @Inject lateinit var blockingDialog: BlockingDialog
    @Inject lateinit var navigator: Navigator
    @Inject lateinit var recipientAdapter: ConversationRecipientAdapter
    @Inject lateinit var mediaAdapter: ConversationMediaAdapter
    @Inject lateinit var itemDecoration: GridSpacingItemDecoration
    @Inject lateinit var adapter: ConversationInfoAdapter

    private val nameDialog: FieldDialog by lazy {
        FieldDialog(activity!!, activity!!.getString(R.string.info_name), nameChangeSubject::onNext)
@@ -71,16 +67,17 @@ class ConversationInfoController(
    }

    override fun onViewCreated() {
        items.postDelayed({ items?.animateLayoutChanges = true }, 100)

        recipients.adapter = recipientAdapter

        media.adapter = mediaAdapter
        media.addItemDecoration(itemDecoration)
        recyclerView.adapter = adapter
        recyclerView.addItemDecoration(GridSpacingItemDecoration(adapter, activity!!))
        recyclerView.layoutManager = GridLayoutManager(activity, 3).apply {
            spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
                override fun getSpanSize(position: Int): Int = if (adapter.getItemViewType(position) == 2) 1 else 3
            }
        }

        themedActivity?.theme
                ?.autoDisposable(scope())
                ?.subscribe { recipients.scrapViews() }
                ?.subscribe { recyclerView.scrapViews() }
    }

    override fun onAttach(view: View) {
@@ -90,53 +87,27 @@ class ConversationInfoController(
        showBackButton(true)
    }

    override fun recipientClicks(): Observable<Long> = recipientAdapter.recipientClicks

    override fun recipientLongClicks(): Observable<Long> = recipientAdapter.recipientLongClicks

    override fun themeClicks(): Observable<Long> = recipientAdapter.themeClicks

    override fun nameClicks(): Observable<*> = name.clicks()

    override fun nameChanges(): Observable<String> = nameChangeSubject

    override fun notificationClicks(): Observable<*> = notifications.clicks()

    override fun archiveClicks(): Observable<*> = archive.clicks()

    override fun blockClicks(): Observable<*> = block.clicks()

    override fun deleteClicks(): Observable<*> = delete.clicks()

    override fun confirmDelete(): Observable<*> = confirmDeleteSubject

    override fun render(state: ConversationInfoState) {
        if (state.hasError) {
            activity?.finish()
            return
        }

        recipientAdapter.updateData(state.recipients)

        name.setVisible(state.recipients?.size ?: 0 >= 2)
        name.summary = state.name

        notifications.isEnabled = !state.blocked

        archive.isEnabled = !state.blocked
        archive.title = activity?.getString(when (state.archived) {
            true -> R.string.info_unarchive
            false -> R.string.info_archive
        })

        block.title = activity?.getString(when (state.blocked) {
            true -> R.string.info_unblock
            false -> R.string.info_block
        })

        mediaAdapter.updateData(state.media)
        adapter.data = state.data
    }

    override fun recipientClicks(): Observable<Long> = adapter.recipientClicks
    override fun recipientLongClicks(): Observable<Long> = adapter.recipientLongClicks
    override fun themeClicks(): Observable<Long> = adapter.themeClicks
    override fun nameClicks(): Observable<*> = adapter.nameClicks
    override fun nameChanges(): Observable<String> = nameChangeSubject
    override fun notificationClicks(): Observable<*> = adapter.notificationClicks
    override fun archiveClicks(): Observable<*> = adapter.archiveClicks
    override fun blockClicks(): Observable<*> = adapter.blockClicks
    override fun deleteClicks(): Observable<*> = adapter.deleteClicks
    override fun confirmDelete(): Observable<*> = confirmDeleteSubject
    override fun mediaClicks(): Observable<Long> = adapter.mediaClicks

    override fun showNameDialog(name: String) = nameDialog.setText(name).show()

    override fun showThemePicker(recipientId: Long) {
+20 −0
Original line number Diff line number Diff line
package com.moez.QKSMS.feature.conversationinfo

import com.moez.QKSMS.model.MmsPart
import com.moez.QKSMS.model.Recipient
import io.realm.RealmList

sealed class ConversationInfoItem {

    data class ConversationInfoRecipient(val value: Recipient) : ConversationInfoItem()

    data class ConversationInfoSettings(
        val name: String,
        val recipients: RealmList<Recipient>,
        val archived: Boolean,
        val blocked: Boolean
    ) : ConversationInfoItem()

    data class ConversationInfoMedia(val value: MmsPart) : ConversationInfoItem()

}
+27 −23
Original line number Diff line number Diff line
@@ -27,6 +27,8 @@ import com.moez.QKSMS.common.util.ClipboardUtils
import com.moez.QKSMS.common.util.extensions.makeToast
import com.moez.QKSMS.extensions.asObservable
import com.moez.QKSMS.extensions.mapNotNull
import com.moez.QKSMS.feature.conversationinfo.ConversationInfoItem.ConversationInfoMedia
import com.moez.QKSMS.feature.conversationinfo.ConversationInfoItem.ConversationInfoRecipient
import com.moez.QKSMS.interactor.DeleteConversations
import com.moez.QKSMS.interactor.MarkArchived
import com.moez.QKSMS.interactor.MarkUnarchived
@@ -37,6 +39,7 @@ import com.moez.QKSMS.repository.MessageRepository
import com.uber.autodispose.android.lifecycle.scope
import com.uber.autodispose.autoDisposable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.rxkotlin.Observables
import io.reactivex.rxkotlin.plusAssign
import io.reactivex.rxkotlin.withLatestFrom
import io.reactivex.subjects.BehaviorSubject
@@ -55,7 +58,7 @@ class ConversationInfoPresenter @Inject constructor(
    private val navigator: Navigator,
    private val permissionManager: PermissionManager
) : QkPresenter<ConversationInfoView, ConversationInfoState>(
        ConversationInfoState(threadId = threadId, media = messageRepo.getPartsForConversation(threadId))
        ConversationInfoState(threadId = threadId)
) {

    private val conversation: Subject<Conversation> = BehaviorSubject.create()
@@ -77,29 +80,25 @@ class ConversationInfoPresenter @Inject constructor(
        disposables += markUnarchived
        disposables += deleteConversations

        // Update the recipients whenever they change
        disposables += conversation
                .map { conversation -> conversation.recipients }
                .distinctUntilChanged()
                .subscribe { recipients -> newState { copy(recipients = recipients) } }
        val partsObservable = messageRepo.getPartsForConversation(threadId)
                .asObservable()
                .filter { parts -> parts.isLoaded && parts.isValid}

        // Update conversation title whenever it changes
        disposables += conversation
                .map { conversation -> conversation.name }
                .distinctUntilChanged()
                .subscribe { name -> newState { copy(name = name) } }

        // Update the view's archived state whenever it changes
        disposables += conversation
                .map { conversation -> conversation.archived }
                .distinctUntilChanged()
                .subscribe { archived -> newState { copy(archived = archived) } }

        // Update the view's blocked state whenever it changes
        disposables += conversation
                .map { conversation -> conversation.blocked }
                .distinctUntilChanged()
                .subscribe { blocked -> newState { copy(blocked = blocked) } }
        disposables += Observables
                .combineLatest(conversation, partsObservable) { conversation, parts ->
                    val data = mutableListOf<ConversationInfoItem>()

                    data += conversation.recipients.map(::ConversationInfoRecipient)
                    data += ConversationInfoItem.ConversationInfoSettings(
                            name = conversation.name,
                            recipients = conversation.recipients,
                            archived = conversation.archived,
                            blocked = conversation.blocked)
                    data += parts.map(::ConversationInfoMedia)

                    newState { copy(data = data) }
                }
                .subscribe()
    }

    override fun bindIntents(view: ConversationInfoView) {
@@ -180,6 +179,11 @@ class ConversationInfoPresenter @Inject constructor(
                .withLatestFrom(conversation) { _, conversation -> conversation }
                .autoDisposable(view.scope())
                .subscribe { conversation -> deleteConversations.execute(listOf(conversation.id)) }

        // Media
        view.mediaClicks()
                .autoDisposable(view.scope())
                .subscribe(navigator::showMedia)
    }

}
 No newline at end of file
+2 −11
Original line number Diff line number Diff line
@@ -18,17 +18,8 @@
 */
package com.moez.QKSMS.feature.conversationinfo

import com.moez.QKSMS.model.MmsPart
import com.moez.QKSMS.model.Recipient
import io.realm.RealmList
import io.realm.RealmResults

data class ConversationInfoState(
    val name: String = "",
    val recipients: RealmList<Recipient>? = null,
    val threadId: Long = 0,
    val archived: Boolean = false,
    val blocked: Boolean = false,
    val media: RealmResults<MmsPart>? = null,
    val data: List<ConversationInfoItem> = listOf(),
    val hasError: Boolean = false
)
Loading