diff --git a/data/src/main/java/com/moez/QKSMS/filter/ContactFilter.kt b/data/src/main/java/com/moez/QKSMS/filter/ContactFilter.kt index 8e77da1a..6010c7b3 100644 --- a/data/src/main/java/com/moez/QKSMS/filter/ContactFilter.kt +++ b/data/src/main/java/com/moez/QKSMS/filter/ContactFilter.kt @@ -29,4 +29,4 @@ class ContactFilter @Inject constructor(private val phoneNumberFilter: PhoneNumb item.numbers.map { it.address }.any { address -> phoneNumberFilter.filter(address, query) } // Number } -} \ No newline at end of file +} diff --git a/data/src/main/java/com/moez/QKSMS/filter/ContactGroupFilter.kt b/data/src/main/java/com/moez/QKSMS/filter/ContactGroupFilter.kt new file mode 100644 index 00000000..903e16fd --- /dev/null +++ b/data/src/main/java/com/moez/QKSMS/filter/ContactGroupFilter.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2019 Moez Bhatti + * + * This file is part of QKSMS. + * + * QKSMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QKSMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QKSMS. If not, see . + */ +package com.moez.QKSMS.filter + +import com.moez.QKSMS.extensions.removeAccents +import com.moez.QKSMS.model.ContactGroup +import javax.inject.Inject + +class ContactGroupFilter @Inject constructor(private val contactFilter: ContactFilter) : Filter() { + + override fun filter(item: ContactGroup, query: CharSequence): Boolean { + return item.title.removeAccents().contains(query, true) || // Name + item.contacts.any { contact -> contactFilter.filter(contact, query) } // Contacts + } + +} diff --git a/data/src/main/java/com/moez/QKSMS/repository/ContactRepositoryImpl.kt b/data/src/main/java/com/moez/QKSMS/repository/ContactRepositoryImpl.kt index 45248ff9..842a423c 100644 --- a/data/src/main/java/com/moez/QKSMS/repository/ContactRepositoryImpl.kt +++ b/data/src/main/java/com/moez/QKSMS/repository/ContactRepositoryImpl.kt @@ -25,10 +25,13 @@ import android.provider.ContactsContract import android.provider.ContactsContract.CommonDataKinds.Email import android.provider.ContactsContract.CommonDataKinds.Phone import com.moez.QKSMS.extensions.asFlowable +import com.moez.QKSMS.extensions.asObservable import com.moez.QKSMS.extensions.mapNotNull import com.moez.QKSMS.model.Contact +import com.moez.QKSMS.model.ContactGroup import com.moez.QKSMS.util.Preferences import io.reactivex.Flowable +import io.reactivex.Observable import io.reactivex.Single import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers @@ -70,55 +73,68 @@ class ContactRepositoryImpl @Inject constructor( .findAll() } - override fun getUnmanagedContacts(): Flowable> { + override fun getUnmanagedContacts(starred: Boolean): Observable> { val realm = Realm.getDefaultInstance() - val mobileLabel by lazy { - Phone.getTypeLabel(context.resources, Phone.TYPE_MOBILE, "Mobile").toString() + val mobileOnly = prefs.mobileOnly.get() + val mobileLabel by lazy { Phone.getTypeLabel(context.resources, Phone.TYPE_MOBILE, "Mobile").toString() } + + var query = realm.where(Contact::class.java) + + if (mobileOnly) { + query = query.contains("numbers.type", mobileLabel) } - val contactsFlowable = when (prefs.mobileOnly.get()) { - true -> realm.where(Contact::class.java) - .contains("numbers.type", mobileLabel) - .findAllAsync() - .asFlowable() - .filter { it.isLoaded } - .filter { it.isValid } - .map { realm.copyFromRealm(it) } - .subscribeOn(AndroidSchedulers.mainThread()) - .observeOn(Schedulers.io()) - .map { contacts -> + if (starred) { + query = query.equalTo("starred", true) + } + + return query + .findAllAsync() + .asObservable() + .filter { it.isLoaded } + .filter { it.isValid } + .map { realm.copyFromRealm(it) } + .subscribeOn(AndroidSchedulers.mainThread()) + .observeOn(Schedulers.io()) + .map { contacts -> + if (mobileOnly) { contacts.map { contact -> val filteredNumbers = contact.numbers.filter { number -> number.type == mobileLabel } contact.numbers.clear() contact.numbers.addAll(filteredNumbers) contact } + } else { + contacts } - - false -> realm.where(Contact::class.java) - .findAllAsync() - .asFlowable() - .filter { it.isLoaded } - .filter { it.isValid } - .map { realm.copyFromRealm(it) } - .subscribeOn(AndroidSchedulers.mainThread()) - .observeOn(Schedulers.io()) - } - - return contactsFlowable.map { contacts -> - contacts.sortedWith(Comparator { c1, c2 -> - val initial = c1.name.firstOrNull() - val other = c2.name.firstOrNull() - if (initial?.isLetter() == true && other?.isLetter() != true) { - -1 - } else if (initial?.isLetter() != true && other?.isLetter() == true) { - 1 - } else { - c1.name.compareTo(c2.name, ignoreCase = true) } - }) - } + .map { contacts -> + contacts.sortedWith(Comparator { c1, c2 -> + val initial = c1.name.firstOrNull() + val other = c2.name.firstOrNull() + if (initial?.isLetter() == true && other?.isLetter() != true) { + -1 + } else if (initial?.isLetter() != true && other?.isLetter() == true) { + 1 + } else { + c1.name.compareTo(c2.name, ignoreCase = true) + } + }) + } + } + + override fun getUnmanagedContactGroups(): Observable> { + val realm = Realm.getDefaultInstance() + return realm.where(ContactGroup::class.java) + .isNotEmpty("contacts") + .findAllAsync() + .asObservable() + .filter { it.isLoaded } + .filter { it.isValid } + .map { realm.copyFromRealm(it) } + .subscribeOn(AndroidSchedulers.mainThread()) + .observeOn(Schedulers.io()) } } diff --git a/data/src/main/java/com/moez/QKSMS/repository/ConversationRepositoryImpl.kt b/data/src/main/java/com/moez/QKSMS/repository/ConversationRepositoryImpl.kt index 0c05e222..988b5ce4 100644 --- a/data/src/main/java/com/moez/QKSMS/repository/ConversationRepositoryImpl.kt +++ b/data/src/main/java/com/moez/QKSMS/repository/ConversationRepositoryImpl.kt @@ -23,6 +23,7 @@ import android.content.Context import android.provider.Telephony import com.moez.QKSMS.compat.TelephonyCompat import com.moez.QKSMS.extensions.anyOf +import com.moez.QKSMS.extensions.asObservable import com.moez.QKSMS.extensions.map import com.moez.QKSMS.extensions.removeAccents import com.moez.QKSMS.filter.ConversationFilter @@ -35,6 +36,9 @@ import com.moez.QKSMS.model.Recipient import com.moez.QKSMS.model.SearchResult import com.moez.QKSMS.util.PhoneNumberUtils import com.moez.QKSMS.util.tryOrNull +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers import io.realm.Case import io.realm.Realm import io.realm.RealmResults @@ -183,6 +187,25 @@ class ConversationRepositoryImpl @Inject constructor( .findAll() } + override fun getUnmanagedConversations(): Observable> { + val realm = Realm.getDefaultInstance() + return realm.where(Conversation::class.java) + .sort("date", Sort.DESCENDING) + .notEqualTo("id", 0L) + .greaterThan("count", 0) + .equalTo("archived", false) + .equalTo("blocked", false) + .isNotEmpty("recipients") + .limit(5) + .findAllAsync() + .asObservable() + .filter { it.isLoaded } + .filter { it.isValid } + .map { realm.copyFromRealm(it) } + .subscribeOn(AndroidSchedulers.mainThread()) + .observeOn(Schedulers.io()) + } + override fun getRecipient(recipientId: Long): Recipient? { return Realm.getDefaultInstance() .where(Recipient::class.java) diff --git a/domain/src/main/java/com/moez/QKSMS/repository/ContactRepository.kt b/domain/src/main/java/com/moez/QKSMS/repository/ContactRepository.kt index 03733626..0bc8471b 100644 --- a/domain/src/main/java/com/moez/QKSMS/repository/ContactRepository.kt +++ b/domain/src/main/java/com/moez/QKSMS/repository/ContactRepository.kt @@ -20,7 +20,8 @@ package com.moez.QKSMS.repository import android.net.Uri import com.moez.QKSMS.model.Contact -import io.reactivex.Flowable +import com.moez.QKSMS.model.ContactGroup +import io.reactivex.Observable import io.reactivex.Single import io.realm.RealmResults @@ -30,6 +31,8 @@ interface ContactRepository { fun getContacts(): RealmResults - fun getUnmanagedContacts(): Flowable> + fun getUnmanagedContacts(starred: Boolean = false): Observable> + + fun getUnmanagedContactGroups(): Observable> } \ No newline at end of file diff --git a/domain/src/main/java/com/moez/QKSMS/repository/ConversationRepository.kt b/domain/src/main/java/com/moez/QKSMS/repository/ConversationRepository.kt index 7c517c10..a6f1a071 100644 --- a/domain/src/main/java/com/moez/QKSMS/repository/ConversationRepository.kt +++ b/domain/src/main/java/com/moez/QKSMS/repository/ConversationRepository.kt @@ -21,6 +21,7 @@ package com.moez.QKSMS.repository import com.moez.QKSMS.model.Conversation import com.moez.QKSMS.model.Recipient import com.moez.QKSMS.model.SearchResult +import io.reactivex.Observable import io.realm.RealmResults interface ConversationRepository { @@ -51,6 +52,8 @@ interface ConversationRepository { */ fun getConversations(vararg threadIds: Long): RealmResults + fun getUnmanagedConversations(): Observable> + fun getRecipient(recipientId: Long): Recipient? fun getThreadId(recipient: String): Long? diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeActivity.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeActivity.kt index a0c38ad0..94706ccf 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeActivity.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeActivity.kt @@ -76,7 +76,7 @@ class ComposeActivity : QkThemedActivity(), ComposeView { @Inject lateinit var attachmentAdapter: AttachmentAdapter @Inject lateinit var chipsAdapter: ChipsAdapter - @Inject lateinit var contactsAdapter: ContactAdapter + @Inject lateinit var contactsAdapter: ComposeItemAdapter @Inject lateinit var dateFormatter: DateFormatter @Inject lateinit var messageAdapter: MessagesAdapter @Inject lateinit var navigator: Navigator @@ -86,7 +86,7 @@ class ComposeActivity : QkThemedActivity(), ComposeView { override val queryChangedIntent: Observable by lazy { chipsAdapter.textChanges } override val queryBackspaceIntent: Observable<*> by lazy { chipsAdapter.backspaces } override val queryEditorActionIntent: Observable by lazy { chipsAdapter.actions } - override val chipSelectedIntent: Subject by lazy { contactsAdapter.contactSelected } + override val chipSelectedIntent: Subject by lazy { contactsAdapter.itemSelected } override val chipDeletedIntent: Subject by lazy { chipsAdapter.chipDeleted } override val menuReadyIntent: Observable = menu.map { Unit } override val optionsItemIntent: Subject = PublishSubject.create() @@ -211,7 +211,7 @@ class ComposeActivity : QkThemedActivity(), ComposeView { } chipsAdapter.data = state.selectedContacts - contactsAdapter.data = state.contacts + contactsAdapter.data = state.composeItems loading.setVisible(state.loading) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeItem.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeItem.kt new file mode 100644 index 00000000..e089b689 --- /dev/null +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeItem.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2019 Moez Bhatti + * + * This file is part of QKSMS. + * + * QKSMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QKSMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QKSMS. If not, see . + */ +package com.moez.QKSMS.feature.compose + +import com.moez.QKSMS.model.Contact +import com.moez.QKSMS.model.ContactGroup +import com.moez.QKSMS.model.Conversation +import com.moez.QKSMS.model.PhoneNumber +import io.realm.RealmList + +sealed class ComposeItem { + + abstract fun getContacts(): List + + data class New(val value: Contact) : ComposeItem() { + override fun getContacts(): List = listOf(value) + } + + data class Recent(val value: Conversation) : ComposeItem() { + override fun getContacts(): List = value.recipients.map { recipient -> + Contact(numbers = RealmList(PhoneNumber(address = recipient.address))) + } + } + + data class Starred(val value: Contact) : ComposeItem() { + override fun getContacts(): List = listOf(value) + } + + data class Group(val value: ContactGroup) : ComposeItem() { + override fun getContacts(): List = value.contacts + } + + data class Person(val value: Contact) : ComposeItem() { + override fun getContacts(): List = listOf(value) + } +} diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeItemAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeItemAdapter.kt new file mode 100644 index 00000000..8294fea6 --- /dev/null +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeItemAdapter.kt @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2017 Moez Bhatti + * + * This file is part of QKSMS. + * + * QKSMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QKSMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QKSMS. If not, see . + */ +package com.moez.QKSMS.feature.compose + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +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.forwardTouches +import com.moez.QKSMS.common.util.extensions.setTint +import com.moez.QKSMS.model.Contact +import com.moez.QKSMS.model.ContactGroup +import com.moez.QKSMS.model.Conversation +import com.moez.QKSMS.model.Recipient +import io.reactivex.subjects.PublishSubject +import io.reactivex.subjects.Subject +import kotlinx.android.synthetic.main.contact_list_item.view.* +import javax.inject.Inject + +class ComposeItemAdapter @Inject constructor(private val colors: Colors) : QkAdapter() { + + val itemSelected: Subject = PublishSubject.create() + + private val numbersViewPool = RecyclerView.RecycledViewPool() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkViewHolder { + val layoutInflater = LayoutInflater.from(parent.context) + val view = layoutInflater.inflate(R.layout.contact_list_item, parent, false) + + view.icon.setTint(colors.theme().theme) + + view.numbers.setRecycledViewPool(numbersViewPool) + view.numbers.adapter = PhoneNumberAdapter() + view.numbers.forwardTouches(view) + + return QkViewHolder(view).apply { + view.setOnClickListener { + val item = getItem(adapterPosition) + itemSelected.onNext(item) + } + } + } + + override fun onBindViewHolder(holder: QkViewHolder, position: Int) { + val prevItem = if (position > 0) getItem(position - 1) else null + val item = getItem(position) + val view = holder.containerView + + when (item) { + is ComposeItem.New -> bindNew(view, item.value) + is ComposeItem.Recent -> bindRecent(view, item.value, prevItem) + is ComposeItem.Starred -> bindStarred(view, item.value, prevItem) + is ComposeItem.Person -> bindPerson(view, item.value, prevItem) + is ComposeItem.Group -> bindGroup(view, item.value, prevItem) + } + } + + private fun bindNew(view: View, contact: Contact) { + view.index.isVisible = false + + view.icon.isVisible = false + + view.avatar.contacts = listOf(Recipient(contact = contact)) + + view.title.text = contact.numbers.joinToString { it.address } + + view.subtitle.isVisible = false + + view.numbers.isVisible = false + } + + private fun bindRecent(view: View, conversation: Conversation, prev: ComposeItem?) { + view.index.isVisible = false + + view.icon.isVisible = prev !is ComposeItem.Recent + view.icon.setImageResource(R.drawable.ic_history_black_24dp) + + view.avatar.contacts = conversation.recipients + + view.title.text = conversation.getTitle() + + view.subtitle.isVisible = conversation.recipients.size > 1 && conversation.name.isBlank() + view.subtitle.text = conversation.recipients.joinToString(", ") { recipient -> + recipient.contact?.name ?: recipient.address + } + + view.numbers.isVisible = conversation.recipients.size == 1 + (view.numbers.adapter as PhoneNumberAdapter).data = conversation.recipients + .mapNotNull { recipient -> recipient.contact } + .flatMap { contact -> contact.numbers } + } + + private fun bindStarred(view: View, contact: Contact, prev: ComposeItem?) { + view.index.isVisible = false + + view.icon.isVisible = prev !is ComposeItem.Starred + view.icon.setImageResource(R.drawable.ic_star_black_24dp) + + view.avatar.contacts = listOf(Recipient(contact = contact)) + + view.title.text = contact.name + + view.subtitle.isVisible = false + + view.numbers.isVisible = true + (view.numbers.adapter as PhoneNumberAdapter).data = contact.numbers + } + + private fun bindGroup(view: View, group: ContactGroup, prev: ComposeItem?) { + view.index.isVisible = false + + view.icon.isVisible = prev !is ComposeItem.Group + view.icon.setImageResource(R.drawable.ic_people_black_24dp) + + view.avatar.contacts = group.contacts.map { contact -> Recipient(contact = contact) } + + view.title.text = group.title + + view.subtitle.isVisible = true + view.subtitle.text = group.contacts.joinToString(", ") { it.name } + + view.numbers.isVisible = false + } + + private fun bindPerson(view: View, contact: Contact, prev: ComposeItem?) { + view.index.isVisible = true + view.index.text = if (contact.name.getOrNull(0)?.isLetter() == true) contact.name[0].toString() else "#" + view.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()) + + view.icon.isVisible = false + + view.avatar.contacts = listOf(Recipient(contact = contact)) + + view.title.text = contact.name + + view.subtitle.isVisible = false + + view.numbers.isVisible = true + (view.numbers.adapter as PhoneNumberAdapter).data = contact.numbers + } + + override fun areContentsTheSame(old: ComposeItem, new: ComposeItem): Boolean = false + +} diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeState.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeState.kt index 29862e2f..08312ef0 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeState.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeState.kt @@ -28,7 +28,7 @@ import io.realm.RealmResults data class ComposeState( val hasError: Boolean = false, val editingMode: Boolean = false, - val contacts: List = ArrayList(), + val composeItems: List = ArrayList(), val contactsVisible: Boolean = false, val selectedConversation: Long = 0, val selectedContacts: List = ArrayList(), diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeView.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeView.kt index 137a2aae..e420dac5 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeView.kt @@ -33,7 +33,7 @@ interface ComposeView : QkView { val queryChangedIntent: Observable val queryBackspaceIntent: Observable<*> val queryEditorActionIntent: Observable - val chipSelectedIntent: Subject + val chipSelectedIntent: Subject val chipDeletedIntent: Subject val menuReadyIntent: Observable val optionsItemIntent: Observable diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeViewModel.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeViewModel.kt index 9d54191b..9da74c03 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeViewModel.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeViewModel.kt @@ -37,7 +37,9 @@ import com.moez.QKSMS.extensions.isImage import com.moez.QKSMS.extensions.isVideo import com.moez.QKSMS.extensions.mapNotNull import com.moez.QKSMS.extensions.removeAccents +import com.moez.QKSMS.feature.compose.ComposeItem.* import com.moez.QKSMS.filter.ContactFilter +import com.moez.QKSMS.filter.ContactGroupFilter import com.moez.QKSMS.interactor.AddScheduledMessage import com.moez.QKSMS.interactor.CancelDelayedMessage import com.moez.QKSMS.interactor.ContactSync @@ -50,6 +52,7 @@ import com.moez.QKSMS.manager.PermissionManager import com.moez.QKSMS.model.Attachment import com.moez.QKSMS.model.Attachments import com.moez.QKSMS.model.Contact +import com.moez.QKSMS.model.ContactGroup import com.moez.QKSMS.model.Conversation import com.moez.QKSMS.model.Message import com.moez.QKSMS.model.PhoneNumber @@ -89,6 +92,7 @@ class ComposeViewModel @Inject constructor( private val billingManager: BillingManager, private val cancelMessage: CancelDelayedMessage, private val contactFilter: ContactFilter, + private val contactGroupFilter: ContactGroupFilter, private val contactsRepo: ContactRepository, private val conversationRepo: ConversationRepository, private val deleteMessages: DeleteMessages, @@ -110,13 +114,16 @@ class ComposeViewModel @Inject constructor( ) { private val attachments: Subject> = BehaviorSubject.createDefault(sharedAttachments) - private val contacts: Observable> by lazy { contactsRepo.getUnmanagedContacts().toObservable() } + private val contactGroups: Observable> by lazy { contactsRepo.getUnmanagedContactGroups() } + private val contacts: Observable> by lazy { contactsRepo.getUnmanagedContacts() } private val contactsReducer: Subject<(List) -> List> = PublishSubject.create() + private val conversation: Subject = BehaviorSubject.create() + private val messages: Subject> = BehaviorSubject.create() + private val recents: Observable> by lazy { conversationRepo.getUnmanagedConversations() } private val selectedContacts: Subject> = BehaviorSubject.createDefault(listOf()) private val searchResults: Subject> = BehaviorSubject.create() private val searchSelection: Subject = BehaviorSubject.createDefault(-1) - private val conversation: Subject = BehaviorSubject.create() - private val messages: Subject> = BehaviorSubject.create() + private val starredContacts: Observable> by lazy { contactsRepo.getUnmanagedContacts(true) } init { val initialConversation = threadId.takeIf { it != 0L } @@ -250,31 +257,48 @@ class ComposeViewModel @Inject constructor( // Update the list of contact suggestions based on the query input, while also filtering out any contacts // that have already been selected Observables - .combineLatest(view.queryChangedIntent, contacts, selectedContacts) { query, contacts, - selectedContacts -> + .combineLatest( + view.queryChangedIntent, recents, starredContacts, contactGroups, contacts, selectedContacts + ) { query, recents, starredContacts, contactGroups, contacts, selectedContacts -> + val composeItems = mutableListOf() + if (query.isBlank()) { + composeItems += recents.map(::Recent) + composeItems += starredContacts.map(::Starred) + composeItems += contactGroups.map(::Group) + composeItems += contacts.map(::Person) + } else { + // If the entry is a valid destination, allow it as a recipient + if (phoneNumberUtils.isPossibleNumber(query.toString())) { + val newAddress = phoneNumberUtils.formatNumber(query) + val newContact = Contact(numbers = RealmList(PhoneNumber(address = newAddress))) + composeItems += New(newContact) + } - // Strip the accents from the query. This can be an expensive operation, so - // cache the result instead of doing it for each contact - val normalizedQuery = query.removeAccents() + // Strip the accents from the query. This can be an expensive operation, so + // cache the result instead of doing it for each contact + val normalizedQuery = query.removeAccents() + composeItems += starredContacts + .filterNot { contact -> selectedContacts.contains(contact) } + .filter { contact -> contactFilter.filter(contact, normalizedQuery) } + .map(::Starred) - var filteredContacts = contacts - .filterNot { contact -> selectedContacts.contains(contact) } - .filter { contact -> contactFilter.filter(contact, normalizedQuery) } + composeItems += contactGroups + .filter { group -> contactGroupFilter.filter(group, normalizedQuery) } + .map(::Group) - // If the entry is a valid destination, allow it as a recipient - if (phoneNumberUtils.isPossibleNumber(query.toString())) { - val newAddress = phoneNumberUtils.formatNumber(query) - val newContact = Contact(numbers = RealmList(PhoneNumber(address = newAddress))) - filteredContacts = listOf(newContact) + filteredContacts + composeItems += contacts + .filterNot { contact -> selectedContacts.contains(contact) } + .filter { contact -> contactFilter.filter(contact, normalizedQuery) } + .map(::Person) } - filteredContacts + composeItems } .skipUntil(state.filter { state -> state.editingMode }) .takeUntil(state.filter { state -> !state.editingMode }) .subscribeOn(Schedulers.computation()) .autoDisposable(view.scope()) - .subscribe { contacts -> newState { copy(contacts = contacts) } } + .subscribe { items -> newState { copy(composeItems = items) } } // Backspaces should delete the most recent contact if there's no text input // Close the activity if user presses back @@ -293,8 +317,8 @@ class ComposeViewModel @Inject constructor( .withLatestFrom(state) { _, state -> state } .autoDisposable(view.scope()) .subscribe { state -> - state.contacts.firstOrNull()?.let { contact -> - contactsReducer.onNext { contacts -> contacts + contact } + state.composeItems.firstOrNull()?.let { composeItem -> + contactsReducer.onNext { contacts -> contacts + composeItem.getContacts() } } } @@ -303,8 +327,10 @@ class ComposeViewModel @Inject constructor( view.chipDeletedIntent.doOnNext { contact -> contactsReducer.onNext { contacts -> contacts.filterNot { it == contact } } }, - view.chipSelectedIntent.doOnNext { contact -> - contactsReducer.onNext { contacts -> contacts.toMutableList().apply { add(contact) } } + view.chipSelectedIntent.doOnNext { composeItem -> + contactsReducer.onNext { contacts -> + contacts.toMutableList().apply { addAll(composeItem.getContacts()) } + } }) .skipUntil(state.filter { state -> state.editingMode }) .takeUntil(state.filter { state -> !state.editingMode }) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ContactAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ContactAdapter.kt deleted file mode 100644 index 595f6918..00000000 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ContactAdapter.kt +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (C) 2017 Moez Bhatti - * - * This file is part of QKSMS. - * - * QKSMS is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * QKSMS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with QKSMS. If not, see . - */ -package com.moez.QKSMS.feature.compose - -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.core.view.isVisible -import androidx.recyclerview.widget.RecyclerView -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.extensions.forwardTouches -import com.moez.QKSMS.common.util.extensions.setVisible -import com.moez.QKSMS.model.Contact -import io.reactivex.subjects.PublishSubject -import io.reactivex.subjects.Subject -import kotlinx.android.synthetic.main.contact_list_item.view.* -import javax.inject.Inject - -class ContactAdapter @Inject constructor() : QkAdapter() { - - val contactSelected: Subject = PublishSubject.create() - - private val numbersViewPool = RecyclerView.RecycledViewPool() - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkViewHolder { - val layoutInflater = LayoutInflater.from(parent.context) - val view = layoutInflater.inflate(R.layout.contact_list_item, parent, false) - - view.addresses.setRecycledViewPool(numbersViewPool) - view.addresses.adapter = PhoneNumberAdapter() - view.addresses.forwardTouches(view) - - return QkViewHolder(view).apply { - view.setOnClickListener { - val contact = getItem(adapterPosition) - contactSelected.onNext(contact) - } - } - } - - override fun onBindViewHolder(holder: QkViewHolder, position: Int) { - val prevContact = if (position > 0) getItem(position - 1) else null - val contact = getItem(position) - val view = holder.containerView - - view.index.text = if (contact.name.getOrNull(0)?.isLetter() == true) contact.name[0].toString() else "#" - view.index.isVisible = prevContact == null || - (contact.name[0].isLetter() && !contact.name[0].equals(prevContact.name[0], ignoreCase = true)) || - (!contact.name[0].isLetter() && prevContact.name[0].isLetter()) - - view.avatar.setContact(contact) - view.name.text = contact.name - view.name.setVisible(view.name.text.isNotEmpty()) - - (view.addresses.adapter as PhoneNumberAdapter).data = contact.numbers - } - - /** - * Creates a copy of the contact with only one phone number, so that the chips - * view can still display the name/photo, and not get confused about which phone number to use - */ - private fun copyContact(contact: Contact, numberIndex: Int) = Contact().apply { - lookupKey = contact.lookupKey - name = contact.name - numbers.add(contact.numbers[numberIndex]) - } - - override fun areContentsTheSame(old: Contact, new: Contact): Boolean = false - -} diff --git a/presentation/src/main/res/layout/contact_list_item.xml b/presentation/src/main/res/layout/contact_list_item.xml index 7ccfbbe5..3b669e33 100644 --- a/presentation/src/main/res/layout/contact_list_item.xml +++ b/presentation/src/main/res/layout/contact_list_item.xml @@ -30,7 +30,7 @@ android:id="@+id/index" android:layout_width="24dp" android:layout_height="wrap_content" - android:layout_marginStart="12dp" + android:layout_marginStart="16dp" android:gravity="center_horizontal" android:maxLength="1" android:textStyle="bold" @@ -47,31 +47,31 @@ android:id="@+id/icon" android:layout_width="24dp" android:layout_height="24dp" - android:layout_marginStart="12dp" + android:layout_marginStart="16dp" android:visibility="invisible" app:layout_constraintBottom_toBottomOf="@id/avatar" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@id/avatar" /> - + + \ No newline at end of file