From 95e6d8175a36b22af885c77084c25cd979ae8beb Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Thu, 26 Dec 2019 18:20:14 -0500 Subject: [PATCH 001/109] Respect threadId when sending MMS --- .../com/android/mms/service_alt/DownloadRequest.java | 2 +- .../android/mms/transaction/NotificationTransaction.java | 9 ++++----- .../java/com/android/mms/transaction/PushReceiver.java | 7 ++++--- .../com/android/mms/transaction/RetrieveTransaction.java | 4 ++-- .../com/google/android/mms/pdu_alt/PduPersister.java | 8 ++++---- .../java/com/klinker/android/send_message/Transaction.kt | 2 +- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/android-smsmms/src/main/java/com/android/mms/service_alt/DownloadRequest.java b/android-smsmms/src/main/java/com/android/mms/service_alt/DownloadRequest.java index c413ca74e..b70679497 100755 --- a/android-smsmms/src/main/java/com/android/mms/service_alt/DownloadRequest.java +++ b/android-smsmms/src/main/java/com/android/mms/service_alt/DownloadRequest.java @@ -169,7 +169,7 @@ public class DownloadRequest extends MmsRequest { // } // Store the downloaded message final PduPersister persister = PduPersister.getPduPersister(context); - final Uri messageUri = persister.persist(pdu, Telephony.Mms.Inbox.CONTENT_URI, true, true, null); + final Uri messageUri = persister.persist(pdu, Telephony.Mms.Inbox.CONTENT_URI, PduPersister.DUMMY_THREAD_ID, true, true, null); if (messageUri == null) { Timber.e("DownloadRequest.persistIfRequired: can not persist message"); return null; diff --git a/android-smsmms/src/main/java/com/android/mms/transaction/NotificationTransaction.java b/android-smsmms/src/main/java/com/android/mms/transaction/NotificationTransaction.java index 074586bda..1b03c0bab 100755 --- a/android-smsmms/src/main/java/com/android/mms/transaction/NotificationTransaction.java +++ b/android-smsmms/src/main/java/com/android/mms/transaction/NotificationTransaction.java @@ -110,9 +110,8 @@ public class NotificationTransaction extends Transaction implements Runnable { try { // Save the pdu. If we can start downloading the real pdu immediately, don't allow // persist() to create a thread for the notificationInd because it causes UI jank. - mUri = PduPersister.getPduPersister(context).persist( - ind, Inbox.CONTENT_URI, !allowAutoDownload(mContext), - true, null); + mUri = PduPersister.getPduPersister(context).persist(ind, Inbox.CONTENT_URI, + PduPersister.DUMMY_THREAD_ID, !allowAutoDownload(mContext), true, null); } catch (MmsException e) { Timber.e(e, "Failed to save NotificationInd in constructor."); throw new IllegalArgumentException(); @@ -185,8 +184,8 @@ public class NotificationTransaction extends Transaction implements Runnable { } else { // Save the received PDU (must be a M-RETRIEVE.CONF). PduPersister p = PduPersister.getPduPersister(mContext); - Uri uri = p.persist(pdu, Inbox.CONTENT_URI, true, - true, null); + Uri uri = p.persist(pdu, Inbox.CONTENT_URI, PduPersister.DUMMY_THREAD_ID, + true, true, null); RetrieveConf retrieveConf = (RetrieveConf) pdu; diff --git a/android-smsmms/src/main/java/com/android/mms/transaction/PushReceiver.java b/android-smsmms/src/main/java/com/android/mms/transaction/PushReceiver.java index a701be4b7..722b2f834 100755 --- a/android-smsmms/src/main/java/com/android/mms/transaction/PushReceiver.java +++ b/android-smsmms/src/main/java/com/android/mms/transaction/PushReceiver.java @@ -107,8 +107,8 @@ public class PushReceiver extends BroadcastReceiver { break; } - Uri uri = p.persist(pdu, Uri.parse("content://mms/inbox"), true, - true, null); + Uri uri = p.persist(pdu, Uri.parse("content://mms/inbox"), + PduPersister.DUMMY_THREAD_ID, true, true, null); // Update thread ID for ReadOrigInd & DeliveryInd. ContentValues values = new ContentValues(1); values.put(Mms.THREAD_ID, threadId); @@ -136,7 +136,8 @@ public class PushReceiver extends BroadcastReceiver { // Save the pdu. If we can start downloading the real pdu immediately, // don't allow persist() to create a thread for the notificationInd // because it causes UI jank. - Uri uri = p.persist(pdu, Inbox.CONTENT_URI, true, true, null); + Uri uri = p.persist(pdu, Inbox.CONTENT_URI, + PduPersister.DUMMY_THREAD_ID, true, true, null); String location = getContentLocation(mContext, uri); if (downloadedUrls.contains(location)) { diff --git a/android-smsmms/src/main/java/com/android/mms/transaction/RetrieveTransaction.java b/android-smsmms/src/main/java/com/android/mms/transaction/RetrieveTransaction.java index a9ea47b21..4de1c9b8a 100755 --- a/android-smsmms/src/main/java/com/android/mms/transaction/RetrieveTransaction.java +++ b/android-smsmms/src/main/java/com/android/mms/transaction/RetrieveTransaction.java @@ -153,8 +153,8 @@ public class RetrieveTransaction extends Transaction implements Runnable { } else { // Store M-Retrieve.conf into Inbox PduPersister persister = PduPersister.getPduPersister(mContext); - msgUri = persister.persist(retrieveConf, Inbox.CONTENT_URI, true, - true, null); + msgUri = persister.persist(retrieveConf, Inbox.CONTENT_URI, + PduPersister.DUMMY_THREAD_ID, true, true, null); // Use local time instead of PDU time ContentValues values = new ContentValues(3); diff --git a/android-smsmms/src/main/java/com/google/android/mms/pdu_alt/PduPersister.java b/android-smsmms/src/main/java/com/google/android/mms/pdu_alt/PduPersister.java index 638b4c9ab..07bcb551f 100755 --- a/android-smsmms/src/main/java/com/google/android/mms/pdu_alt/PduPersister.java +++ b/android-smsmms/src/main/java/com/google/android/mms/pdu_alt/PduPersister.java @@ -67,7 +67,7 @@ import java.util.Set; public class PduPersister { private static final boolean LOCAL_LOGV = false; - private static final long DUMMY_THREAD_ID = Long.MAX_VALUE; + public static final long DUMMY_THREAD_ID = Long.MAX_VALUE; private static final int DEFAULT_SUBSCRIPTION = 0; private static final int MAX_TEXT_BODY_SIZE = 300 * 1024; @@ -1263,6 +1263,7 @@ public class PduPersister { * * @param pdu The PDU object to be stored. * @param uri Where to store the given PDU object. + * @param threadId * @param createThreadId if true, this function may create a thread id for the recipients * @param groupMmsEnabled if true, all of the recipients addressed in the PDU will be used * to create the associated thread. When false, only the sender will be used in finding or @@ -1271,7 +1272,7 @@ public class PduPersister { * @return A Uri which can be used to access the stored PDU. */ - public Uri persist(GenericPdu pdu, Uri uri, boolean createThreadId, boolean groupMmsEnabled, + public Uri persist(GenericPdu pdu, Uri uri, long threadId, boolean createThreadId, boolean groupMmsEnabled, HashMap preOpenedFiles) throws MmsException { if (uri == null) { @@ -1400,8 +1401,7 @@ public class PduPersister { loadRecipients(PduHeaders.TO, recipients, addressMap, false); break; } - long threadId = DUMMY_THREAD_ID; - if (createThreadId && !recipients.isEmpty()) { + if (threadId == DUMMY_THREAD_ID && createThreadId && !recipients.isEmpty()) { // Given all the recipients associated with this message, find (or create) the // correct thread. threadId = Threads.getOrCreateThreadId(mContext, recipients); diff --git a/android-smsmms/src/main/java/com/klinker/android/send_message/Transaction.kt b/android-smsmms/src/main/java/com/klinker/android/send_message/Transaction.kt index 575484500..35912778f 100755 --- a/android-smsmms/src/main/java/com/klinker/android/send_message/Transaction.kt +++ b/android-smsmms/src/main/java/com/klinker/android/send_message/Transaction.kt @@ -94,7 +94,7 @@ class Transaction @JvmOverloads constructor(private val context: Context, settin val sendReq = buildPdu(context, addresses, subject, parts) val persister = PduPersister.getPduPersister(context) - val messageUri = existingUri ?: persister.persist(sendReq, Uri.parse("content://mms/outbox"), true, true, null) + val messageUri = existingUri ?: persister.persist(sendReq, Uri.parse("content://mms/outbox"), threadId, true, true, null) val sentIntent = Intent(MMS_SENT) BroadcastUtils.addClassName(context, sentIntent, MMS_SENT) -- GitLab From 3530164f96f578906321c0c2760ad2bb17c41710 Mon Sep 17 00:00:00 2001 From: moezbhatti Date: Sun, 29 Sep 2019 15:04:42 -0400 Subject: [PATCH 002/109] Stop flashing for unchanged numbers --- .../com/moez/QKSMS/feature/compose/PhoneNumberAdapter.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/PhoneNumberAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/PhoneNumberAdapter.kt index 3e8689434..94d62e381 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/PhoneNumberAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/PhoneNumberAdapter.kt @@ -51,4 +51,12 @@ class PhoneNumberAdapter( view.type.text = number.type } + override fun areItemsTheSame(old: PhoneNumber, new: PhoneNumber): Boolean { + return old.type == new.type && old.address == new.address + } + + override fun areContentsTheSame(old: PhoneNumber, new: PhoneNumber): Boolean { + return old.type == new.type && old.address == new.address + } + } \ No newline at end of file -- GitLab From 66208c445a14443f0b90ed863b12a1ba3129d3e0 Mon Sep 17 00:00:00 2001 From: moezbhatti Date: Sun, 29 Sep 2019 17:43:51 -0400 Subject: [PATCH 003/109] Update contact list stype to show indices --- .../QKSMS/feature/compose/ContactAdapter.kt | 28 +++---- .../feature/compose/PhoneNumberAdapter.kt | 10 +-- .../src/main/res/layout/contact_list_item.xml | 84 +++++++++---------- .../res/layout/contact_number_list_item.xml | 29 +++---- 4 files changed, 70 insertions(+), 81 deletions(-) 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 index 524bb783c..0f8b7875e 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ContactAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ContactAdapter.kt @@ -20,10 +20,12 @@ 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 @@ -42,32 +44,34 @@ class ContactAdapter @Inject constructor() : QkAdapter() { 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.primary.setOnClickListener { + view.setOnClickListener { val contact = getItem(adapterPosition) - contactSelected.onNext(copyContact(contact, 0)) - } - - view.addresses.adapter = PhoneNumberAdapter { contact, index -> - contactSelected.onNext(copyContact(contact, index + 1)) + 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[0].isLetter()) contact.name[0].toString() else "#" + view.index.isVisible = prevContact == null || + (contact.name[0].isLetter() && contact.name[0] != prevContact.name[0]) || + (!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.address.text = contact.numbers.firstOrNull()?.address ?: "" - view.type.text = contact.numbers.firstOrNull()?.type ?: "" val adapter = view.addresses.adapter as PhoneNumberAdapter adapter.contact = contact - adapter.data = contact.numbers.drop(Math.min(contact.numbers.size, 1)) + adapter.data = contact.numbers } /** @@ -80,8 +84,4 @@ class ContactAdapter @Inject constructor() : QkAdapter() { numbers.add(contact.numbers[numberIndex]) } - override fun areItemsTheSame(old: Contact, new: Contact): Boolean { - return old.lookupKey == new.lookupKey - } - -} \ No newline at end of file +} diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/PhoneNumberAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/PhoneNumberAdapter.kt index 94d62e381..14db217c9 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/PhoneNumberAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/PhoneNumberAdapter.kt @@ -25,11 +25,9 @@ import com.moez.QKSMS.common.base.QkAdapter import com.moez.QKSMS.common.base.QkViewHolder import com.moez.QKSMS.model.Contact import com.moez.QKSMS.model.PhoneNumber -import kotlinx.android.synthetic.main.contact_list_item.view.* +import kotlinx.android.synthetic.main.contact_number_list_item.view.* -class PhoneNumberAdapter( - private val numberClicked: (Contact, Int) -> Unit -) : QkAdapter() { +class PhoneNumberAdapter : QkAdapter() { lateinit var contact: Contact @@ -43,10 +41,6 @@ class PhoneNumberAdapter( val number = getItem(position) val view = holder.containerView - // Setting this in onCreateViewHolder causes a crash sometimes. [contact] returns the - // contact from a different row, I'm not sure why - view.setOnClickListener { numberClicked(contact, position) } - view.address.text = number.address view.type.text = number.type } diff --git a/presentation/src/main/res/layout/contact_list_item.xml b/presentation/src/main/res/layout/contact_list_item.xml index a2c3bd384..7ccfbbe5a 100644 --- a/presentation/src/main/res/layout/contact_list_item.xml +++ b/presentation/src/main/res/layout/contact_list_item.xml @@ -21,16 +21,45 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="wrap_content"> + android:layout_height="wrap_content" + android:background="?attr/selectableItemBackground" + android:paddingTop="8dp" + android:paddingBottom="8dp"> + + + + @@ -41,59 +70,24 @@ android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginEnd="16dp" - app:layout_constraintBottom_toTopOf="@id/address" + android:textStyle="bold" + app:layout_constraintBottom_toTopOf="@id/addresses" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0" app:layout_constraintStart_toEndOf="@id/avatar" - app:layout_constraintTop_toTopOf="@id/avatar" + app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_chainStyle="packed" tools:text="Moez Bhatti" /> - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/contact_number_list_item.xml b/presentation/src/main/res/layout/contact_number_list_item.xml index 5aa2b23d9..0ea6fab36 100644 --- a/presentation/src/main/res/layout/contact_number_list_item.xml +++ b/presentation/src/main/res/layout/contact_number_list_item.xml @@ -22,26 +22,27 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="?attr/selectableItemBackground"> + android:paddingTop="2dp"> + tools:text="Mobile" /> + android:textColor="?android:attr/textColorTertiary" + app:textSize="secondary" + tools:text="(123) 456-7890" /> \ No newline at end of file -- GitLab From 8740390ad11bca4078a82dc384b9eb5e9ec2b971 Mon Sep 17 00:00:00 2001 From: moezbhatti Date: Sun, 29 Sep 2019 18:16:28 -0400 Subject: [PATCH 004/109] Show two initials for avatar --- .../java/com/moez/QKSMS/common/widget/AvatarView.kt | 10 ++++++++-- presentation/src/main/res/layout/avatar_view.xml | 6 ++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/presentation/src/main/java/com/moez/QKSMS/common/widget/AvatarView.kt b/presentation/src/main/java/com/moez/QKSMS/common/widget/AvatarView.kt index ba4e854a3..12fcf47e0 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/widget/AvatarView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/widget/AvatarView.kt @@ -35,7 +35,9 @@ import com.moez.QKSMS.util.GlideApp import kotlinx.android.synthetic.main.avatar_view.view.* import javax.inject.Inject -class AvatarView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : FrameLayout(context, attrs) { +class AvatarView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null +) : FrameLayout(context, attrs) { @Inject lateinit var colors: Colors @Inject lateinit var navigator: Navigator @@ -116,7 +118,11 @@ class AvatarView @JvmOverloads constructor(context: Context, attrs: AttributeSet private fun updateView() { if (name?.isNotEmpty() == true) { - initial.text = name?.substring(0, 1) + val initials = name?.split(" ").orEmpty() + .filter { name -> name.isNotEmpty() } + .map { name -> name[0].toString() } + + initial.text = if (initials.size > 1) initials.first() + initials.last() else initials.first() icon.visibility = GONE } else { initial.text = null diff --git a/presentation/src/main/res/layout/avatar_view.xml b/presentation/src/main/res/layout/avatar_view.xml index c0896e8f6..8f8e2aa9f 100644 --- a/presentation/src/main/res/layout/avatar_view.xml +++ b/presentation/src/main/res/layout/avatar_view.xml @@ -18,6 +18,7 @@ ~ along with QKSMS. If not, see . --> + app:autoSizeMaxTextSize="22sp" + app:autoSizeTextType="uniform" /> Date: Sun, 27 Oct 2019 12:14:33 -0400 Subject: [PATCH 005/109] Don't crash when name is empty --- .../main/java/com/moez/QKSMS/feature/compose/ContactAdapter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 0f8b7875e..934c21ca8 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ContactAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ContactAdapter.kt @@ -60,7 +60,7 @@ class ContactAdapter @Inject constructor() : QkAdapter() { val contact = getItem(position) val view = holder.containerView - view.index.text = if (contact.name[0].isLetter()) contact.name[0].toString() else "#" + 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] != prevContact.name[0]) || (!contact.name[0].isLetter() && prevContact.name[0].isLetter()) -- GitLab From 66fdee21f9b70dc458f1a079264e115bd4679cee Mon Sep 17 00:00:00 2001 From: moezbhatti Date: Sun, 27 Oct 2019 13:07:25 -0400 Subject: [PATCH 006/109] Make sure indices get updated --- .../java/com/moez/QKSMS/feature/compose/ContactAdapter.kt | 6 +++--- .../com/moez/QKSMS/feature/compose/PhoneNumberAdapter.kt | 3 --- 2 files changed, 3 insertions(+), 6 deletions(-) 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 index 934c21ca8..a7cb4ce4d 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ContactAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ContactAdapter.kt @@ -69,9 +69,7 @@ class ContactAdapter @Inject constructor() : QkAdapter() { view.name.text = contact.name view.name.setVisible(view.name.text.isNotEmpty()) - val adapter = view.addresses.adapter as PhoneNumberAdapter - adapter.contact = contact - adapter.data = contact.numbers + (view.addresses.adapter as PhoneNumberAdapter).data = contact.numbers } /** @@ -84,4 +82,6 @@ class ContactAdapter @Inject constructor() : QkAdapter() { numbers.add(contact.numbers[numberIndex]) } + override fun areContentsTheSame(old: Contact, new: Contact): Boolean = false + } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/PhoneNumberAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/PhoneNumberAdapter.kt index 14db217c9..ca7df4496 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/PhoneNumberAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/PhoneNumberAdapter.kt @@ -23,14 +23,11 @@ import android.view.ViewGroup import com.moez.QKSMS.R import com.moez.QKSMS.common.base.QkAdapter import com.moez.QKSMS.common.base.QkViewHolder -import com.moez.QKSMS.model.Contact import com.moez.QKSMS.model.PhoneNumber import kotlinx.android.synthetic.main.contact_number_list_item.view.* class PhoneNumberAdapter : QkAdapter() { - lateinit var contact: Contact - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkViewHolder { val inflater = LayoutInflater.from(parent.context) val view = inflater.inflate(R.layout.contact_number_list_item, parent, false) -- GitLab From 24fe776fda51ad213f9bfd3648a616b77244c1f0 Mon Sep 17 00:00:00 2001 From: moezbhatti Date: Sun, 27 Oct 2019 13:24:48 -0400 Subject: [PATCH 007/109] Sort contacts with letter names first --- .../QKSMS/repository/ContactRepositoryImpl.kt | 20 +++++++++++++++---- .../QKSMS/feature/compose/ContactAdapter.kt | 2 +- 2 files changed, 17 insertions(+), 5 deletions(-) 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 848e7d38a..45248ff9e 100644 --- a/data/src/main/java/com/moez/QKSMS/repository/ContactRepositoryImpl.kt +++ b/data/src/main/java/com/moez/QKSMS/repository/ContactRepositoryImpl.kt @@ -77,10 +77,9 @@ class ContactRepositoryImpl @Inject constructor( Phone.getTypeLabel(context.resources, Phone.TYPE_MOBILE, "Mobile").toString() } - return when (prefs.mobileOnly.get()) { + val contactsFlowable = when (prefs.mobileOnly.get()) { true -> realm.where(Contact::class.java) .contains("numbers.type", mobileLabel) - .sort("name") .findAllAsync() .asFlowable() .filter { it.isLoaded } @@ -98,7 +97,6 @@ class ContactRepositoryImpl @Inject constructor( } false -> realm.where(Contact::class.java) - .sort("name") .findAllAsync() .asFlowable() .filter { it.isLoaded } @@ -107,6 +105,20 @@ class ContactRepositoryImpl @Inject constructor( .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) + } + }) + } } -} \ No newline at end of file +} 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 index a7cb4ce4d..595f69187 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ContactAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ContactAdapter.kt @@ -62,7 +62,7 @@ class ContactAdapter @Inject constructor() : QkAdapter() { 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] != prevContact.name[0]) || + (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) -- GitLab From 11f0a7c1f0980648bbd5c5fab26f0f4e13d7be38 Mon Sep 17 00:00:00 2001 From: moezbhatti Date: Sun, 27 Oct 2019 17:50:27 -0400 Subject: [PATCH 008/109] Sync starred contacts and groups --- .../moez/QKSMS/extensions/CursorExtensions.kt | 37 +++--------- .../QKSMS/mapper/CursorToContactGroupImpl.kt | 52 ++++++++++++++++ .../mapper/CursorToContactGroupMemberImpl.kt | 52 ++++++++++++++++ .../moez/QKSMS/mapper/CursorToContactImpl.kt | 7 ++- .../moez/QKSMS/migration/QkRealmMigration.kt | 14 ++++- .../QKSMS/repository/SyncRepositoryImpl.kt | 60 +++++++++++++------ .../moez/QKSMS/mapper/CursorToContactGroup.kt | 28 +++++++++ .../mapper/CursorToContactGroupMember.kt | 29 +++++++++ .../main/java/com/moez/QKSMS/model/Contact.kt | 1 + .../java/com/moez/QKSMS/model/ContactGroup.kt | 29 +++++++++ .../com/moez/QKSMS/injection/AppModule.kt | 10 ++++ 11 files changed, 268 insertions(+), 51 deletions(-) create mode 100644 data/src/main/java/com/moez/QKSMS/mapper/CursorToContactGroupImpl.kt create mode 100644 data/src/main/java/com/moez/QKSMS/mapper/CursorToContactGroupMemberImpl.kt create mode 100644 domain/src/main/java/com/moez/QKSMS/mapper/CursorToContactGroup.kt create mode 100644 domain/src/main/java/com/moez/QKSMS/mapper/CursorToContactGroupMember.kt create mode 100644 domain/src/main/java/com/moez/QKSMS/model/ContactGroup.kt diff --git a/data/src/main/java/com/moez/QKSMS/extensions/CursorExtensions.kt b/data/src/main/java/com/moez/QKSMS/extensions/CursorExtensions.kt index 1214d0be8..a23e95b25 100644 --- a/data/src/main/java/com/moez/QKSMS/extensions/CursorExtensions.kt +++ b/data/src/main/java/com/moez/QKSMS/extensions/CursorExtensions.kt @@ -20,8 +20,6 @@ package com.moez.QKSMS.extensions import android.database.Cursor import io.reactivex.Flowable -import io.reactivex.Maybe -import io.reactivex.subjects.MaybeSubject fun Cursor.forEach(closeOnComplete: Boolean = true, method: (Cursor) -> Unit = {}) { moveToPosition(-1) @@ -41,21 +39,6 @@ fun Cursor.map(map: (Cursor) -> T): List { } } -fun Cursor.mapWhile(map: (Cursor) -> T, predicate: (T) -> Boolean): ArrayList { - val result = ArrayList() - - moveToPosition(-1) - while (moveToNext()) { - val item = map(this) - - if (!predicate(item)) break - - result.add(item) - } - - return result -} - /** * We're using this simple implementation with .range() because of the * complexities of dealing with Backpressure with a Cursor. We can't simply @@ -72,18 +55,14 @@ fun Cursor.asFlowable(): Flowable { .doOnComplete { close() } } -fun Cursor.asMaybe(): Maybe { - val subject = MaybeSubject.create() +/** + * Dumps the contents of the cursor as a CSV string + */ +fun Cursor.dump(): String { + val lines = mutableListOf() - if (moveToFirst()) { - subject.onSuccess(this) - } else { - subject.onError(IndexOutOfBoundsException("The cursor has no items")) - } + lines += columnNames.joinToString(",") + forEach { lines += (0 until columnCount).joinToString(",", transform = ::getString) } - subject.doOnComplete { close() } - return subject + return lines.joinToString("\n") } - - - diff --git a/data/src/main/java/com/moez/QKSMS/mapper/CursorToContactGroupImpl.kt b/data/src/main/java/com/moez/QKSMS/mapper/CursorToContactGroupImpl.kt new file mode 100644 index 000000000..f2f290b5d --- /dev/null +++ b/data/src/main/java/com/moez/QKSMS/mapper/CursorToContactGroupImpl.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.mapper + +import android.content.Context +import android.database.Cursor +import android.provider.ContactsContract +import com.moez.QKSMS.model.ContactGroup +import javax.inject.Inject + +class CursorToContactGroupImpl @Inject constructor( + private val context: Context +) : CursorToContactGroup { + + companion object { + private val URI = ContactsContract.Groups.CONTENT_URI + private val PROJECTION = arrayOf( + ContactsContract.Groups._ID, + ContactsContract.Groups.TITLE) + private const val SELECTION = "${ContactsContract.Groups.AUTO_ADD}=0 " + + "AND ${ContactsContract.Groups.DELETED}=0 " + + "AND ${ContactsContract.Groups.FAVORITES}=0" + + private const val ID = 0 + private const val TITLE = 1 + } + + override fun map(from: Cursor): ContactGroup { + return ContactGroup(from.getLong(ID), from.getString(TITLE)) + } + + override fun getContactGroupsCursor(): Cursor? { + return context.contentResolver.query(URI, PROJECTION, SELECTION, null, null) + } + +} diff --git a/data/src/main/java/com/moez/QKSMS/mapper/CursorToContactGroupMemberImpl.kt b/data/src/main/java/com/moez/QKSMS/mapper/CursorToContactGroupMemberImpl.kt new file mode 100644 index 000000000..7eb72aab7 --- /dev/null +++ b/data/src/main/java/com/moez/QKSMS/mapper/CursorToContactGroupMemberImpl.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.mapper + +import android.content.Context +import android.database.Cursor +import android.provider.ContactsContract +import javax.inject.Inject + +class CursorToContactGroupMemberImpl @Inject constructor( + private val context: Context +) : CursorToContactGroupMember { + + companion object { + private val URI = ContactsContract.Data.CONTENT_URI + private val PROJECTION = arrayOf( + ContactsContract.Data.LOOKUP_KEY, + ContactsContract.Data.DATA1) + + private const val SELECTION = "${ContactsContract.Data.MIMETYPE}=?" + private val SELECTION_ARGS = arrayOf( + ContactsContract.CommonDataKinds.GroupMembership.CONTENT_ITEM_TYPE) + + private const val LOOKUP_KEY = 0 + private const val GROUP_ID = 1 + } + + override fun map(from: Cursor): CursorToContactGroupMember.GroupMember { + return CursorToContactGroupMember.GroupMember(from.getString(LOOKUP_KEY), from.getLong(GROUP_ID)) + } + + override fun getGroupMembersCursor(): Cursor? { + return context.contentResolver.query(URI, PROJECTION, SELECTION, SELECTION_ARGS, null) + } + +} diff --git a/data/src/main/java/com/moez/QKSMS/mapper/CursorToContactImpl.kt b/data/src/main/java/com/moez/QKSMS/mapper/CursorToContactImpl.kt index 82d50ca9b..072e777ee 100644 --- a/data/src/main/java/com/moez/QKSMS/mapper/CursorToContactImpl.kt +++ b/data/src/main/java/com/moez/QKSMS/mapper/CursorToContactImpl.kt @@ -39,6 +39,7 @@ class CursorToContactImpl @Inject constructor( Phone.TYPE, Phone.LABEL, Phone.DISPLAY_NAME, + Phone.STARRED, Phone.CONTACT_LAST_UPDATED_TIMESTAMP ) @@ -47,7 +48,8 @@ class CursorToContactImpl @Inject constructor( const val COLUMN_TYPE = 2 const val COLUMN_LABEL = 3 const val COLUMN_DISPLAY_NAME = 4 - const val CONTACT_LAST_UPDATED = 5 + const val COLUMN_STARRED = 5 + const val CONTACT_LAST_UPDATED = 6 } override fun map(from: Cursor) = Contact().apply { @@ -58,6 +60,7 @@ class CursorToContactImpl @Inject constructor( type = Phone.getTypeLabel(context.resources, from.getInt(COLUMN_TYPE), from.getString(COLUMN_LABEL)).toString() )) + starred = from.getInt(COLUMN_STARRED) != 0 lastUpdate = from.getLong(CONTACT_LAST_UPDATED) } @@ -68,4 +71,4 @@ class CursorToContactImpl @Inject constructor( } } -} \ No newline at end of file +} diff --git a/data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt b/data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt index 0d5ea8178..1461fa9bb 100644 --- a/data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt +++ b/data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt @@ -26,7 +26,7 @@ import io.realm.Sort class QkRealmMigration : RealmMigration { companion object { - const val SCHEMA_VERSION: Long = 8 + const val SCHEMA_VERSION: Long = 9 } override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { @@ -118,6 +118,18 @@ class QkRealmMigration : RealmMigration { version++ } + if (version == 8L) { + realm.schema.create("ContactGroup") + .addField("id", Long::class.java, FieldAttribute.PRIMARY_KEY, FieldAttribute.REQUIRED) + .addField("title", String::class.java, FieldAttribute.REQUIRED) + .addRealmListField("contacts", realm.schema.get("Contact")) + + realm.schema.get("Contact") + ?.addField("starred", Boolean::class.java, FieldAttribute.REQUIRED) + + version++ + } + check(version >= newVersion) { "Migration missing from v$oldVersion to v$newVersion" } } diff --git a/data/src/main/java/com/moez/QKSMS/repository/SyncRepositoryImpl.kt b/data/src/main/java/com/moez/QKSMS/repository/SyncRepositoryImpl.kt index 46bc532d8..679f592dc 100644 --- a/data/src/main/java/com/moez/QKSMS/repository/SyncRepositoryImpl.kt +++ b/data/src/main/java/com/moez/QKSMS/repository/SyncRepositoryImpl.kt @@ -27,10 +27,13 @@ import com.moez.QKSMS.extensions.insertOrUpdate import com.moez.QKSMS.extensions.map import com.moez.QKSMS.manager.KeyManager import com.moez.QKSMS.mapper.CursorToContact +import com.moez.QKSMS.mapper.CursorToContactGroup +import com.moez.QKSMS.mapper.CursorToContactGroupMember import com.moez.QKSMS.mapper.CursorToConversation import com.moez.QKSMS.mapper.CursorToMessage import com.moez.QKSMS.mapper.CursorToRecipient 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.MmsPart @@ -53,6 +56,8 @@ class SyncRepositoryImpl @Inject constructor( private val cursorToMessage: CursorToMessage, private val cursorToRecipient: CursorToRecipient, private val cursorToContact: CursorToContact, + private val cursorToContactGroup: CursorToContactGroup, + private val cursorToContactGroupMember: CursorToContactGroupMember, private val keys: KeyManager, private val phoneNumberUtils: PhoneNumberUtils, private val rxPrefs: RxSharedPreferences @@ -89,6 +94,7 @@ class SyncRepositoryImpl @Inject constructor( .toMutableMap() realm.delete(Contact::class.java) + realm.delete(ContactGroup::class.java) realm.delete(Conversation::class.java) realm.delete(Message::class.java) realm.delete(MmsPart::class.java) @@ -234,19 +240,19 @@ class SyncRepositoryImpl @Inject constructor( realm.executeTransaction { realm.delete(Contact::class.java) + realm.delete(ContactGroup::class.java) contacts = realm.copyToRealm(contacts) + realm.insertOrUpdate(getContactGroups(contacts)) // Update all the recipients with the new contacts - val updatedRecipients = recipients.map { recipient -> - recipient.apply { - contact = contacts.firstOrNull { - it.numbers.any { phoneNumberUtils.compare(recipient.address, it.address) } - } + recipients.forEach { recipient -> + recipient.contact = contacts.find { contact -> + contact.numbers.any { phoneNumberUtils.compare(recipient.address, it.address) } } } - realm.insertOrUpdate(updatedRecipients) + realm.insertOrUpdate(recipients) } } @@ -254,26 +260,24 @@ class SyncRepositoryImpl @Inject constructor( override fun syncContact(address: String): Boolean { // See if there's a contact that matches this phone number - var contact = getContacts().firstOrNull { - it.numbers.any { number -> phoneNumberUtils.compare(number.address, address) } + var contact = getContacts().find { contact -> + contact.numbers.any { number -> phoneNumberUtils.compare(number.address, address) } } ?: return false Realm.getDefaultInstance().use { realm -> - val recipients = realm.where(Recipient::class.java).findAll() + val recipients = realm.where(Recipient::class.java).findAll().filter { recipient -> + contact.numbers.any { number -> + phoneNumberUtils.compare(recipient.address, number.address) + } + } realm.executeTransaction { contact = realm.copyToRealmOrUpdate(contact) // Update all the matching recipients with the new contact - val updatedRecipients = recipients - .filter { recipient -> - contact.numbers.any { number -> - phoneNumberUtils.compare(recipient.address, number.address) - } - } - .map { recipient -> recipient.apply { this.contact = contact } } - - realm.insertOrUpdate(updatedRecipients) + recipients.forEach { recipient -> recipient.contact = contact } + + realm.insertOrUpdate(recipients) } } @@ -293,4 +297,22 @@ class SyncRepositoryImpl @Inject constructor( } ?: listOf() } -} \ No newline at end of file + private fun getContactGroups(contacts: List): List { + val groupMembers = cursorToContactGroupMember.getGroupMembersCursor() + ?.map(cursorToContactGroupMember::map) + .orEmpty() + + val groups = cursorToContactGroup.getContactGroupsCursor() + ?.map(cursorToContactGroup::map) + .orEmpty() + + groups.forEach { group -> + group.contacts.addAll(groupMembers + .filter { member -> member.groupId == group.id } + .mapNotNull { member -> contacts.find { contact -> contact.lookupKey == member.lookupKey } }) + } + + return groups + } + +} diff --git a/domain/src/main/java/com/moez/QKSMS/mapper/CursorToContactGroup.kt b/domain/src/main/java/com/moez/QKSMS/mapper/CursorToContactGroup.kt new file mode 100644 index 000000000..2de28c6f4 --- /dev/null +++ b/domain/src/main/java/com/moez/QKSMS/mapper/CursorToContactGroup.kt @@ -0,0 +1,28 @@ +/* + * 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.mapper + +import android.database.Cursor +import com.moez.QKSMS.model.ContactGroup + +interface CursorToContactGroup : Mapper { + + fun getContactGroupsCursor(): Cursor? + +} diff --git a/domain/src/main/java/com/moez/QKSMS/mapper/CursorToContactGroupMember.kt b/domain/src/main/java/com/moez/QKSMS/mapper/CursorToContactGroupMember.kt new file mode 100644 index 000000000..22861fdd4 --- /dev/null +++ b/domain/src/main/java/com/moez/QKSMS/mapper/CursorToContactGroupMember.kt @@ -0,0 +1,29 @@ +/* + * 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.mapper + +import android.database.Cursor + +interface CursorToContactGroupMember : Mapper { + + data class GroupMember(val lookupKey: String, val groupId: Long) + + fun getGroupMembersCursor(): Cursor? + +} diff --git a/domain/src/main/java/com/moez/QKSMS/model/Contact.kt b/domain/src/main/java/com/moez/QKSMS/model/Contact.kt index e7e256379..dd325dacf 100644 --- a/domain/src/main/java/com/moez/QKSMS/model/Contact.kt +++ b/domain/src/main/java/com/moez/QKSMS/model/Contact.kt @@ -26,5 +26,6 @@ open class Contact( @PrimaryKey var lookupKey: String = "", var numbers: RealmList = RealmList(), var name: String = "", + var starred: Boolean = false, var lastUpdate: Long = 0 ) : RealmObject() \ No newline at end of file diff --git a/domain/src/main/java/com/moez/QKSMS/model/ContactGroup.kt b/domain/src/main/java/com/moez/QKSMS/model/ContactGroup.kt new file mode 100644 index 000000000..fd0e4363f --- /dev/null +++ b/domain/src/main/java/com/moez/QKSMS/model/ContactGroup.kt @@ -0,0 +1,29 @@ +/* + * 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.model + +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +open class ContactGroup( + @PrimaryKey var id: Long = 0, + var title: String = "", + var contacts: RealmList = RealmList() +) : RealmObject() diff --git a/presentation/src/main/java/com/moez/QKSMS/injection/AppModule.kt b/presentation/src/main/java/com/moez/QKSMS/injection/AppModule.kt index b8f68adbb..1192b9e76 100644 --- a/presentation/src/main/java/com/moez/QKSMS/injection/AppModule.kt +++ b/presentation/src/main/java/com/moez/QKSMS/injection/AppModule.kt @@ -50,6 +50,10 @@ import com.moez.QKSMS.manager.ShortcutManager import com.moez.QKSMS.manager.WidgetManager import com.moez.QKSMS.manager.WidgetManagerImpl import com.moez.QKSMS.mapper.CursorToContact +import com.moez.QKSMS.mapper.CursorToContactGroup +import com.moez.QKSMS.mapper.CursorToContactGroupImpl +import com.moez.QKSMS.mapper.CursorToContactGroupMember +import com.moez.QKSMS.mapper.CursorToContactGroupMemberImpl import com.moez.QKSMS.mapper.CursorToContactImpl import com.moez.QKSMS.mapper.CursorToConversation import com.moez.QKSMS.mapper.CursorToConversationImpl @@ -153,6 +157,12 @@ class AppModule(private var application: Application) { @Provides fun provideCursorToContact(mapper: CursorToContactImpl): CursorToContact = mapper + @Provides + fun provideCursorToContactGroup(mapper: CursorToContactGroupImpl): CursorToContactGroup = mapper + + @Provides + fun provideCursorToContactGroupMember(mapper: CursorToContactGroupMemberImpl): CursorToContactGroupMember = mapper + @Provides fun provideCursorToConversation(mapper: CursorToConversationImpl): CursorToConversation = mapper -- GitLab From 389baca6f7b49d7bbc68d988f713c8435382505f Mon Sep 17 00:00:00 2001 From: moezbhatti Date: Sun, 27 Oct 2019 22:04:43 -0400 Subject: [PATCH 009/109] Display recent, starred, and contact groups in compose --- .../com/moez/QKSMS/filter/ContactFilter.kt | 2 +- .../moez/QKSMS/filter/ContactGroupFilter.kt | 32 ++++ .../QKSMS/repository/ContactRepositoryImpl.kt | 90 ++++++---- .../repository/ConversationRepositoryImpl.kt | 23 +++ .../QKSMS/repository/ContactRepository.kt | 7 +- .../repository/ConversationRepository.kt | 3 + .../QKSMS/feature/compose/ComposeActivity.kt | 6 +- .../moez/QKSMS/feature/compose/ComposeItem.kt | 52 ++++++ .../feature/compose/ComposeItemAdapter.kt | 167 ++++++++++++++++++ .../QKSMS/feature/compose/ComposeState.kt | 2 +- .../moez/QKSMS/feature/compose/ComposeView.kt | 2 +- .../QKSMS/feature/compose/ComposeViewModel.kt | 137 +++++++++----- .../QKSMS/feature/compose/ContactAdapter.kt | 87 --------- .../src/main/res/layout/contact_list_item.xml | 32 ++-- 14 files changed, 451 insertions(+), 191 deletions(-) create mode 100644 data/src/main/java/com/moez/QKSMS/filter/ContactGroupFilter.kt create mode 100644 presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeItem.kt create mode 100644 presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeItemAdapter.kt delete mode 100644 presentation/src/main/java/com/moez/QKSMS/feature/compose/ContactAdapter.kt 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 8e77da1af..6010c7b30 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 000000000..903e16fd8 --- /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 45248ff9e..842a423c1 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) + } + + if (starred) { + query = query.equalTo("starred", true) } - 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 -> + 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 0c05e2227..988b5ce4a 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 037336261..0bc8471bd 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 7c517c105..a6f1a0711 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 6f894223a..fcd71374c 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 @@ -78,7 +78,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 @@ -88,7 +88,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() @@ -232,7 +232,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 000000000..e089b689e --- /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 000000000..8294fea6a --- /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 29862e2ff..08312ef05 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 b59321efe..42b80eff8 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 d74fc9aa0..02f3d917e 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 @@ -40,11 +40,27 @@ 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.interactor.* import com.moez.QKSMS.manager.ActiveConversationManager import com.moez.QKSMS.manager.PermissionManager import com.moez.QKSMS.model.* +import com.moez.QKSMS.filter.ContactGroupFilter +import com.moez.QKSMS.interactor.AddScheduledMessage +import com.moez.QKSMS.interactor.CancelDelayedMessage +import com.moez.QKSMS.interactor.ContactSync +import com.moez.QKSMS.interactor.DeleteMessages +import com.moez.QKSMS.interactor.MarkRead +import com.moez.QKSMS.interactor.RetrySending +import com.moez.QKSMS.interactor.SendMessage +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 import com.moez.QKSMS.repository.ContactRepository import com.moez.QKSMS.repository.ConversationRepository import com.moez.QKSMS.repository.MessageRepository @@ -70,30 +86,31 @@ import javax.inject.Inject import javax.inject.Named class ComposeViewModel @Inject constructor( - @Named("query") private val query: String, - @Named("threadId") private val threadId: Long, - @Named("address") private val address: String, - @Named("text") private val sharedText: String, - @Named("attachments") private val sharedAttachments: Attachments, - private val context: Context, - private val activeConversationManager: ActiveConversationManager, - private val addScheduledMessage: AddScheduledMessage, - private val cancelMessage: CancelDelayedMessage, - private val contactFilter: ContactFilter, - private val contactsRepo: ContactRepository, - private val conversationRepo: ConversationRepository, - private val deleteMessages: DeleteMessages, - private val markRead: MarkRead, - private val messageDetailsFormatter: MessageDetailsFormatter, - private val messageRepo: MessageRepository, - private val navigator: Navigator, - private val permissionManager: PermissionManager, - private val phoneNumberUtils: PhoneNumberUtils, - private val prefs: Preferences, - private val retrySending: RetrySending, - private val sendMessage: SendMessage, - private val subscriptionManager: SubscriptionManagerCompat, - private val syncContacts: ContactSync + @Named("query") private val query: String, + @Named("threadId") private val threadId: Long, + @Named("address") private val address: String, + @Named("text") private val sharedText: String, + @Named("attachments") private val sharedAttachments: Attachments, + private val context: Context, + private val activeConversationManager: ActiveConversationManager, + private val addScheduledMessage: AddScheduledMessage, + private val cancelMessage: CancelDelayedMessage, + private val contactFilter: ContactFilter, + private val contactGroupFilter: ContactGroupFilter, + private val contactsRepo: ContactRepository, + private val conversationRepo: ConversationRepository, + private val deleteMessages: DeleteMessages, + private val markRead: MarkRead, + private val messageDetailsFormatter: MessageDetailsFormatter, + private val messageRepo: MessageRepository, + private val navigator: Navigator, + private val permissionManager: PermissionManager, + private val phoneNumberUtils: PhoneNumberUtils, + private val prefs: Preferences, + private val retrySending: RetrySending, + private val sendMessage: SendMessage, + private val subscriptionManager: SubscriptionManagerCompat, + private val syncContacts: ContactSync ) : QkViewModel(ComposeState( editingMode = threadId == 0L && address.isBlank(), selectedConversation = threadId, @@ -101,13 +118,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 } @@ -242,31 +262,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 -> - - // 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() - - var filteredContacts = contacts - .filterNot { contact -> selectedContacts.contains(contact) } - .filter { contact -> contactFilter.filter(contact, normalizedQuery) } - - // 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 + .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() + composeItems += starredContacts + .filterNot { contact -> selectedContacts.contains(contact) } + .filter { contact -> contactFilter.filter(contact, normalizedQuery) } + .map(::Starred) + + composeItems += contactGroups + .filter { group -> contactGroupFilter.filter(group, normalizedQuery) } + .map(::Group) + + 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 @@ -285,8 +322,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() } } } @@ -295,8 +332,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 595f69187..000000000 --- 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 7ccfbbe5a..3b669e330 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 -- GitLab From 36bd1558254ca23667e78d74273fee0acd49b544 Mon Sep 17 00:00:00 2001 From: moezbhatti Date: Sun, 27 Oct 2019 22:07:38 -0400 Subject: [PATCH 010/109] Organize compose package --- .../java/com/moez/QKSMS/feature/compose/ComposeActivity.kt | 3 +++ .../java/com/moez/QKSMS/feature/compose/ComposeState.kt | 1 + .../main/java/com/moez/QKSMS/feature/compose/ComposeView.kt | 1 + .../java/com/moez/QKSMS/feature/compose/ComposeViewModel.kt | 3 ++- .../com/moez/QKSMS/feature/compose/ComposeWindowCallback.kt | 6 +++++- .../QKSMS/feature/compose/{ => editing}/ChipsAdapter.kt | 4 ++-- .../moez/QKSMS/feature/compose/{ => editing}/ComposeItem.kt | 2 +- .../feature/compose/{ => editing}/ComposeItemAdapter.kt | 4 ++-- .../QKSMS/feature/compose/{ => editing}/DetailedChipView.kt | 5 ++--- .../feature/compose/{ => editing}/PhoneNumberAdapter.kt | 2 +- .../src/main/java/com/moez/QKSMS/injection/AppComponent.kt | 2 +- 11 files changed, 21 insertions(+), 12 deletions(-) rename presentation/src/main/java/com/moez/QKSMS/feature/compose/{ => editing}/ChipsAdapter.kt (98%) rename presentation/src/main/java/com/moez/QKSMS/feature/compose/{ => editing}/ComposeItem.kt (97%) rename presentation/src/main/java/com/moez/QKSMS/feature/compose/{ => editing}/ComposeItemAdapter.kt (98%) rename presentation/src/main/java/com/moez/QKSMS/feature/compose/{ => editing}/DetailedChipView.kt (95%) rename presentation/src/main/java/com/moez/QKSMS/feature/compose/{ => editing}/PhoneNumberAdapter.kt (97%) 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 fcd71374c..e031c5a3c 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 @@ -55,6 +55,9 @@ import com.moez.QKSMS.common.util.extensions.setBackgroundTint import com.moez.QKSMS.common.util.extensions.setTint import com.moez.QKSMS.common.util.extensions.setVisible import com.moez.QKSMS.common.util.extensions.showKeyboard +import com.moez.QKSMS.feature.compose.editing.ChipsAdapter +import com.moez.QKSMS.feature.compose.editing.ComposeItem +import com.moez.QKSMS.feature.compose.editing.ComposeItemAdapter import com.moez.QKSMS.model.Attachment import com.moez.QKSMS.model.Contact import com.uber.autodispose.android.lifecycle.scope 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 08312ef05..9b4b7e6c9 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 @@ -19,6 +19,7 @@ package com.moez.QKSMS.feature.compose import com.moez.QKSMS.compat.SubscriptionInfoCompat +import com.moez.QKSMS.feature.compose.editing.ComposeItem import com.moez.QKSMS.model.Attachment import com.moez.QKSMS.model.Contact import com.moez.QKSMS.model.Conversation 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 42b80eff8..e08decc69 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 @@ -22,6 +22,7 @@ import android.net.Uri import androidx.annotation.StringRes import androidx.core.view.inputmethod.InputContentInfoCompat import com.moez.QKSMS.common.base.QkView +import com.moez.QKSMS.feature.compose.editing.ComposeItem import com.moez.QKSMS.model.Attachment import com.moez.QKSMS.model.Contact import io.reactivex.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 02f3d917e..a297c2264 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 @@ -40,7 +40,8 @@ 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.feature.compose.editing.ComposeItem +import com.moez.QKSMS.feature.compose.editing.ComposeItem.* import com.moez.QKSMS.filter.ContactFilter import com.moez.QKSMS.interactor.* import com.moez.QKSMS.manager.ActiveConversationManager diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeWindowCallback.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeWindowCallback.kt index b3f8acbfe..0ea9eab79 100755 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeWindowCallback.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeWindowCallback.kt @@ -32,8 +32,12 @@ import android.view.Window import android.view.WindowManager import android.view.accessibility.AccessibilityEvent import androidx.annotation.RequiresApi +import com.moez.QKSMS.feature.compose.editing.DetailedChipView -class ComposeWindowCallback(private val localCallback: Window.Callback, private val activity: Activity) : Window.Callback { +class ComposeWindowCallback( + private val localCallback: Window.Callback, + private val activity: Activity +) : Window.Callback { override fun dispatchKeyEvent(keyEvent: KeyEvent): Boolean { return localCallback.dispatchKeyEvent(keyEvent) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ChipsAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ChipsAdapter.kt similarity index 98% rename from presentation/src/main/java/com/moez/QKSMS/feature/compose/ChipsAdapter.kt rename to presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ChipsAdapter.kt index 2d7d7d50a..5bd9f7377 100755 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ChipsAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ChipsAdapter.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017 Moez Bhatti + * Copyright (C) 2019 Moez Bhatti * * This file is part of QKSMS. * @@ -16,7 +16,7 @@ * You should have received a copy of the GNU General Public License * along with QKSMS. If not, see . */ -package com.moez.QKSMS.feature.compose +package com.moez.QKSMS.feature.compose.editing import android.content.Context import android.view.LayoutInflater diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeItem.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ComposeItem.kt similarity index 97% rename from presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeItem.kt rename to presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ComposeItem.kt index e089b689e..894daa1e2 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeItem.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ComposeItem.kt @@ -16,7 +16,7 @@ * You should have received a copy of the GNU General Public License * along with QKSMS. If not, see . */ -package com.moez.QKSMS.feature.compose +package com.moez.QKSMS.feature.compose.editing import com.moez.QKSMS.model.Contact import com.moez.QKSMS.model.ContactGroup diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeItemAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ComposeItemAdapter.kt similarity index 98% rename from presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeItemAdapter.kt rename to presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ComposeItemAdapter.kt index 8294fea6a..8a4a4dcbe 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeItemAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ComposeItemAdapter.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017 Moez Bhatti + * Copyright (C) 2019 Moez Bhatti * * This file is part of QKSMS. * @@ -16,7 +16,7 @@ * You should have received a copy of the GNU General Public License * along with QKSMS. If not, see . */ -package com.moez.QKSMS.feature.compose +package com.moez.QKSMS.feature.compose.editing import android.view.LayoutInflater import android.view.View diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/DetailedChipView.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/DetailedChipView.kt similarity index 95% rename from presentation/src/main/java/com/moez/QKSMS/feature/compose/DetailedChipView.kt rename to presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/DetailedChipView.kt index 5bcca0dae..731d9c9da 100755 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/DetailedChipView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/DetailedChipView.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017 Moez Bhatti + * Copyright (C) 2019 Moez Bhatti * * This file is part of QKSMS. * @@ -16,7 +16,7 @@ * You should have received a copy of the GNU General Public License * along with QKSMS. If not, see . */ -package com.moez.QKSMS.feature.compose +package com.moez.QKSMS.feature.compose.editing import android.content.Context import android.view.View @@ -31,7 +31,6 @@ import com.moez.QKSMS.model.Contact import kotlinx.android.synthetic.main.contact_chip_detailed.view.* import javax.inject.Inject - class DetailedChipView(context: Context) : RelativeLayout(context) { @Inject lateinit var colors: Colors diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/PhoneNumberAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/PhoneNumberAdapter.kt similarity index 97% rename from presentation/src/main/java/com/moez/QKSMS/feature/compose/PhoneNumberAdapter.kt rename to presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/PhoneNumberAdapter.kt index ca7df4496..565bd1b9f 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/PhoneNumberAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/PhoneNumberAdapter.kt @@ -16,7 +16,7 @@ * You should have received a copy of the GNU General Public License * along with QKSMS. If not, see . */ -package com.moez.QKSMS.feature.compose +package com.moez.QKSMS.feature.compose.editing import android.view.LayoutInflater import android.view.ViewGroup diff --git a/presentation/src/main/java/com/moez/QKSMS/injection/AppComponent.kt b/presentation/src/main/java/com/moez/QKSMS/injection/AppComponent.kt index 0ac8b34b2..b54506a20 100644 --- a/presentation/src/main/java/com/moez/QKSMS/injection/AppComponent.kt +++ b/presentation/src/main/java/com/moez/QKSMS/injection/AppComponent.kt @@ -33,7 +33,7 @@ import com.moez.QKSMS.feature.blocking.BlockingController import com.moez.QKSMS.feature.blocking.manager.BlockingManagerController import com.moez.QKSMS.feature.blocking.messages.BlockedMessagesController import com.moez.QKSMS.feature.blocking.numbers.BlockedNumbersController -import com.moez.QKSMS.feature.compose.DetailedChipView +import com.moez.QKSMS.feature.compose.editing.DetailedChipView import com.moez.QKSMS.feature.conversationinfo.injection.ConversationInfoComponent import com.moez.QKSMS.feature.service.ESmsRestoreService import com.moez.QKSMS.feature.settings.SettingsController -- GitLab From 29ee691a8f30f6ba666e8065fcc5065fe4b7161b Mon Sep 17 00:00:00 2001 From: moezbhatti Date: Wed, 30 Oct 2019 21:05:41 -0400 Subject: [PATCH 011/109] Show button to add new contact --- .../QKSMS/feature/compose/ComposeActivity.kt | 15 ++-- .../QKSMS/feature/compose/ComposeState.kt | 2 +- .../QKSMS/feature/compose/ComposeViewModel.kt | 15 ++-- .../feature/compose/editing/ChipsAdapter.kt | 88 ++++--------------- .../src/main/res/layout/compose_activity.xml | 14 +++ presentation/src/main/res/menu/compose.xml | 7 ++ presentation/src/main/res/values/strings.xml | 1 + 7 files changed, 56 insertions(+), 86 deletions(-) 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 e031c5a3c..6b3a1ab99 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 @@ -43,6 +43,7 @@ import androidx.lifecycle.ViewModelProviders import com.google.android.flexbox.FlexboxLayoutManager import com.google.android.material.snackbar.Snackbar import com.jakewharton.rxbinding2.view.clicks +import com.jakewharton.rxbinding2.widget.editorActions import com.jakewharton.rxbinding2.widget.textChanges import com.moez.QKSMS.R import com.moez.QKSMS.common.Navigator @@ -88,9 +89,9 @@ class ComposeActivity : QkThemedActivity(), ComposeView { @Inject lateinit var viewModelFactory: ViewModelProvider.Factory override val activityVisibleIntent: Subject = PublishSubject.create() - 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 queryChangedIntent: Observable by lazy { search.textChanges() } + override val queryBackspaceIntent: Observable<*> by lazy { search.backspaces } + override val queryEditorActionIntent: Observable by lazy { search.editorActions() } override val chipSelectedIntent: Subject by lazy { contactsAdapter.itemSelected } override val chipDeletedIntent: Subject by lazy { chipsAdapter.chipDeleted } override val menuReadyIntent: Observable = menu.map { Unit } @@ -212,14 +213,16 @@ class ComposeActivity : QkThemedActivity(), ComposeView { toolbarSubtitle.text = getString(R.string.compose_subtitle_results, state.searchSelectionPosition, state.searchResults) toolbarTitle.setVisible(!state.editingMode) - chips.setVisible(state.editingMode) - contacts.setVisible(state.contactsVisible) - composeBar.setVisible(!state.contactsVisible && !state.loading) + chips.setVisible(state.editingMode && !state.searching) + search.setVisible(state.editingMode && state.searching) + contacts.setVisible(state.editingMode && state.searching) + composeBar.setVisible(!state.searching && !state.loading) // Don't set the adapters unless needed if (state.editingMode && chips.adapter == null) chips.adapter = chipsAdapter if (state.editingMode && contacts.adapter == null) contacts.adapter = contactsAdapter + toolbar.menu.findItem(R.id.add)?.isVisible = state.editingMode && !state.searching 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 && state.query.isEmpty() toolbar.menu.findItem(R.id.copy)?.isVisible = !state.editingMode && state.selectedMessages == 1 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 9b4b7e6c9..2502442d9 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 @@ -29,8 +29,8 @@ import io.realm.RealmResults data class ComposeState( val hasError: Boolean = false, val editingMode: Boolean = false, + val searching: Boolean = false, val composeItems: List = ArrayList(), - val contactsVisible: Boolean = false, val selectedConversation: Long = 0, val selectedContacts: List = ArrayList(), val sendAsGroup: Boolean = true, 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 a297c2264..b0e7195d1 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 @@ -114,6 +114,7 @@ class ComposeViewModel @Inject constructor( private val syncContacts: ContactSync ) : QkViewModel(ComposeState( editingMode = threadId == 0L && address.isBlank(), + searching = threadId == 0L && address.isBlank(), selectedConversation = threadId, query = query) ) { @@ -248,17 +249,13 @@ class ComposeViewModel @Inject constructor( override fun bindView(view: ComposeView) { super.bindView(view) - // Set the contact suggestions list to visible at all times when in editing mode and there are no contacts - // selected yet, and also visible while in editing mode and there is text entered in the query field - Observables - .combineLatest(view.queryChangedIntent, selectedContacts) { query, selectedContacts -> - selectedContacts.isEmpty() || query.isNotEmpty() - } + // Set the contact suggestions list to visible when the add button is pressed + view.optionsItemIntent + .filter { it == R.id.add } .skipUntil(state.filter { state -> state.editingMode }) .takeUntil(state.filter { state -> !state.editingMode }) - .distinctUntilChanged() .autoDisposable(view.scope()) - .subscribe { contactsVisible -> newState { copy(contactsVisible = contactsVisible && editingMode) } } + .subscribe { newState { copy(searching = true) } } // Update the list of contact suggestions based on the query input, while also filtering out any contacts // that have already been selected @@ -323,6 +320,7 @@ class ComposeViewModel @Inject constructor( .withLatestFrom(state) { _, state -> state } .autoDisposable(view.scope()) .subscribe { state -> + newState { copy(searching = false) } state.composeItems.firstOrNull()?.let { composeItem -> contactsReducer.onNext { contacts -> contacts + composeItem.getContacts() } } @@ -334,6 +332,7 @@ class ComposeViewModel @Inject constructor( contactsReducer.onNext { contacts -> contacts.filterNot { it == contact } } }, view.chipSelectedIntent.doOnNext { composeItem -> + newState { copy(searching = false) } contactsReducer.onNext { contacts -> contacts.toMutableList().apply { addAll(composeItem.getContacts()) } } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ChipsAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ChipsAdapter.kt index 5bd9f7377..ca5b5364a 100755 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ChipsAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ChipsAdapter.kt @@ -20,103 +20,49 @@ package com.moez.QKSMS.feature.compose.editing import android.content.Context import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import android.widget.RelativeLayout import androidx.recyclerview.widget.RecyclerView -import com.google.android.flexbox.FlexboxLayoutManager -import com.jakewharton.rxbinding2.widget.editorActions -import com.jakewharton.rxbinding2.widget.textChanges 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.dpToPx -import com.moez.QKSMS.common.util.extensions.resolveThemeColor -import com.moez.QKSMS.common.util.extensions.showKeyboard -import com.moez.QKSMS.common.widget.QkEditText import com.moez.QKSMS.model.Contact import io.reactivex.subjects.PublishSubject import kotlinx.android.synthetic.main.contact_chip.view.* import javax.inject.Inject -class ChipsAdapter @Inject constructor(private val context: Context) : QkAdapter() { - - companion object { - private const val TYPE_EDIT_TEXT = 0 - private const val TYPE_ITEM = 1 - } - - private val hint: String = context.getString(R.string.title_compose) - private val editText = View.inflate(context, R.layout.chip_input_list_item, null) as QkEditText +class ChipsAdapter @Inject constructor() : QkAdapter() { var view: RecyclerView? = null val chipDeleted: PublishSubject = PublishSubject.create() - val textChanges = editText.textChanges() - val actions = editText.editorActions() - val backspaces = editText.backspaces - - init { - val wrap = ViewGroup.LayoutParams.WRAP_CONTENT - editText.layoutParams = FlexboxLayoutManager.LayoutParams(wrap, wrap).apply { - minHeight = 36.dpToPx(context) - minWidth = 56.dpToPx(context) - flexGrow = 8f - } - - editText.hint = hint - } - - override fun onDatasetChanged() { - editText.text = null - editText.hint = if (itemCount == 1) hint else null - if (itemCount != 2) { - editText.showKeyboard() - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when (viewType) { - TYPE_EDIT_TEXT -> { - editText.setTextColor(parent.context.resolveThemeColor(android.R.attr.textColorPrimary)) - editText.setHintTextColor(parent.context.resolveThemeColor(android.R.attr.textColorTertiary)) - QkViewHolder(editText) - } - - else -> { - val inflater = LayoutInflater.from(parent.context) - val view = inflater.inflate(R.layout.contact_chip, parent, false) - QkViewHolder(view).apply { - view.setOnClickListener { - val contact = getItem(adapterPosition) - showDetailedChip(view.context, contact) - } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkViewHolder { + val inflater = LayoutInflater.from(parent.context) + val view = inflater.inflate(R.layout.contact_chip, parent, false) + return QkViewHolder(view).apply { + view.setOnClickListener { + val contact = getItem(adapterPosition) + showDetailedChip(view.context, contact) } } } override fun onBindViewHolder(holder: QkViewHolder, position: Int) { - when (getItemViewType(position)) { - TYPE_ITEM -> { - val contact = getItem(position) - val view = holder.containerView + val contact = getItem(position) + val view = holder.containerView - view.avatar.setContact(contact) + view.avatar.setContact(contact) - // If the contact's name is empty, try to display a phone number instead - // The contacts provided here should only have one number - view.name.text = if (contact.name.isNotBlank()) { - contact.name - } else { - contact.numbers.firstOrNull { it.address.isNotBlank() }?.address ?: "" - } - } + // If the contact's name is empty, try to display a phone number instead + // The contacts provided here should only have one number + view.name.text = if (contact.name.isNotBlank()) { + contact.name + } else { + contact.numbers.firstOrNull { it.address.isNotBlank() }?.address ?: "" } } - override fun getItemCount() = super.getItemCount() + 1 - - override fun getItemViewType(position: Int) = if (position == itemCount - 1) TYPE_EDIT_TEXT else TYPE_ITEM - /** * The [context] has to come from a view, because we're inflating a view that used themed attrs */ diff --git a/presentation/src/main/res/layout/compose_activity.xml b/presentation/src/main/res/layout/compose_activity.xml index bc3f37315..c29637e41 100644 --- a/presentation/src/main/res/layout/compose_activity.xml +++ b/presentation/src/main/res/layout/compose_activity.xml @@ -383,6 +383,20 @@ android:scrollbars="vertical" tools:visibility="gone" /> + + diff --git a/presentation/src/main/res/menu/compose.xml b/presentation/src/main/res/menu/compose.xml index 85d05b57f..c39f25ac2 100644 --- a/presentation/src/main/res/menu/compose.xml +++ b/presentation/src/main/res/menu/compose.xml @@ -20,6 +20,13 @@ + + Skip Continue + Add person Call Details Save to gallery -- GitLab From c23b344096f929edf3d6b88442af08f6efcb579f Mon Sep 17 00:00:00 2001 From: moezbhatti Date: Wed, 30 Oct 2019 21:26:43 -0400 Subject: [PATCH 012/109] Go back to search mode if all contacts are removed --- .../java/com/moez/QKSMS/common/widget/GroupAvatarView.kt | 4 +++- .../com/moez/QKSMS/feature/compose/ComposeViewModel.kt | 8 +++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/presentation/src/main/java/com/moez/QKSMS/common/widget/GroupAvatarView.kt b/presentation/src/main/java/com/moez/QKSMS/common/widget/GroupAvatarView.kt index 0021a7e42..d3f2d9afd 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/widget/GroupAvatarView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/widget/GroupAvatarView.kt @@ -27,7 +27,9 @@ import com.moez.QKSMS.R import com.moez.QKSMS.model.Recipient import kotlinx.android.synthetic.main.group_avatar_view.view.* -class GroupAvatarView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : ConstraintLayout(context, attrs) { +class GroupAvatarView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null +) : ConstraintLayout(context, attrs) { var contacts: List = ArrayList() set(value) { 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 b0e7195d1..15a20f78b 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 @@ -329,7 +329,13 @@ class ComposeViewModel @Inject constructor( // Update the list of selected contacts when a new contact is selected or an existing one is deselected Observable.merge( view.chipDeletedIntent.doOnNext { contact -> - contactsReducer.onNext { contacts -> contacts.filterNot { it == contact } } + contactsReducer.onNext { contacts -> + val result = contacts.filterNot { it == contact } + if (result.isEmpty()) { + newState { copy(searching = true) } + } + result + } }, view.chipSelectedIntent.doOnNext { composeItem -> newState { copy(searching = false) } -- GitLab From a27eb5619472f67a3d35edd4e0bb4b2a8f3b9ca2 Mon Sep 17 00:00:00 2001 From: moezbhatti Date: Thu, 31 Oct 2019 00:09:40 -0400 Subject: [PATCH 013/109] Updated group avatar style Closes #1492, closes #1093 --- .../QKSMS/common/widget/GroupAvatarView.kt | 34 ++++++------- .../src/main/res/layout/blocked_list_item.xml | 16 +++--- .../src/main/res/layout/contact_list_item.xml | 8 +-- .../res/layout/conversation_list_item.xml | 12 ++--- .../src/main/res/layout/group_avatar_view.xml | 50 ++++++++++++------- .../layout/scheduled_message_list_item.xml | 6 +-- .../src/main/res/layout/search_list_item.xml | 12 ++--- 7 files changed, 76 insertions(+), 62 deletions(-) diff --git a/presentation/src/main/java/com/moez/QKSMS/common/widget/GroupAvatarView.kt b/presentation/src/main/java/com/moez/QKSMS/common/widget/GroupAvatarView.kt index d3f2d9afd..904ec4608 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/widget/GroupAvatarView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/widget/GroupAvatarView.kt @@ -19,11 +19,15 @@ package com.moez.QKSMS.common.widget import android.content.Context -import android.os.Build import android.util.AttributeSet import android.view.View import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams import com.moez.QKSMS.R +import com.moez.QKSMS.common.util.extensions.getColorCompat +import com.moez.QKSMS.common.util.extensions.resolveThemeColor +import com.moez.QKSMS.common.util.extensions.setBackgroundTint import com.moez.QKSMS.model.Recipient import kotlinx.android.synthetic.main.group_avatar_view.view.* @@ -37,36 +41,30 @@ class GroupAvatarView @JvmOverloads constructor( updateView() } - private val avatars by lazy { listOf(avatar1, avatar2, avatar3) } - init { View.inflate(context, R.layout.group_avatar_view, this) - setBackgroundResource(R.drawable.circle) - clipToOutline = true } override fun onFinishInflate() { super.onFinishInflate() - avatars.forEach { avatar -> - avatar.setBackgroundResource(R.drawable.rectangle) - - // If we're on API 21 we need to reapply the tint after changing the background - if (Build.VERSION.SDK_INT < 22) { - avatar.applyTheme(0) - } - } - if (!isInEditMode) { updateView() } } private fun updateView() { - avatars.forEachIndexed { index, avatar -> - avatar.visibility = if (contacts.size > index) View.VISIBLE else View.GONE - avatar.setContact(contacts.getOrNull(index)) + avatar1Frame.setBackgroundTint(when (contacts.size > 1) { + true -> context.resolveThemeColor(android.R.attr.windowBackground) + false -> context.getColorCompat(android.R.color.transparent) + }) + avatar1Frame.updateLayoutParams { + matchConstraintPercentWidth = if (contacts.size > 1) 0.75f else 1.0f } + avatar2.isVisible = contacts.size > 1 + + avatar1.setContact(contacts.getOrNull(0)) + avatar2.setContact(contacts.getOrNull(1)) } -} \ No newline at end of file +} diff --git a/presentation/src/main/res/layout/blocked_list_item.xml b/presentation/src/main/res/layout/blocked_list_item.xml index 5c41be73c..a70b945eb 100644 --- a/presentation/src/main/res/layout/blocked_list_item.xml +++ b/presentation/src/main/res/layout/blocked_list_item.xml @@ -23,15 +23,15 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="?attr/selectableItemBackground" - android:paddingTop="8dp" - android:paddingBottom="8dp" + android:paddingTop="4dp" + android:paddingBottom="4dp" app:layout_constraintVertical_chainStyle="packed"> @@ -69,12 +70,11 @@ android:id="@+id/blocker" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="16dp" android:layout_marginTop="2dp" android:textColor="?android:attr/textColorTertiary" android:textStyle="bold" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintStart_toEndOf="@id/avatars" + app:layout_constraintStart_toStartOf="@id/title" app:layout_constraintTop_toBottomOf="@id/title" app:textSize="secondary" tools:text="Call Control" /> diff --git a/presentation/src/main/res/layout/contact_list_item.xml b/presentation/src/main/res/layout/contact_list_item.xml index 3b669e330..9c10ef533 100644 --- a/presentation/src/main/res/layout/contact_list_item.xml +++ b/presentation/src/main/res/layout/contact_list_item.xml @@ -55,9 +55,9 @@ + android:paddingBottom="8dp"> - + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_bias="0" + app:layout_constraintWidth_percent=".75"> - + - + + + app:layout_constraintHorizontal_bias="0" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_bias="1" + app:layout_constraintWidth_percent=".75"> + + + + \ No newline at end of file diff --git a/presentation/src/main/res/layout/scheduled_message_list_item.xml b/presentation/src/main/res/layout/scheduled_message_list_item.xml index 46e6733ae..6520ea741 100644 --- a/presentation/src/main/res/layout/scheduled_message_list_item.xml +++ b/presentation/src/main/res/layout/scheduled_message_list_item.xml @@ -25,12 +25,12 @@ android:background="?attr/selectableItemBackground" android:gravity="center_vertical" android:orientation="horizontal" - android:padding="16dp"> + android:padding="12dp"> Date: Thu, 31 Oct 2019 00:32:50 -0400 Subject: [PATCH 014/109] Fix avatars not working --- .../src/main/java/com/moez/QKSMS/common/widget/AvatarView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/presentation/src/main/java/com/moez/QKSMS/common/widget/AvatarView.kt b/presentation/src/main/java/com/moez/QKSMS/common/widget/AvatarView.kt index 12fcf47e0..a5b2ca156 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/widget/AvatarView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/widget/AvatarView.kt @@ -94,7 +94,7 @@ class AvatarView @JvmOverloads constructor( lookupKey = contact?.lookupKey name = contact?.name // If a contactAddress has been given, we use it. Use the contact address otherwise. - address = contactAddress ?: contact?.numbers?.firstOrNull()?.address + address = contactAddress?.takeIf { it.isNotEmpty() } ?: contact?.numbers?.firstOrNull()?.address lastUpdated = contact?.lastUpdate updateView() } -- GitLab From 65be76c37a85dcd31d7891b35fd7bfa7b1ca9fff Mon Sep 17 00:00:00 2001 From: moezbhatti Date: Thu, 31 Oct 2019 01:17:59 -0400 Subject: [PATCH 015/109] Fix vertical padding for contact list item --- presentation/src/main/res/layout/contact_list_item.xml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/presentation/src/main/res/layout/contact_list_item.xml b/presentation/src/main/res/layout/contact_list_item.xml index 9c10ef533..0fc94716e 100644 --- a/presentation/src/main/res/layout/contact_list_item.xml +++ b/presentation/src/main/res/layout/contact_list_item.xml @@ -22,9 +22,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="?attr/selectableItemBackground" - android:paddingTop="8dp" - android:paddingBottom="8dp"> + android:background="?attr/selectableItemBackground"> Date: Thu, 31 Oct 2019 01:19:47 -0400 Subject: [PATCH 016/109] Filter out duplicate phone number entries that come from multiple contacts providers Closes #1285 --- .../moez/QKSMS/mapper/CursorToContactImpl.kt | 15 ++++++++------ .../moez/QKSMS/migration/QkRealmMigration.kt | 3 +++ .../QKSMS/repository/SyncRepositoryImpl.kt | 20 +++++++++++++++++-- .../java/com/moez/QKSMS/model/PhoneNumber.kt | 1 + 4 files changed, 31 insertions(+), 8 deletions(-) diff --git a/data/src/main/java/com/moez/QKSMS/mapper/CursorToContactImpl.kt b/data/src/main/java/com/moez/QKSMS/mapper/CursorToContactImpl.kt index 072e777ee..62f1602d3 100644 --- a/data/src/main/java/com/moez/QKSMS/mapper/CursorToContactImpl.kt +++ b/data/src/main/java/com/moez/QKSMS/mapper/CursorToContactImpl.kt @@ -35,6 +35,7 @@ class CursorToContactImpl @Inject constructor( val URI = Phone.CONTENT_URI val PROJECTION = arrayOf( Phone.LOOKUP_KEY, + Phone.ACCOUNT_TYPE_AND_DATA_SET, Phone.NUMBER, Phone.TYPE, Phone.LABEL, @@ -44,18 +45,20 @@ class CursorToContactImpl @Inject constructor( ) const val COLUMN_LOOKUP_KEY = 0 - const val COLUMN_NUMBER = 1 - const val COLUMN_TYPE = 2 - const val COLUMN_LABEL = 3 - const val COLUMN_DISPLAY_NAME = 4 - const val COLUMN_STARRED = 5 - const val CONTACT_LAST_UPDATED = 6 + const val COLUMN_ACCOUNT_TYPE = 1 + const val COLUMN_NUMBER = 2 + const val COLUMN_TYPE = 3 + const val COLUMN_LABEL = 4 + const val COLUMN_DISPLAY_NAME = 5 + const val COLUMN_STARRED = 6 + const val CONTACT_LAST_UPDATED = 7 } override fun map(from: Cursor) = Contact().apply { lookupKey = from.getString(COLUMN_LOOKUP_KEY) name = from.getString(COLUMN_DISPLAY_NAME) ?: "" numbers.add(PhoneNumber( + accountType = from.getString(COLUMN_ACCOUNT_TYPE), address = from.getString(COLUMN_NUMBER) ?: "", type = Phone.getTypeLabel(context.resources, from.getInt(COLUMN_TYPE), from.getString(COLUMN_LABEL)).toString() diff --git a/data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt b/data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt index 1461fa9bb..2cf98f1f8 100644 --- a/data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt +++ b/data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt @@ -127,6 +127,9 @@ class QkRealmMigration : RealmMigration { realm.schema.get("Contact") ?.addField("starred", Boolean::class.java, FieldAttribute.REQUIRED) + realm.schema.get("PhoneNumber") + ?.addField("accountType", String::class.java, FieldAttribute.REQUIRED) + version++ } diff --git a/data/src/main/java/com/moez/QKSMS/repository/SyncRepositoryImpl.kt b/data/src/main/java/com/moez/QKSMS/repository/SyncRepositoryImpl.kt index 679f592dc..77bd2f9e4 100644 --- a/data/src/main/java/com/moez/QKSMS/repository/SyncRepositoryImpl.kt +++ b/data/src/main/java/com/moez/QKSMS/repository/SyncRepositoryImpl.kt @@ -37,6 +37,7 @@ import com.moez.QKSMS.model.ContactGroup import com.moez.QKSMS.model.Conversation import com.moez.QKSMS.model.Message import com.moez.QKSMS.model.MmsPart +import com.moez.QKSMS.model.PhoneNumber import com.moez.QKSMS.model.Recipient import com.moez.QKSMS.model.SyncLog import com.moez.QKSMS.util.PhoneNumberUtils @@ -289,10 +290,25 @@ class SyncRepositoryImpl @Inject constructor( ?.map { cursor -> cursorToContact.map(cursor) } ?.groupBy { contact -> contact.lookupKey } ?.map { contacts -> - val allNumbers = contacts.value.map { it.numbers }.flatten() + // Sometimes, contacts providers on the phone will create duplicate phone number entries. This + // commonly happens with Whatsapp. Let's try to detect these duplicate entries and filter them out + val uniqueNumbers = mutableListOf() + contacts.value + .flatMap { it.numbers } + .sortedBy { it.accountType } + .forEach { number -> + val duplicate = uniqueNumbers.any { other -> + number.accountType != other.accountType + && phoneNumberUtils.compare(number.address, other.address) + } + if (!duplicate) { + uniqueNumbers += number + } + } + contacts.value.first().apply { numbers.clear() - numbers.addAll(allNumbers) + numbers.addAll(uniqueNumbers) } } ?: listOf() } diff --git a/domain/src/main/java/com/moez/QKSMS/model/PhoneNumber.kt b/domain/src/main/java/com/moez/QKSMS/model/PhoneNumber.kt index 070fb1306..db02b9ef6 100644 --- a/domain/src/main/java/com/moez/QKSMS/model/PhoneNumber.kt +++ b/domain/src/main/java/com/moez/QKSMS/model/PhoneNumber.kt @@ -21,6 +21,7 @@ package com.moez.QKSMS.model import io.realm.RealmObject open class PhoneNumber( + var accountType: String = "", var address: String = "", var type: String = "" ) : RealmObject() \ No newline at end of file -- GitLab From d73494ba357c55730163e344a99f4659925b3fee Mon Sep 17 00:00:00 2001 From: moezbhatti Date: Thu, 31 Oct 2019 01:28:40 -0400 Subject: [PATCH 017/109] Fix top margin for subtitle --- presentation/src/main/res/layout/contact_list_item.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/presentation/src/main/res/layout/contact_list_item.xml b/presentation/src/main/res/layout/contact_list_item.xml index 0fc94716e..d445cf22c 100644 --- a/presentation/src/main/res/layout/contact_list_item.xml +++ b/presentation/src/main/res/layout/contact_list_item.xml @@ -84,6 +84,7 @@ android:id="@+id/subtitle" android:layout_width="0dp" android:layout_height="wrap_content" + android:layout_marginTop="2dp" android:textColor="?android:attr/textColorTertiary" app:layout_constraintBottom_toTopOf="@id/numbers" app:layout_constraintEnd_toEndOf="parent" -- GitLab From 1d0e5a638a322c0eefaa5066def06f6f672a4bb2 Mon Sep 17 00:00:00 2001 From: moezbhatti Date: Thu, 31 Oct 2019 02:06:53 -0400 Subject: [PATCH 018/109] Create chip model to keep original contact and know which number was selected --- .../QKSMS/feature/compose/ComposeActivity.kt | 10 +-- .../QKSMS/feature/compose/ComposeState.kt | 4 +- .../moez/QKSMS/feature/compose/ComposeView.kt | 4 +- .../QKSMS/feature/compose/ComposeViewModel.kt | 68 +++++++++---------- .../QKSMS/feature/compose/editing/Chip.kt | 26 +++++++ .../feature/compose/editing/ChipsAdapter.kt | 32 +++------ .../feature/compose/editing/ComposeItem.kt | 2 +- .../compose/editing/DetailedChipView.kt | 9 ++- 8 files changed, 82 insertions(+), 73 deletions(-) create mode 100644 presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/Chip.kt 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 6b3a1ab99..ba3fc72e7 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 @@ -56,11 +56,11 @@ import com.moez.QKSMS.common.util.extensions.setBackgroundTint import com.moez.QKSMS.common.util.extensions.setTint import com.moez.QKSMS.common.util.extensions.setVisible import com.moez.QKSMS.common.util.extensions.showKeyboard +import com.moez.QKSMS.feature.compose.editing.Chip import com.moez.QKSMS.feature.compose.editing.ChipsAdapter import com.moez.QKSMS.feature.compose.editing.ComposeItem import com.moez.QKSMS.feature.compose.editing.ComposeItemAdapter import com.moez.QKSMS.model.Attachment -import com.moez.QKSMS.model.Contact import com.uber.autodispose.android.lifecycle.scope import com.uber.autodispose.autoDisposable import dagger.android.AndroidInjection @@ -93,7 +93,7 @@ class ComposeActivity : QkThemedActivity(), ComposeView { override val queryBackspaceIntent: Observable<*> by lazy { search.backspaces } override val queryEditorActionIntent: Observable by lazy { search.editorActions() } override val chipSelectedIntent: Subject by lazy { contactsAdapter.itemSelected } - override val chipDeletedIntent: Subject by lazy { chipsAdapter.chipDeleted } + override val chipDeletedIntent: Subject by lazy { chipsAdapter.chipDeleted } override val menuReadyIntent: Observable = menu.map { Unit } override val optionsItemIntent: Subject = PublishSubject.create() override val sendAsGroupIntent by lazy { sendAsGroupBackground.clicks() } @@ -233,16 +233,16 @@ class ComposeActivity : QkThemedActivity(), ComposeView { 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() - if (chipsAdapter.data.isEmpty() && state.selectedContacts.isNotEmpty()) { + if (chipsAdapter.data.isEmpty() && state.selectedChips.isNotEmpty()) { message.showKeyboard() } - chipsAdapter.data = state.selectedContacts + chipsAdapter.data = state.selectedChips contactsAdapter.data = state.composeItems loading.setVisible(state.loading) - sendAsGroup.setVisible(state.editingMode && state.selectedContacts.size >= 2) + sendAsGroup.setVisible(state.editingMode && state.selectedChips.size >= 2) sendAsGroupSwitch.isChecked = state.sendAsGroup messageList.setVisible(state.sendAsGroup) 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 2502442d9..b2f55b0ee 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 @@ -19,9 +19,9 @@ package com.moez.QKSMS.feature.compose import com.moez.QKSMS.compat.SubscriptionInfoCompat +import com.moez.QKSMS.feature.compose.editing.Chip import com.moez.QKSMS.feature.compose.editing.ComposeItem import com.moez.QKSMS.model.Attachment -import com.moez.QKSMS.model.Contact import com.moez.QKSMS.model.Conversation import com.moez.QKSMS.model.Message import io.realm.RealmResults @@ -32,7 +32,7 @@ data class ComposeState( val searching: Boolean = false, val composeItems: List = ArrayList(), val selectedConversation: Long = 0, - val selectedContacts: List = ArrayList(), + val selectedChips: List = ArrayList(), val sendAsGroup: Boolean = true, val conversationtitle: String = "", val loading: Boolean = false, 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 e08decc69..d7661cf28 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 @@ -22,9 +22,9 @@ import android.net.Uri import androidx.annotation.StringRes import androidx.core.view.inputmethod.InputContentInfoCompat import com.moez.QKSMS.common.base.QkView +import com.moez.QKSMS.feature.compose.editing.Chip import com.moez.QKSMS.feature.compose.editing.ComposeItem import com.moez.QKSMS.model.Attachment -import com.moez.QKSMS.model.Contact import io.reactivex.Observable import io.reactivex.subjects.Subject @@ -35,7 +35,7 @@ interface ComposeView : QkView { val queryBackspaceIntent: Observable<*> val queryEditorActionIntent: Observable val chipSelectedIntent: Subject - val chipDeletedIntent: Subject + val chipDeletedIntent: Subject val menuReadyIntent: Observable val optionsItemIntent: Observable val sendAsGroupIntent: 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 15a20f78b..ce0286773 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 @@ -40,6 +40,7 @@ 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.editing.Chip import com.moez.QKSMS.feature.compose.editing.ComposeItem import com.moez.QKSMS.feature.compose.editing.ComposeItem.* import com.moez.QKSMS.filter.ContactFilter @@ -122,11 +123,11 @@ class ComposeViewModel @Inject constructor( private val attachments: Subject> = BehaviorSubject.createDefault(sharedAttachments) 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 chipsReducer: 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 selectedChips: Subject> = BehaviorSubject.createDefault(listOf()) private val searchResults: Subject> = BehaviorSubject.create() private val searchSelection: Subject = BehaviorSubject.createDefault(-1) private val starredContacts: Observable> by lazy { contactsRepo.getUnmanagedContacts(true) } @@ -137,9 +138,9 @@ class ComposeViewModel @Inject constructor( ?.asObservable() ?: Observable.empty() - val selectedConversation = selectedContacts + val selectedConversation = selectedChips .skipWhile { it.isEmpty() } - .map { contacts -> contacts.map { it.numbers.firstOrNull()?.address ?: "" } } + .map { chips -> chips.map { it.address } } .distinctUntilChanged() .doOnNext { newState { copy(loading = true) } } .observeOn(Schedulers.io()) @@ -181,15 +182,15 @@ class ComposeViewModel @Inject constructor( .subscribe(conversation::onNext) if (address.isNotBlank()) { - selectedContacts.onNext(listOf(Contact(numbers = RealmList(PhoneNumber(address))))) + selectedChips.onNext(listOf(Chip(address))) } - disposables += contactsReducer - .scan(listOf()) { previousState, reducer -> reducer(previousState) } - .doOnNext { contacts -> newState { copy(selectedContacts = contacts) } } + disposables += chipsReducer + .scan(listOf()) { previousState, reducer -> reducer(previousState) } + .doOnNext { chips -> newState { copy(selectedChips = chips) } } .skipUntil(state.filter { state -> state.editingMode }) .takeUntil(state.filter { state -> !state.editingMode }) - .subscribe(selectedContacts::onNext) + .subscribe(selectedChips::onNext) // When the conversation changes, mark read, and update the threadId and the messages for the adapter disposables += conversation @@ -261,8 +262,8 @@ class ComposeViewModel @Inject constructor( // that have already been selected Observables .combineLatest( - view.queryChangedIntent, recents, starredContacts, contactGroups, contacts, selectedContacts - ) { query, recents, starredContacts, contactGroups, contacts, selectedContacts -> + view.queryChangedIntent, recents, starredContacts, contactGroups, contacts, selectedChips + ) { query, recents, starredContacts, contactGroups, contacts, selectedChips -> val composeItems = mutableListOf() if (query.isBlank()) { composeItems += recents.map(::Recent) @@ -281,7 +282,7 @@ class ComposeViewModel @Inject constructor( // cache the result instead of doing it for each contact val normalizedQuery = query.removeAccents() composeItems += starredContacts - .filterNot { contact -> selectedContacts.contains(contact) } + .filterNot { contact -> selectedChips.map { it.contact }.contains(contact) } .filter { contact -> contactFilter.filter(contact, normalizedQuery) } .map(::Starred) @@ -290,7 +291,7 @@ class ComposeViewModel @Inject constructor( .map(::Group) composeItems += contacts - .filterNot { contact -> selectedContacts.contains(contact) } + .filterNot { contact -> selectedChips.map { it.contact }.contains(contact) } .filter { contact -> contactFilter.filter(contact, normalizedQuery) } .map(::Person) } @@ -306,47 +307,42 @@ class ComposeViewModel @Inject constructor( // Backspaces should delete the most recent contact if there's no text input // Close the activity if user presses back view.queryBackspaceIntent - .withLatestFrom(selectedContacts, view.queryChangedIntent) { event, contacts, query -> + .withLatestFrom(selectedChips, view.queryChangedIntent) { event, contacts, query -> if (contacts.isNotEmpty() && query.isEmpty()) { - contactsReducer.onNext { it.dropLast(1) } + chipsReducer.onNext { it.dropLast(1) } } } .autoDisposable(view.scope()) .subscribe() - // Enter the first contact suggestion if the enter button is pressed + // Enter the first contact suggestion if the enter button is pressed, and enter contacts that are selected view.queryEditorActionIntent .filter { actionId -> actionId == EditorInfo.IME_ACTION_DONE } .withLatestFrom(state) { _, state -> state } + .mapNotNull { state -> state.composeItems.firstOrNull() } + .mergeWith(view.chipSelectedIntent) .autoDisposable(view.scope()) - .subscribe { state -> + .subscribe { composeItem -> newState { copy(searching = false) } - state.composeItems.firstOrNull()?.let { composeItem -> - contactsReducer.onNext { contacts -> contacts + composeItem.getContacts() } + chipsReducer.onNext { chips -> + chips + composeItem.getContacts().map { Chip(it.numbers.first()?.address.orEmpty(), it) } } } // Update the list of selected contacts when a new contact is selected or an existing one is deselected - Observable.merge( - view.chipDeletedIntent.doOnNext { contact -> - contactsReducer.onNext { contacts -> + view.chipDeletedIntent + .skipUntil(state.filter { state -> state.editingMode }) + .takeUntil(state.filter { state -> !state.editingMode }) + .autoDisposable(view.scope()) + .subscribe { contact -> + chipsReducer.onNext { contacts -> val result = contacts.filterNot { it == contact } if (result.isEmpty()) { newState { copy(searching = true) } } result } - }, - view.chipSelectedIntent.doOnNext { composeItem -> - newState { copy(searching = false) } - contactsReducer.onNext { contacts -> - contacts.toMutableList().apply { addAll(composeItem.getContacts()) } - } - }) - .skipUntil(state.filter { state -> state.editingMode }) - .takeUntil(state.filter { state -> !state.editingMode }) - .autoDisposable(view.scope()) - .subscribe() + } // When the menu is loaded, trigger a new state so that the menu options can be rendered correctly view.menuReadyIntent @@ -683,12 +679,12 @@ class ComposeViewModel @Inject constructor( .filter { permissionManager.hasSendSms().also { if (!it) view.requestSmsPermission() } } .withLatestFrom(view.textChangedIntent) { _, body -> body } .map { body -> body.toString() } - .withLatestFrom(state, attachments, conversation, selectedContacts) { body, state, attachments, - conversation, contacts -> + .withLatestFrom(state, attachments, conversation, selectedChips) { body, state, attachments, + conversation, chips -> val subId = state.subscription?.subscriptionId ?: -1 val addresses = when (conversation.recipients.isNotEmpty()) { true -> conversation.recipients.map { it.address } - false -> contacts.mapNotNull { it.numbers.firstOrNull()?.address } + false -> chips.map { chip -> chip.address } } val delay = when (prefs.sendDelay.get()) { Preferences.SEND_DELAY_SHORT -> 3000 diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/Chip.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/Chip.kt new file mode 100644 index 000000000..31846fbdb --- /dev/null +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/Chip.kt @@ -0,0 +1,26 @@ +/* + * 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.editing + +import com.moez.QKSMS.model.Contact + +data class Chip( + val address: String, + val contact: Contact? = null +) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ChipsAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ChipsAdapter.kt index ca5b5364a..f0dc7935f 100755 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ChipsAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ChipsAdapter.kt @@ -27,48 +27,40 @@ 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.dpToPx -import com.moez.QKSMS.model.Contact import io.reactivex.subjects.PublishSubject import kotlinx.android.synthetic.main.contact_chip.view.* import javax.inject.Inject -class ChipsAdapter @Inject constructor() : QkAdapter() { +class ChipsAdapter @Inject constructor() : QkAdapter() { var view: RecyclerView? = null - val chipDeleted: PublishSubject = PublishSubject.create() + val chipDeleted: PublishSubject = PublishSubject.create() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkViewHolder { val inflater = LayoutInflater.from(parent.context) val view = inflater.inflate(R.layout.contact_chip, parent, false) return QkViewHolder(view).apply { view.setOnClickListener { - val contact = getItem(adapterPosition) - showDetailedChip(view.context, contact) + val chip = getItem(adapterPosition) + showDetailedChip(view.context, chip) } } } override fun onBindViewHolder(holder: QkViewHolder, position: Int) { - val contact = getItem(position) + val chip = getItem(position) val view = holder.containerView - view.avatar.setContact(contact) - - // If the contact's name is empty, try to display a phone number instead - // The contacts provided here should only have one number - view.name.text = if (contact.name.isNotBlank()) { - contact.name - } else { - contact.numbers.firstOrNull { it.address.isNotBlank() }?.address ?: "" - } + view.avatar.setContact(chip.contact, chip.address) + view.name.text = chip.contact?.name?.takeIf { it.isNotBlank() } ?: chip.address } /** * The [context] has to come from a view, because we're inflating a view that used themed attrs */ - private fun showDetailedChip(context: Context, contact: Contact) { + private fun showDetailedChip(context: Context, chip: Chip) { val detailedChipView = DetailedChipView(context) - detailedChipView.setContact(contact) + detailedChipView.setChip(chip) val rootView = view?.rootView as ViewGroup @@ -83,12 +75,8 @@ class ChipsAdapter @Inject constructor() : QkAdapter() { detailedChipView.show() detailedChipView.setOnDeleteListener { - chipDeleted.onNext(contact) + chipDeleted.onNext(chip) detailedChipView.hide() } } - - override fun areItemsTheSame(old: Contact, new: Contact): Boolean { - return old.lookupKey == new.lookupKey - } } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ComposeItem.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ComposeItem.kt index 894daa1e2..421418bf8 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ComposeItem.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ComposeItem.kt @@ -34,7 +34,7 @@ sealed class ComposeItem { data class Recent(val value: Conversation) : ComposeItem() { override fun getContacts(): List = value.recipients.map { recipient -> - Contact(numbers = RealmList(PhoneNumber(address = recipient.address))) + recipient.contact ?: Contact(numbers = RealmList(PhoneNumber(address = recipient.address))) } } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/DetailedChipView.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/DetailedChipView.kt index 731d9c9da..262d0ab6b 100755 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/DetailedChipView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/DetailedChipView.kt @@ -27,7 +27,6 @@ import com.moez.QKSMS.common.util.Colors import com.moez.QKSMS.common.util.extensions.setBackgroundTint import com.moez.QKSMS.common.util.extensions.setTint import com.moez.QKSMS.injection.appComponent -import com.moez.QKSMS.model.Contact import kotlinx.android.synthetic.main.contact_chip_detailed.view.* import javax.inject.Inject @@ -54,10 +53,10 @@ class DetailedChipView(context: Context) : RelativeLayout(context) { } } - fun setContact(contact: Contact) { - avatar.setContact(contact) - name.text = contact.name - info.text = contact.numbers.joinToString(", ") { it.address } + fun setChip(chip: Chip) { + avatar.setContact(chip.contact, chip.address) + name.text = chip.contact?.name?.takeIf { it.isNotBlank() } ?: chip.address + info.text = chip.address } fun show() { -- GitLab From ddc672d74d0d291c335dd0eaa3199f1ec2e8fab7 Mon Sep 17 00:00:00 2001 From: moezbhatti Date: Thu, 7 Nov 2019 02:44:19 -0500 Subject: [PATCH 019/109] Build phone number picker --- data/build.gradle | 1 - .../moez/QKSMS/mapper/CursorToContactImpl.kt | 19 ++-- .../moez/QKSMS/migration/QkRealmMigration.kt | 2 + .../QKSMS/repository/ContactRepositoryImpl.kt | 16 +++ .../QKSMS/repository/SyncRepositoryImpl.kt | 11 +- domain/build.gradle | 1 - .../QKSMS/interactor/SetDefaultPhoneNumber.kt | 38 +++++++ .../main/java/com/moez/QKSMS/model/Contact.kt | 6 +- .../java/com/moez/QKSMS/model/PhoneNumber.kt | 7 +- .../QKSMS/repository/ContactRepository.kt | 4 +- .../com/moez/QKSMS/common/widget/QkDialog.kt | 105 ++++++++++++++++++ .../QKSMS/feature/compose/ComposeActivity.kt | 39 ++++++- .../QKSMS/feature/compose/ComposeState.kt | 2 + .../moez/QKSMS/feature/compose/ComposeView.kt | 7 +- .../QKSMS/feature/compose/ComposeViewModel.kt | 54 +++++++-- .../compose/editing/ComposeItemAdapter.kt | 10 +- .../compose/editing/PhoneNumberAction.kt | 25 +++++ .../editing/PhoneNumberPickerAdapter.kt | 77 +++++++++++++ .../res/layout/phone_number_list_item.xml | 33 ++++++ .../src/main/res/layout/qk_dialog.xml | 104 +++++++++++++++++ presentation/src/main/res/values/strings.xml | 4 + 21 files changed, 535 insertions(+), 30 deletions(-) create mode 100644 domain/src/main/java/com/moez/QKSMS/interactor/SetDefaultPhoneNumber.kt create mode 100644 presentation/src/main/java/com/moez/QKSMS/common/widget/QkDialog.kt create mode 100644 presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/PhoneNumberAction.kt create mode 100644 presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/PhoneNumberPickerAdapter.kt create mode 100644 presentation/src/main/res/layout/phone_number_list_item.xml create mode 100644 presentation/src/main/res/layout/qk_dialog.xml diff --git a/data/build.gradle b/data/build.gradle index 215e8ce5f..bb2f87704 100644 --- a/data/build.gradle +++ b/data/build.gradle @@ -83,7 +83,6 @@ dependencies { // coroutines implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-reactive:$coroutines_version" diff --git a/data/src/main/java/com/moez/QKSMS/mapper/CursorToContactImpl.kt b/data/src/main/java/com/moez/QKSMS/mapper/CursorToContactImpl.kt index 62f1602d3..23ba4e5fb 100644 --- a/data/src/main/java/com/moez/QKSMS/mapper/CursorToContactImpl.kt +++ b/data/src/main/java/com/moez/QKSMS/mapper/CursorToContactImpl.kt @@ -34,6 +34,7 @@ class CursorToContactImpl @Inject constructor( companion object { val URI = Phone.CONTENT_URI val PROJECTION = arrayOf( + Phone._ID, Phone.LOOKUP_KEY, Phone.ACCOUNT_TYPE_AND_DATA_SET, Phone.NUMBER, @@ -44,20 +45,22 @@ class CursorToContactImpl @Inject constructor( Phone.CONTACT_LAST_UPDATED_TIMESTAMP ) - const val COLUMN_LOOKUP_KEY = 0 - const val COLUMN_ACCOUNT_TYPE = 1 - const val COLUMN_NUMBER = 2 - const val COLUMN_TYPE = 3 - const val COLUMN_LABEL = 4 - const val COLUMN_DISPLAY_NAME = 5 - const val COLUMN_STARRED = 6 - const val CONTACT_LAST_UPDATED = 7 + const val COLUMN_ID = 0 + const val COLUMN_LOOKUP_KEY = 1 + const val COLUMN_ACCOUNT_TYPE = 2 + const val COLUMN_NUMBER = 3 + const val COLUMN_TYPE = 4 + const val COLUMN_LABEL = 5 + const val COLUMN_DISPLAY_NAME = 6 + const val COLUMN_STARRED = 7 + const val CONTACT_LAST_UPDATED = 8 } override fun map(from: Cursor) = Contact().apply { lookupKey = from.getString(COLUMN_LOOKUP_KEY) name = from.getString(COLUMN_DISPLAY_NAME) ?: "" numbers.add(PhoneNumber( + id = from.getLong(COLUMN_ID), accountType = from.getString(COLUMN_ACCOUNT_TYPE), address = from.getString(COLUMN_NUMBER) ?: "", type = Phone.getTypeLabel(context.resources, from.getInt(COLUMN_TYPE), diff --git a/data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt b/data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt index 2cf98f1f8..06a10c53f 100644 --- a/data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt +++ b/data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt @@ -128,7 +128,9 @@ class QkRealmMigration : RealmMigration { ?.addField("starred", Boolean::class.java, FieldAttribute.REQUIRED) realm.schema.get("PhoneNumber") + ?.addField("id", Long::class.java, FieldAttribute.PRIMARY_KEY, FieldAttribute.REQUIRED) ?.addField("accountType", String::class.java, FieldAttribute.REQUIRED) + ?.addField("isDefault", Boolean::class.java, FieldAttribute.REQUIRED) version++ } 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 842a423c1..268913a68 100644 --- a/data/src/main/java/com/moez/QKSMS/repository/ContactRepositoryImpl.kt +++ b/data/src/main/java/com/moez/QKSMS/repository/ContactRepositoryImpl.kt @@ -137,4 +137,20 @@ class ContactRepositoryImpl @Inject constructor( .observeOn(Schedulers.io()) } + override fun setDefaultPhoneNumber(lookupKey: String, phoneNumberId: Long) { + Realm.getDefaultInstance().use { realm -> + realm.refresh() + val contact = realm.where(Contact::class.java) + .equalTo("lookupKey", lookupKey) + .findFirst() + ?: return + + realm.executeTransaction { + contact.numbers.forEach { number -> + number.isDefault = number.id == phoneNumberId + } + } + } + } + } diff --git a/data/src/main/java/com/moez/QKSMS/repository/SyncRepositoryImpl.kt b/data/src/main/java/com/moez/QKSMS/repository/SyncRepositoryImpl.kt index 77bd2f9e4..3748bdcb9 100644 --- a/data/src/main/java/com/moez/QKSMS/repository/SyncRepositoryImpl.kt +++ b/data/src/main/java/com/moez/QKSMS/repository/SyncRepositoryImpl.kt @@ -243,7 +243,7 @@ class SyncRepositoryImpl @Inject constructor( realm.delete(Contact::class.java) realm.delete(ContactGroup::class.java) - contacts = realm.copyToRealm(contacts) + contacts = realm.copyToRealmOrUpdate(contacts) realm.insertOrUpdate(getContactGroups(contacts)) // Update all the recipients with the new contacts @@ -286,6 +286,13 @@ class SyncRepositoryImpl @Inject constructor( } private fun getContacts(): List { + val defaultNumberIds = Realm.getDefaultInstance().use { realm -> + realm.where(PhoneNumber::class.java) + .equalTo("isDefault", true) + .findAll() + .map { number -> number.id } + } + return cursorToContact.getContactsCursor() ?.map { cursor -> cursorToContact.map(cursor) } ?.groupBy { contact -> contact.lookupKey } @@ -297,10 +304,12 @@ class SyncRepositoryImpl @Inject constructor( .flatMap { it.numbers } .sortedBy { it.accountType } .forEach { number -> + number.isDefault = defaultNumberIds.any { id -> id == number.id } val duplicate = uniqueNumbers.any { other -> number.accountType != other.accountType && phoneNumberUtils.compare(number.address, other.address) } + if (!duplicate) { uniqueNumbers += number } diff --git a/domain/build.gradle b/domain/build.gradle index fe7c5c19e..e428250e6 100644 --- a/domain/build.gradle +++ b/domain/build.gradle @@ -63,7 +63,6 @@ dependencies { // coroutines implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-reactive:$coroutines_version" diff --git a/domain/src/main/java/com/moez/QKSMS/interactor/SetDefaultPhoneNumber.kt b/domain/src/main/java/com/moez/QKSMS/interactor/SetDefaultPhoneNumber.kt new file mode 100644 index 000000000..639c15426 --- /dev/null +++ b/domain/src/main/java/com/moez/QKSMS/interactor/SetDefaultPhoneNumber.kt @@ -0,0 +1,38 @@ +/* + * 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.interactor + +import com.moez.QKSMS.repository.ContactRepository +import io.reactivex.Flowable +import javax.inject.Inject + +class SetDefaultPhoneNumber @Inject constructor( + private val contactRepo: ContactRepository +) : Interactor() { + + data class Params(val lookupKey: String, val phoneNumberId: Long) + + override fun buildObservable(params: Params): Flowable<*> { + return Flowable.just(params) + .doOnNext { (lookupKey, phoneNumberId) -> + contactRepo.setDefaultPhoneNumber(lookupKey, phoneNumberId) + } + } + +} diff --git a/domain/src/main/java/com/moez/QKSMS/model/Contact.kt b/domain/src/main/java/com/moez/QKSMS/model/Contact.kt index dd325dacf..7e8cd27c9 100644 --- a/domain/src/main/java/com/moez/QKSMS/model/Contact.kt +++ b/domain/src/main/java/com/moez/QKSMS/model/Contact.kt @@ -28,4 +28,8 @@ open class Contact( var name: String = "", var starred: Boolean = false, var lastUpdate: Long = 0 -) : RealmObject() \ No newline at end of file +) : RealmObject() { + + fun getDefaultNumber(): PhoneNumber? = numbers.find { number -> number.isDefault } + +} diff --git a/domain/src/main/java/com/moez/QKSMS/model/PhoneNumber.kt b/domain/src/main/java/com/moez/QKSMS/model/PhoneNumber.kt index db02b9ef6..2669049df 100644 --- a/domain/src/main/java/com/moez/QKSMS/model/PhoneNumber.kt +++ b/domain/src/main/java/com/moez/QKSMS/model/PhoneNumber.kt @@ -19,9 +19,12 @@ package com.moez.QKSMS.model import io.realm.RealmObject +import io.realm.annotations.PrimaryKey open class PhoneNumber( + @PrimaryKey var id: Long = 0, var accountType: String = "", var address: String = "", - var type: String = "" -) : RealmObject() \ No newline at end of file + var type: String = "", + var isDefault: Boolean = false +) : RealmObject() 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 0bc8471bd..4242afb52 100644 --- a/domain/src/main/java/com/moez/QKSMS/repository/ContactRepository.kt +++ b/domain/src/main/java/com/moez/QKSMS/repository/ContactRepository.kt @@ -35,4 +35,6 @@ interface ContactRepository { fun getUnmanagedContactGroups(): Observable> -} \ No newline at end of file + fun setDefaultPhoneNumber(lookupKey: String, phoneNumberId: Long) + +} diff --git a/presentation/src/main/java/com/moez/QKSMS/common/widget/QkDialog.kt b/presentation/src/main/java/com/moez/QKSMS/common/widget/QkDialog.kt new file mode 100644 index 000000000..f275c8a0c --- /dev/null +++ b/presentation/src/main/java/com/moez/QKSMS/common/widget/QkDialog.kt @@ -0,0 +1,105 @@ +/* + * 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.common.widget + +import android.app.Activity +import android.view.LayoutInflater +import androidx.annotation.StringRes +import androidx.appcompat.app.AlertDialog +import androidx.core.view.isVisible +import com.moez.QKSMS.R +import com.moez.QKSMS.common.base.QkAdapter +import kotlinx.android.synthetic.main.qk_dialog.view.* + +class QkDialog(private val context: Activity) : AlertDialog(context) { + + private val view = LayoutInflater.from(context).inflate(R.layout.qk_dialog, null) + + @StringRes + var titleRes: Int? = null + set(value) { + field = value + title = value?.let(context::getString) + } + + var title: String? = null + set(value) { + field = value + view.title.text = value + view.title.isVisible = !value.isNullOrBlank() + } + + @StringRes + var subtitleRes: Int? = null + set(value) { + field = value + subtitle = value?.let(context::getString) + } + + var subtitle: String? = null + set(value) { + field = value + view.subtitle.text = value + view.subtitle.isVisible = !value.isNullOrBlank() + } + + var adapter: QkAdapter<*>? = null + set(value) { + field = value + view.list.isVisible = value != null + view.list.adapter = value + } + + var positiveButtonListener: (() -> Unit)? = null + + @StringRes + var positiveButton: Int? = null + set(value) { + field = value + value?.run(view.positiveButton::setText) + view.positiveButton.isVisible = value != null + view.positiveButton.setOnClickListener { + positiveButtonListener?.invoke() ?: dismiss() + } + } + + var negativeButtonListener: (() -> Unit)? = null + + @StringRes + var negativeButton: Int? = null + set(value) { + field = value + value?.run(view.negativeButton::setText) + view.negativeButton.isVisible = value != null + view.negativeButton.setOnClickListener { + negativeButtonListener?.invoke() ?: dismiss() + } + } + + var cancelListener: (() -> Unit)? = null + set(value) { + field = value + setOnCancelListener { value?.invoke() } + } + + init { + setView(view) + } + +} 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 ba3fc72e7..d3f7d97c5 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 @@ -56,10 +56,14 @@ import com.moez.QKSMS.common.util.extensions.setBackgroundTint import com.moez.QKSMS.common.util.extensions.setTint import com.moez.QKSMS.common.util.extensions.setVisible import com.moez.QKSMS.common.util.extensions.showKeyboard +import com.moez.QKSMS.common.widget.QkDialog +import com.moez.QKSMS.extensions.Optional import com.moez.QKSMS.feature.compose.editing.Chip import com.moez.QKSMS.feature.compose.editing.ChipsAdapter import com.moez.QKSMS.feature.compose.editing.ComposeItem import com.moez.QKSMS.feature.compose.editing.ComposeItemAdapter +import com.moez.QKSMS.feature.compose.editing.PhoneNumberAction +import com.moez.QKSMS.feature.compose.editing.PhoneNumberPickerAdapter import com.moez.QKSMS.model.Attachment import com.uber.autodispose.android.lifecycle.scope import com.uber.autodispose.autoDisposable @@ -86,13 +90,17 @@ class ComposeActivity : QkThemedActivity(), ComposeView { @Inject lateinit var dateFormatter: DateFormatter @Inject lateinit var messageAdapter: MessagesAdapter @Inject lateinit var navigator: Navigator + @Inject lateinit var phoneNumberAdapter: PhoneNumberPickerAdapter @Inject lateinit var viewModelFactory: ViewModelProvider.Factory override val activityVisibleIntent: Subject = PublishSubject.create() override val queryChangedIntent: Observable by lazy { search.textChanges() } override val queryBackspaceIntent: Observable<*> by lazy { search.backspaces } override val queryEditorActionIntent: Observable by lazy { search.editorActions() } - override val chipSelectedIntent: Subject by lazy { contactsAdapter.itemSelected } + override val composeItemPressedIntent: Subject by lazy { contactsAdapter.clicks } + override val composeItemLongPressedIntent: Subject by lazy { contactsAdapter.longClicks } + override val phoneNumberSelectedIntent: Subject> by lazy { phoneNumberAdapter.selectedItemChanges } + override val phoneNumberActionIntent: Subject = PublishSubject.create() override val chipDeletedIntent: Subject by lazy { chipsAdapter.chipDeleted } override val menuReadyIntent: Observable = menu.map { Unit } override val optionsItemIntent: Subject = PublishSubject.create() @@ -119,6 +127,18 @@ class ComposeActivity : QkThemedActivity(), ComposeView { override val viewQksmsPlusIntent: Subject = PublishSubject.create() override val backPressedIntent: Subject = PublishSubject.create() + private val phoneNumberDialog by lazy { + QkDialog(this).apply { + titleRes = R.string.compose_number_picker_title + adapter = phoneNumberAdapter + positiveButton = R.string.compose_number_picker_always + positiveButtonListener = { phoneNumberActionIntent.onNext(PhoneNumberAction.ALWAYS) } + negativeButton = R.string.compose_number_picker_once + negativeButtonListener = { phoneNumberActionIntent.onNext(PhoneNumberAction.JUST_ONCE) } + cancelListener = { phoneNumberActionIntent.onNext(PhoneNumberAction.CANCEL) } + } + } + private val viewModel by lazy { ViewModelProviders.of(this, viewModelFactory)[ComposeViewModel::class.java] } private var cameraDestination: Uri? = null @@ -223,8 +243,10 @@ class ComposeActivity : QkThemedActivity(), ComposeView { if (state.editingMode && contacts.adapter == null) contacts.adapter = contactsAdapter toolbar.menu.findItem(R.id.add)?.isVisible = state.editingMode && !state.searching - 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 && state.query.isEmpty() + 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 + && state.query.isEmpty() toolbar.menu.findItem(R.id.copy)?.isVisible = !state.editingMode && state.selectedMessages == 1 toolbar.menu.findItem(R.id.details)?.isVisible = !state.editingMode && state.selectedMessages == 1 toolbar.menu.findItem(R.id.delete)?.isVisible = !state.editingMode && state.selectedMessages > 0 @@ -240,6 +262,14 @@ class ComposeActivity : QkThemedActivity(), ComposeView { chipsAdapter.data = state.selectedChips contactsAdapter.data = state.composeItems + if (state.selectedContact != null && !phoneNumberDialog.isShowing) { + phoneNumberAdapter.data = state.selectedContact.numbers + phoneNumberDialog.subtitle = state.selectedContact.name + phoneNumberDialog.show() + } else if (state.selectedContact == null && phoneNumberDialog.isShowing) { + phoneNumberDialog.dismiss() + } + loading.setVisible(state.loading) sendAsGroup.setVisible(state.editingMode && state.selectedChips.size >= 2) @@ -303,7 +333,8 @@ class ComposeActivity : QkThemedActivity(), ComposeView { calendar.set(Calendar.HOUR_OF_DAY, hour) calendar.set(Calendar.MINUTE, minute) scheduleSelectedIntent.onNext(calendar.timeInMillis) - }, calendar.get(Calendar.HOUR_OF_DAY), calendar.get(Calendar.MINUTE), DateFormat.is24HourFormat(this)).show() + }, calendar.get(Calendar.HOUR_OF_DAY), calendar.get(Calendar.MINUTE), DateFormat.is24HourFormat(this)) + .show() }, calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH)).show() } 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 b2f55b0ee..f7b7e7b88 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 @@ -22,6 +22,7 @@ import com.moez.QKSMS.compat.SubscriptionInfoCompat import com.moez.QKSMS.feature.compose.editing.Chip import com.moez.QKSMS.feature.compose.editing.ComposeItem import com.moez.QKSMS.model.Attachment +import com.moez.QKSMS.model.Contact import com.moez.QKSMS.model.Conversation import com.moez.QKSMS.model.Message import io.realm.RealmResults @@ -32,6 +33,7 @@ data class ComposeState( val searching: Boolean = false, val composeItems: List = ArrayList(), val selectedConversation: Long = 0, + val selectedContact: Contact? = null, // For phone number picker val selectedChips: List = ArrayList(), val sendAsGroup: Boolean = true, val conversationtitle: String = "", 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 d7661cf28..6c3b73e2e 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 @@ -22,8 +22,10 @@ import android.net.Uri import androidx.annotation.StringRes import androidx.core.view.inputmethod.InputContentInfoCompat import com.moez.QKSMS.common.base.QkView +import com.moez.QKSMS.extensions.Optional import com.moez.QKSMS.feature.compose.editing.Chip import com.moez.QKSMS.feature.compose.editing.ComposeItem +import com.moez.QKSMS.feature.compose.editing.PhoneNumberAction import com.moez.QKSMS.model.Attachment import io.reactivex.Observable import io.reactivex.subjects.Subject @@ -34,7 +36,10 @@ interface ComposeView : QkView { val queryChangedIntent: Observable val queryBackspaceIntent: Observable<*> val queryEditorActionIntent: Observable - val chipSelectedIntent: Subject + val composeItemPressedIntent: Subject + val composeItemLongPressedIntent: Subject + val phoneNumberSelectedIntent: Subject> + val phoneNumberActionIntent: 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 ce0286773..f76bd945d 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 @@ -30,6 +30,7 @@ import androidx.annotation.RequiresApi import com.moez.QKSMS.R import com.moez.QKSMS.common.Navigator import com.moez.QKSMS.common.base.QkViewModel +import com.moez.QKSMS.common.util.BillingManager import com.moez.QKSMS.common.util.ClipboardUtils import com.moez.QKSMS.common.util.MessageDetailsFormatter import com.moez.QKSMS.common.util.extensions.makeToast @@ -43,10 +44,9 @@ import com.moez.QKSMS.extensions.removeAccents import com.moez.QKSMS.feature.compose.editing.Chip import com.moez.QKSMS.feature.compose.editing.ComposeItem import com.moez.QKSMS.feature.compose.editing.ComposeItem.* +import com.moez.QKSMS.feature.compose.editing.PhoneNumberAction import com.moez.QKSMS.filter.ContactFilter import com.moez.QKSMS.interactor.* -import com.moez.QKSMS.manager.ActiveConversationManager -import com.moez.QKSMS.manager.PermissionManager import com.moez.QKSMS.model.* import com.moez.QKSMS.filter.ContactGroupFilter import com.moez.QKSMS.interactor.AddScheduledMessage @@ -56,6 +56,9 @@ import com.moez.QKSMS.interactor.DeleteMessages import com.moez.QKSMS.interactor.MarkRead import com.moez.QKSMS.interactor.RetrySending import com.moez.QKSMS.interactor.SendMessage +import com.moez.QKSMS.interactor.SetDefaultPhoneNumber +import com.moez.QKSMS.manager.ActiveConversationManager +import com.moez.QKSMS.manager.PermissionManager import com.moez.QKSMS.model.Attachment import com.moez.QKSMS.model.Attachments import com.moez.QKSMS.model.Contact @@ -82,6 +85,8 @@ import io.reactivex.subjects.BehaviorSubject import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.Subject import io.realm.RealmList +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.rx2.awaitFirst import timber.log.Timber import java.util.* import javax.inject.Inject @@ -96,6 +101,7 @@ class ComposeViewModel @Inject constructor( private val context: Context, private val activeConversationManager: ActiveConversationManager, private val addScheduledMessage: AddScheduledMessage, + private val billingManager: BillingManager, private val cancelMessage: CancelDelayedMessage, private val contactFilter: ContactFilter, private val contactGroupFilter: ContactGroupFilter, @@ -111,6 +117,7 @@ class ComposeViewModel @Inject constructor( private val prefs: Preferences, private val retrySending: RetrySending, private val sendMessage: SendMessage, + private val setDefaultPhoneNumber: SetDefaultPhoneNumber, private val subscriptionManager: SubscriptionManagerCompat, private val syncContacts: ContactSync ) : QkViewModel(ComposeState( @@ -315,18 +322,49 @@ class ComposeViewModel @Inject constructor( .autoDisposable(view.scope()) .subscribe() - // Enter the first contact suggestion if the enter button is pressed, and enter contacts that are selected + // Listen for ComposeItems being selected, and then send them off to the number picker dialog in case + // the user needs to select a phone number view.queryEditorActionIntent .filter { actionId -> actionId == EditorInfo.IME_ACTION_DONE } .withLatestFrom(state) { _, state -> state } .mapNotNull { state -> state.composeItems.firstOrNull() } - .mergeWith(view.chipSelectedIntent) + .mergeWith(view.composeItemPressedIntent) + .map { composeItem -> composeItem to false } + .mergeWith(view.composeItemLongPressedIntent.map { composeItem -> composeItem to true }) + .observeOn(Schedulers.io()) + .skipUntil(state.filter { state -> state.editingMode }) + .takeUntil(state.filter { state -> !state.editingMode }) .autoDisposable(view.scope()) - .subscribe { composeItem -> - newState { copy(searching = false) } - chipsReducer.onNext { chips -> - chips + composeItem.getContacts().map { Chip(it.numbers.first()?.address.orEmpty(), it) } + .subscribe { (composeItem, force) -> + val contacts = composeItem.getContacts() + val newChips = contacts.map { contact -> + if (contact.numbers.size == 1 || contact.getDefaultNumber() != null && !force) { + val number = contact.getDefaultNumber() ?: contact.numbers[0]!! + Chip(number.address, contact) + } else { + runBlocking { + newState { copy(selectedContact = contact) } + val action = view.phoneNumberActionIntent.awaitFirst() + newState { copy(selectedContact = null) } + val numberId = view.phoneNumberSelectedIntent.awaitFirst().value + val number = contact.numbers.find { number -> number.id == numberId } + + if (action == PhoneNumberAction.CANCEL || number == null) { + return@runBlocking null + } + + if (action == PhoneNumberAction.ALWAYS) { + val params = SetDefaultPhoneNumber.Params(contact.lookupKey, number.id) + setDefaultPhoneNumber.execute(params) + } + + Chip(number.address, contact) + } ?: return@subscribe + } } + + newState { copy(searching = false) } + chipsReducer.onNext { chips -> chips + newChips } } // Update the list of selected contacts when a new contact is selected or an existing one is deselected diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ComposeItemAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ComposeItemAdapter.kt index 8a4a4dcbe..b411b2143 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ComposeItemAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ComposeItemAdapter.kt @@ -40,7 +40,8 @@ import javax.inject.Inject class ComposeItemAdapter @Inject constructor(private val colors: Colors) : QkAdapter() { - val itemSelected: Subject = PublishSubject.create() + val clicks: Subject = PublishSubject.create() + val longClicks: Subject = PublishSubject.create() private val numbersViewPool = RecyclerView.RecycledViewPool() @@ -57,7 +58,12 @@ class ComposeItemAdapter @Inject constructor(private val colors: Colors) : QkAda return QkViewHolder(view).apply { view.setOnClickListener { val item = getItem(adapterPosition) - itemSelected.onNext(item) + clicks.onNext(item) + } + view.setOnLongClickListener { + val item = getItem(adapterPosition) + longClicks.onNext(item) + true } } } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/PhoneNumberAction.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/PhoneNumberAction.kt new file mode 100644 index 000000000..7464cefd3 --- /dev/null +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/PhoneNumberAction.kt @@ -0,0 +1,25 @@ +/* + * 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.editing + +enum class PhoneNumberAction { + CANCEL, + JUST_ONCE, + ALWAYS +} diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/PhoneNumberPickerAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/PhoneNumberPickerAdapter.kt new file mode 100644 index 000000000..3464241f7 --- /dev/null +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/PhoneNumberPickerAdapter.kt @@ -0,0 +1,77 @@ +/* + * 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.editing + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import com.moez.QKSMS.R +import com.moez.QKSMS.common.base.QkAdapter +import com.moez.QKSMS.common.base.QkViewHolder +import com.moez.QKSMS.extensions.Optional +import com.moez.QKSMS.model.PhoneNumber +import io.reactivex.subjects.BehaviorSubject +import io.reactivex.subjects.Subject +import kotlinx.android.synthetic.main.phone_number_list_item.view.* +import kotlinx.android.synthetic.main.radio_preference_view.view.* +import javax.inject.Inject + +class PhoneNumberPickerAdapter @Inject constructor( + private val context: Context +) : QkAdapter() { + + val selectedItemChanges: Subject> = BehaviorSubject.create() + + private var selectedItem: Long? = null + set(value) { + data.indexOfFirst { number -> number.id == field }.takeIf { it != -1 }?.run(::notifyItemChanged) + field = value + data.indexOfFirst { number -> number.id == field }.takeIf { it != -1 }?.run(::notifyItemChanged) + selectedItemChanges.onNext(Optional(value)) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkViewHolder { + val inflater = LayoutInflater.from(parent.context) + val view = inflater.inflate(R.layout.phone_number_list_item, parent, false) + return QkViewHolder(view).apply { + view.setOnClickListener { + val phoneNumber = getItem(adapterPosition) + selectedItem = phoneNumber.id + } + } + } + + override fun onBindViewHolder(holder: QkViewHolder, position: Int) { + val phoneNumber = getItem(position) + val view = holder.itemView + + view.number.radioButton.isChecked = phoneNumber.id == selectedItem + view.number.titleView.text = phoneNumber.address + view.number.summaryView.text = when (phoneNumber.isDefault) { + true -> context.getString(R.string.compose_number_picker_default, phoneNumber.type) + false -> phoneNumber.type + } + } + + override fun onDatasetChanged() { + super.onDatasetChanged() + selectedItem = data.find { number -> number.isDefault }?.id ?: data.firstOrNull()?.id + } + +} diff --git a/presentation/src/main/res/layout/phone_number_list_item.xml b/presentation/src/main/res/layout/phone_number_list_item.xml new file mode 100644 index 000000000..c5a850483 --- /dev/null +++ b/presentation/src/main/res/layout/phone_number_list_item.xml @@ -0,0 +1,33 @@ + + + + + + + diff --git a/presentation/src/main/res/layout/qk_dialog.xml b/presentation/src/main/res/layout/qk_dialog.xml new file mode 100644 index 000000000..f38decc29 --- /dev/null +++ b/presentation/src/main/res/layout/qk_dialog.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index c95ee6e60..e5a9ded6e 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -90,6 +90,10 @@ Delete + Choose a phone number + %s ∙ Default + Just once + Always %d selected %1$d of %2$d results Send as group message -- GitLab From 0428ea48eb0b94d6ab9888d1c587d75b268bca29 Mon Sep 17 00:00:00 2001 From: moezbhatti Date: Tue, 12 Nov 2019 23:27:31 -0500 Subject: [PATCH 020/109] Merge issues --- .../com/moez/QKSMS/repository/ConversationRepositoryImpl.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 988b5ce4a..24935ee6b 100644 --- a/data/src/main/java/com/moez/QKSMS/repository/ConversationRepositoryImpl.kt +++ b/data/src/main/java/com/moez/QKSMS/repository/ConversationRepositoryImpl.kt @@ -190,9 +190,9 @@ class ConversationRepositoryImpl @Inject constructor( override fun getUnmanagedConversations(): Observable> { val realm = Realm.getDefaultInstance() return realm.where(Conversation::class.java) - .sort("date", Sort.DESCENDING) + .sort("lastMessage.date", Sort.DESCENDING) .notEqualTo("id", 0L) - .greaterThan("count", 0) + .isNotNull("lastMessage") .equalTo("archived", false) .equalTo("blocked", false) .isNotEmpty("recipients") -- GitLab From 7a46aa696001d82d9f9380ea302ba8956029d70b Mon Sep 17 00:00:00 2001 From: moezbhatti Date: Tue, 12 Nov 2019 23:50:44 -0500 Subject: [PATCH 021/109] Guard against duplicate phone numbers --- .../main/java/com/moez/QKSMS/repository/SyncRepositoryImpl.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/src/main/java/com/moez/QKSMS/repository/SyncRepositoryImpl.kt b/data/src/main/java/com/moez/QKSMS/repository/SyncRepositoryImpl.kt index 3748bdcb9..0b1f2362f 100644 --- a/data/src/main/java/com/moez/QKSMS/repository/SyncRepositoryImpl.kt +++ b/data/src/main/java/com/moez/QKSMS/repository/SyncRepositoryImpl.kt @@ -162,7 +162,7 @@ class SyncRepositoryImpl @Inject constructor( // Sync recipients recipientCursor?.use { - val contacts = realm.copyToRealm(getContacts()) + val contacts = realm.copyToRealmOrUpdate(getContacts()) val recipients = recipientCursor.map { cursor -> progress++ syncProgress.onNext(SyncRepository.SyncProgress.Running(max, progress, false)) -- GitLab From be591047ac77866a9523f4f59fa4812fffeefdad Mon Sep 17 00:00:00 2001 From: moezbhatti Date: Sun, 17 Nov 2019 01:17:01 -0500 Subject: [PATCH 022/109] Split contacts to new screen, allow attaching multiple photos --- .../QKSMS/repository/ContactRepositoryImpl.kt | 9 + domain/build.gradle | 1 + .../QKSMS/repository/ContactRepository.kt | 2 + presentation/src/main/AndroidManifest.xml | 1 + .../QKSMS/feature/compose/ComposeActivity.kt | 101 ++++----- .../QKSMS/feature/compose/ComposeState.kt | 5 - .../moez/QKSMS/feature/compose/ComposeView.kt | 12 +- .../QKSMS/feature/compose/ComposeViewModel.kt | 198 +++++------------ .../feature/contacts/ContactsActivity.kt | 104 +++++++++ .../contacts/ContactsActivityModule.kt | 42 ++++ .../feature/contacts/ContactsContract.kt | 40 ++++ .../QKSMS/feature/contacts/ContactsState.kt | 27 +++ .../feature/contacts/ContactsViewModel.kt | 201 ++++++++++++++++++ .../android/ActivityBuilderModule.kt | 6 + .../src/main/res/layout/compose_activity.xml | 46 +--- .../src/main/res/layout/contacts_activity.xml | 80 +++++++ 16 files changed, 612 insertions(+), 263 deletions(-) create mode 100644 presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsActivity.kt create mode 100644 presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsActivityModule.kt create mode 100644 presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsContract.kt create mode 100644 presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsState.kt create mode 100644 presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsViewModel.kt create mode 100644 presentation/src/main/res/layout/contacts_activity.xml 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 268913a68..ee3d25fa2 100644 --- a/data/src/main/java/com/moez/QKSMS/repository/ContactRepositoryImpl.kt +++ b/data/src/main/java/com/moez/QKSMS/repository/ContactRepositoryImpl.kt @@ -73,6 +73,15 @@ class ContactRepositoryImpl @Inject constructor( .findAll() } + override fun getUnmanagedContact(lookupKey: String): Contact? { + return Realm.getDefaultInstance().use { realm -> + realm.where(Contact::class.java) + .equalTo("lookupKey", lookupKey) + .findFirst() + ?.let(realm::copyFromRealm) + } + } + override fun getUnmanagedContacts(starred: Boolean): Observable> { val realm = Realm.getDefaultInstance() diff --git a/domain/build.gradle b/domain/build.gradle index e428250e6..b7ab9bc62 100644 --- a/domain/build.gradle +++ b/domain/build.gradle @@ -19,6 +19,7 @@ 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 { 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 4242afb52..b6f8011b1 100644 --- a/domain/src/main/java/com/moez/QKSMS/repository/ContactRepository.kt +++ b/domain/src/main/java/com/moez/QKSMS/repository/ContactRepository.kt @@ -31,6 +31,8 @@ interface ContactRepository { fun getContacts(): RealmResults + fun getUnmanagedContact(lookupKey: String): Contact? + fun getUnmanagedContacts(starred: Boolean = false): Observable> fun getUnmanagedContactGroups(): Observable> diff --git a/presentation/src/main/AndroidManifest.xml b/presentation/src/main/AndroidManifest.xml index 2faa07aae..a64c8d86b 100644 --- a/presentation/src/main/AndroidManifest.xml +++ b/presentation/src/main/AndroidManifest.xml @@ -108,6 +108,7 @@ android:windowSoftInputMode="adjustResize" /> + 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 d3f7d97c5..a6e9cefb6 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 @@ -43,7 +43,6 @@ import androidx.lifecycle.ViewModelProviders import com.google.android.flexbox.FlexboxLayoutManager import com.google.android.material.snackbar.Snackbar import com.jakewharton.rxbinding2.view.clicks -import com.jakewharton.rxbinding2.widget.editorActions import com.jakewharton.rxbinding2.widget.textChanges import com.moez.QKSMS.R import com.moez.QKSMS.common.Navigator @@ -56,14 +55,9 @@ import com.moez.QKSMS.common.util.extensions.setBackgroundTint import com.moez.QKSMS.common.util.extensions.setTint import com.moez.QKSMS.common.util.extensions.setVisible import com.moez.QKSMS.common.util.extensions.showKeyboard -import com.moez.QKSMS.common.widget.QkDialog -import com.moez.QKSMS.extensions.Optional import com.moez.QKSMS.feature.compose.editing.Chip import com.moez.QKSMS.feature.compose.editing.ChipsAdapter -import com.moez.QKSMS.feature.compose.editing.ComposeItem -import com.moez.QKSMS.feature.compose.editing.ComposeItemAdapter -import com.moez.QKSMS.feature.compose.editing.PhoneNumberAction -import com.moez.QKSMS.feature.compose.editing.PhoneNumberPickerAdapter +import com.moez.QKSMS.feature.contacts.ContactsActivity import com.moez.QKSMS.model.Attachment import com.uber.autodispose.android.lifecycle.scope import com.uber.autodispose.autoDisposable @@ -75,32 +69,26 @@ import kotlinx.android.synthetic.main.compose_activity.* import java.text.SimpleDateFormat import java.util.* import javax.inject.Inject +import kotlin.collections.HashMap class ComposeActivity : QkThemedActivity(), ComposeView { companion object { - private const val CAMERA_REQUEST_CODE = 0 - private const val GALLERY_REQUEST_CODE = 1 - private const val CONTACT_REQUEST_CODE = 2 + private const val SELECT_CONTACT_REQUEST_CODE = 0 + private const val TAKE_PHOTO_REQUEST_CODE = 1 + private const val ATTACH_PHOTO_REQUEST_CODE = 2 + private const val ATTACH_CONTACT_REQUEST_CODE = 3 } @Inject lateinit var attachmentAdapter: AttachmentAdapter @Inject lateinit var chipsAdapter: ChipsAdapter - @Inject lateinit var contactsAdapter: ComposeItemAdapter @Inject lateinit var dateFormatter: DateFormatter @Inject lateinit var messageAdapter: MessagesAdapter @Inject lateinit var navigator: Navigator - @Inject lateinit var phoneNumberAdapter: PhoneNumberPickerAdapter @Inject lateinit var viewModelFactory: ViewModelProvider.Factory override val activityVisibleIntent: Subject = PublishSubject.create() - override val queryChangedIntent: Observable by lazy { search.textChanges() } - override val queryBackspaceIntent: Observable<*> by lazy { search.backspaces } - override val queryEditorActionIntent: Observable by lazy { search.editorActions() } - override val composeItemPressedIntent: Subject by lazy { contactsAdapter.clicks } - override val composeItemLongPressedIntent: Subject by lazy { contactsAdapter.longClicks } - override val phoneNumberSelectedIntent: Subject> by lazy { phoneNumberAdapter.selectedItemChanges } - override val phoneNumberActionIntent: Subject = PublishSubject.create() + override val chipsSelectedIntent: Subject> = PublishSubject.create() override val chipDeletedIntent: Subject by lazy { chipsAdapter.chipDeleted } override val menuReadyIntent: Observable = menu.map { Unit } override val optionsItemIntent: Subject = PublishSubject.create() @@ -127,18 +115,6 @@ class ComposeActivity : QkThemedActivity(), ComposeView { override val viewQksmsPlusIntent: Subject = PublishSubject.create() override val backPressedIntent: Subject = PublishSubject.create() - private val phoneNumberDialog by lazy { - QkDialog(this).apply { - titleRes = R.string.compose_number_picker_title - adapter = phoneNumberAdapter - positiveButton = R.string.compose_number_picker_always - positiveButtonListener = { phoneNumberActionIntent.onNext(PhoneNumberAction.ALWAYS) } - negativeButton = R.string.compose_number_picker_once - negativeButtonListener = { phoneNumberActionIntent.onNext(PhoneNumberAction.JUST_ONCE) } - cancelListener = { phoneNumberActionIntent.onNext(PhoneNumberAction.CANCEL) } - } - } - private val viewModel by lazy { ViewModelProviders.of(this, viewModelFactory)[ComposeViewModel::class.java] } private var cameraDestination: Uri? = null @@ -156,7 +132,6 @@ class ComposeActivity : QkThemedActivity(), ComposeView { chipsAdapter.view = chips - contacts.itemAnimator = null chips.itemAnimator = null chips.layoutManager = FlexboxLayoutManager(this) @@ -230,19 +205,17 @@ class ComposeActivity : QkThemedActivity(), ComposeView { } toolbarSubtitle.setVisible(state.query.isNotEmpty()) - toolbarSubtitle.text = getString(R.string.compose_subtitle_results, state.searchSelectionPosition, state.searchResults) + toolbarSubtitle.text = getString(R.string.compose_subtitle_results, state.searchSelectionPosition, + state.searchResults) toolbarTitle.setVisible(!state.editingMode) - chips.setVisible(state.editingMode && !state.searching) - search.setVisible(state.editingMode && state.searching) - contacts.setVisible(state.editingMode && state.searching) - composeBar.setVisible(!state.searching && !state.loading) + chips.setVisible(state.editingMode) + composeBar.setVisible(!state.loading) // Don't set the adapters unless needed if (state.editingMode && chips.adapter == null) chips.adapter = chipsAdapter - if (state.editingMode && contacts.adapter == null) contacts.adapter = contactsAdapter - toolbar.menu.findItem(R.id.add)?.isVisible = state.editingMode && !state.searching + toolbar.menu.findItem(R.id.add)?.isVisible = state.editingMode 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 @@ -260,15 +233,6 @@ class ComposeActivity : QkThemedActivity(), ComposeView { } chipsAdapter.data = state.selectedChips - contactsAdapter.data = state.composeItems - - if (state.selectedContact != null && !phoneNumberDialog.isShowing) { - phoneNumberAdapter.data = state.selectedContact.numbers - phoneNumberDialog.subtitle = state.selectedContact.name - phoneNumberDialog.show() - } else if (state.selectedContact == null && phoneNumberDialog.isShowing) { - phoneNumberDialog.dismiss() - } loading.setVisible(state.loading) @@ -342,7 +306,14 @@ class ComposeActivity : QkThemedActivity(), ComposeView { val intent = Intent(Intent.ACTION_PICK) .setType(ContactsContract.CommonDataKinds.Phone.CONTENT_TYPE) - startActivityForResult(Intent.createChooser(intent, null), CONTACT_REQUEST_CODE) + startActivityForResult(Intent.createChooser(intent, null), ATTACH_CONTACT_REQUEST_CODE) + } + + override fun showContacts(chips: List) { + val serialized = HashMap(chips.associate { chip -> chip.address to chip.contact?.lookupKey }) + val intent = Intent(this, ContactsActivity::class.java) + .putExtra(ContactsActivity.ChipsKey, serialized) + startActivityForResult(intent, SELECT_CONTACT_REQUEST_CODE) } override fun requestCamera() { @@ -352,17 +323,17 @@ class ComposeActivity : QkThemedActivity(), ComposeView { val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) .putExtra(MediaStore.EXTRA_OUTPUT, cameraDestination) - startActivityForResult(Intent.createChooser(intent, null), CAMERA_REQUEST_CODE) + startActivityForResult(Intent.createChooser(intent, null), TAKE_PHOTO_REQUEST_CODE) } override fun requestGallery() { val intent = Intent(Intent.ACTION_PICK) - .putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false) + .putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) .addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) .putExtra(Intent.EXTRA_LOCAL_ONLY, false) .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) .setType("image/*") - startActivityForResult(Intent.createChooser(intent, null), GALLERY_REQUEST_CODE) + startActivityForResult(Intent.createChooser(intent, null), ATTACH_PHOTO_REQUEST_CODE) } override fun setDraft(draft: String) = message.setText(draft) @@ -397,15 +368,29 @@ class ComposeActivity : QkThemedActivity(), ComposeView { } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (resultCode == Activity.RESULT_OK) { - when (requestCode) { - CAMERA_REQUEST_CODE -> cameraDestination?.let(attachmentSelectedIntent::onNext) - GALLERY_REQUEST_CODE -> data?.data?.let(attachmentSelectedIntent::onNext) - CONTACT_REQUEST_CODE -> data?.data?.let(contactSelectedIntent::onNext) + when { + requestCode == SELECT_CONTACT_REQUEST_CODE -> { + chipsSelectedIntent.onNext(data?.getSerializableExtra(ContactsActivity.ChipsKey) + ?.let { serializable -> serializable as? HashMap } + ?: hashMapOf()) + } + requestCode == TAKE_PHOTO_REQUEST_CODE && resultCode == Activity.RESULT_OK -> { + cameraDestination?.let(attachmentSelectedIntent::onNext) + } + requestCode == ATTACH_PHOTO_REQUEST_CODE && resultCode == Activity.RESULT_OK -> { + data?.clipData?.itemCount + ?.let { count -> 0 until count } + ?.mapNotNull { i -> data.clipData?.getItemAt(i)?.uri } + ?.forEach(attachmentSelectedIntent::onNext) + ?: data?.data?.let(attachmentSelectedIntent::onNext) + } + requestCode == ATTACH_CONTACT_REQUEST_CODE && resultCode == Activity.RESULT_OK -> { + data?.data?.let(contactSelectedIntent::onNext) } + else -> super.onActivityResult(requestCode, resultCode, data) } } override fun onBackPressed() = backPressedIntent.onNext(Unit) -} \ No newline at end of file +} 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 f7b7e7b88..3d6babaf0 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 @@ -20,9 +20,7 @@ package com.moez.QKSMS.feature.compose import com.moez.QKSMS.compat.SubscriptionInfoCompat import com.moez.QKSMS.feature.compose.editing.Chip -import com.moez.QKSMS.feature.compose.editing.ComposeItem import com.moez.QKSMS.model.Attachment -import com.moez.QKSMS.model.Contact import com.moez.QKSMS.model.Conversation import com.moez.QKSMS.model.Message import io.realm.RealmResults @@ -30,10 +28,7 @@ import io.realm.RealmResults data class ComposeState( val hasError: Boolean = false, val editingMode: Boolean = false, - val searching: Boolean = false, - val composeItems: List = ArrayList(), val selectedConversation: Long = 0, - val selectedContact: Contact? = null, // For phone number picker val selectedChips: List = ArrayList(), val sendAsGroup: Boolean = true, val conversationtitle: String = "", 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 6c3b73e2e..ab715f38c 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 @@ -22,10 +22,7 @@ import android.net.Uri import androidx.annotation.StringRes import androidx.core.view.inputmethod.InputContentInfoCompat import com.moez.QKSMS.common.base.QkView -import com.moez.QKSMS.extensions.Optional import com.moez.QKSMS.feature.compose.editing.Chip -import com.moez.QKSMS.feature.compose.editing.ComposeItem -import com.moez.QKSMS.feature.compose.editing.PhoneNumberAction import com.moez.QKSMS.model.Attachment import io.reactivex.Observable import io.reactivex.subjects.Subject @@ -33,13 +30,7 @@ import io.reactivex.subjects.Subject interface ComposeView : QkView { val activityVisibleIntent: Observable - val queryChangedIntent: Observable - val queryBackspaceIntent: Observable<*> - val queryEditorActionIntent: Observable - val composeItemPressedIntent: Subject - val composeItemLongPressedIntent: Subject - val phoneNumberSelectedIntent: Subject> - val phoneNumberActionIntent: Subject + val chipsSelectedIntent: Subject> val chipDeletedIntent: Subject val menuReadyIntent: Observable val optionsItemIntent: Observable @@ -71,6 +62,7 @@ interface ComposeView : QkView { fun requestDefaultSms() fun requestStoragePermission() fun requestSmsPermission() + fun showContacts(chips: List) fun requestCamera() fun requestGallery() fun requestDatePicker() 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 f76bd945d..6f70953f4 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 @@ -40,7 +40,6 @@ import com.moez.QKSMS.extensions.asObservable 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.editing.Chip import com.moez.QKSMS.feature.compose.editing.ComposeItem import com.moez.QKSMS.feature.compose.editing.ComposeItem.* @@ -51,26 +50,20 @@ import com.moez.QKSMS.model.* import com.moez.QKSMS.filter.ContactGroupFilter import com.moez.QKSMS.interactor.AddScheduledMessage import com.moez.QKSMS.interactor.CancelDelayedMessage -import com.moez.QKSMS.interactor.ContactSync import com.moez.QKSMS.interactor.DeleteMessages import com.moez.QKSMS.interactor.MarkRead import com.moez.QKSMS.interactor.RetrySending import com.moez.QKSMS.interactor.SendMessage -import com.moez.QKSMS.interactor.SetDefaultPhoneNumber import com.moez.QKSMS.manager.ActiveConversationManager 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 import com.moez.QKSMS.repository.ContactRepository import com.moez.QKSMS.repository.ConversationRepository import com.moez.QKSMS.repository.MessageRepository import com.moez.QKSMS.util.ActiveSubscriptionObservable -import com.moez.QKSMS.util.PhoneNumberUtils import com.moez.QKSMS.util.Preferences import com.moez.QKSMS.util.tryOrNull import com.uber.autodispose.android.lifecycle.scope @@ -84,60 +77,49 @@ import io.reactivex.schedulers.Schedulers import io.reactivex.subjects.BehaviorSubject import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.Subject -import io.realm.RealmList -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.rx2.awaitFirst import timber.log.Timber import java.util.* import javax.inject.Inject import javax.inject.Named class ComposeViewModel @Inject constructor( - @Named("query") private val query: String, - @Named("threadId") private val threadId: Long, - @Named("address") private val address: String, - @Named("text") private val sharedText: String, - @Named("attachments") private val sharedAttachments: Attachments, - private val context: Context, - private val activeConversationManager: ActiveConversationManager, - private val addScheduledMessage: AddScheduledMessage, - 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, - private val markRead: MarkRead, - private val messageDetailsFormatter: MessageDetailsFormatter, - private val messageRepo: MessageRepository, - private val navigator: Navigator, - private val permissionManager: PermissionManager, - private val phoneNumberUtils: PhoneNumberUtils, - private val prefs: Preferences, - private val retrySending: RetrySending, - private val sendMessage: SendMessage, - private val setDefaultPhoneNumber: SetDefaultPhoneNumber, - private val subscriptionManager: SubscriptionManagerCompat, - private val syncContacts: ContactSync + @Named("query") private val query: String, + @Named("threadId") private val threadId: Long, + @Named("address") private val address: String, + @Named("text") private val sharedText: String, + @Named("attachments") private val sharedAttachments: Attachments, + private val contactRepo: ContactRepository, + private val context: Context, + private val activeConversationManager: ActiveConversationManager, + private val addScheduledMessage: AddScheduledMessage, + private val billingManager: BillingManager, + private val cancelMessage: CancelDelayedMessage, + private val conversationRepo: ConversationRepository, + private val deleteMessages: DeleteMessages, + private val markRead: MarkRead, + private val messageDetailsFormatter: MessageDetailsFormatter, + private val messageRepo: MessageRepository, + private val navigator: Navigator, + private val permissionManager: PermissionManager, + private val prefs: Preferences, + private val retrySending: RetrySending, + private val sendMessage: SendMessage, + private val subscriptionManager: SubscriptionManagerCompat ) : QkViewModel(ComposeState( editingMode = threadId == 0L && address.isBlank(), - searching = threadId == 0L && address.isBlank(), selectedConversation = threadId, query = query) ) { private val attachments: Subject> = BehaviorSubject.createDefault(sharedAttachments) - private val contactGroups: Observable> by lazy { contactsRepo.getUnmanagedContactGroups() } - private val contacts: Observable> by lazy { contactsRepo.getUnmanagedContacts() } private val chipsReducer: 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 selectedChips: Subject> = BehaviorSubject.createDefault(listOf()) private val searchResults: Subject> = BehaviorSubject.create() private val searchSelection: Subject = BehaviorSubject.createDefault(-1) - private val starredContacts: Observable> by lazy { contactsRepo.getUnmanagedContacts(true) } + + private var shouldShowContacts = threadId == 0L && address.isBlank() init { val initialConversation = threadId.takeIf { it != 0L } @@ -247,136 +229,52 @@ class ComposeViewModel @Inject constructor( val sub = if (subs.size > 1) subs.firstOrNull { it.subscriptionId == subId } ?: subs[0] else null newState { copy(subscription = sub) } }.subscribe() - - if (threadId == 0L) { - syncContacts.execute(Unit) - } } @RequiresApi(Build.VERSION_CODES.LOLLIPOP_MR1) override fun bindView(view: ComposeView) { super.bindView(view) - // Set the contact suggestions list to visible when the add button is pressed - view.optionsItemIntent - .filter { it == R.id.add } - .skipUntil(state.filter { state -> state.editingMode }) - .takeUntil(state.filter { state -> !state.editingMode }) - .autoDisposable(view.scope()) - .subscribe { newState { copy(searching = true) } } - - // 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, recents, starredContacts, contactGroups, contacts, selectedChips - ) { query, recents, starredContacts, contactGroups, contacts, selectedChips -> - 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) - } + if (shouldShowContacts) { + shouldShowContacts = false + view.showContacts(selectedChips.blockingFirst()) + } - // 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 -> selectedChips.map { it.contact }.contains(contact) } - .filter { contact -> contactFilter.filter(contact, normalizedQuery) } - .map(::Starred) - - composeItems += contactGroups - .filter { group -> contactGroupFilter.filter(group, normalizedQuery) } - .map(::Group) - - composeItems += contacts - .filterNot { contact -> selectedChips.map { it.contact }.contains(contact) } - .filter { contact -> contactFilter.filter(contact, normalizedQuery) } - .map(::Person) + view.chipsSelectedIntent + .withLatestFrom(selectedChips) { hashmap, chips -> + if (hashmap.isEmpty() && chips.isEmpty()) { + newState { copy(hasError = true) } } - - composeItems + hashmap } - .skipUntil(state.filter { state -> state.editingMode }) - .takeUntil(state.filter { state -> !state.editingMode }) - .subscribeOn(Schedulers.computation()) - .autoDisposable(view.scope()) - .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 - view.queryBackspaceIntent - .withLatestFrom(selectedChips, view.queryChangedIntent) { event, contacts, query -> - if (contacts.isNotEmpty() && query.isEmpty()) { - chipsReducer.onNext { it.dropLast(1) } + .filter { hashmap -> hashmap.isNotEmpty() } + .map { hashmap -> + hashmap.map { (address, lookupKey) -> + Chip(address, lookupKey?.let(contactRepo::getUnmanagedContact)) } } .autoDisposable(view.scope()) - .subscribe() - - // Listen for ComposeItems being selected, and then send them off to the number picker dialog in case - // the user needs to select a phone number - view.queryEditorActionIntent - .filter { actionId -> actionId == EditorInfo.IME_ACTION_DONE } - .withLatestFrom(state) { _, state -> state } - .mapNotNull { state -> state.composeItems.firstOrNull() } - .mergeWith(view.composeItemPressedIntent) - .map { composeItem -> composeItem to false } - .mergeWith(view.composeItemLongPressedIntent.map { composeItem -> composeItem to true }) - .observeOn(Schedulers.io()) - .skipUntil(state.filter { state -> state.editingMode }) - .takeUntil(state.filter { state -> !state.editingMode }) - .autoDisposable(view.scope()) - .subscribe { (composeItem, force) -> - val contacts = composeItem.getContacts() - val newChips = contacts.map { contact -> - if (contact.numbers.size == 1 || contact.getDefaultNumber() != null && !force) { - val number = contact.getDefaultNumber() ?: contact.numbers[0]!! - Chip(number.address, contact) - } else { - runBlocking { - newState { copy(selectedContact = contact) } - val action = view.phoneNumberActionIntent.awaitFirst() - newState { copy(selectedContact = null) } - val numberId = view.phoneNumberSelectedIntent.awaitFirst().value - val number = contact.numbers.find { number -> number.id == numberId } - - if (action == PhoneNumberAction.CANCEL || number == null) { - return@runBlocking null - } - - if (action == PhoneNumberAction.ALWAYS) { - val params = SetDefaultPhoneNumber.Params(contact.lookupKey, number.id) - setDefaultPhoneNumber.execute(params) - } - - Chip(number.address, contact) - } ?: return@subscribe - } - } + .subscribe { chips -> + chipsReducer.onNext { list -> list + chips } + } - newState { copy(searching = false) } - chipsReducer.onNext { chips -> chips + newChips } + // Set the contact suggestions list to visible when the add button is pressed + view.optionsItemIntent + .filter { it == R.id.add } + .withLatestFrom(selectedChips) { _, chips -> + view.showContacts(chips) } + .autoDisposable(view.scope()) + .subscribe() // Update the list of selected contacts when a new contact is selected or an existing one is deselected view.chipDeletedIntent - .skipUntil(state.filter { state -> state.editingMode }) - .takeUntil(state.filter { state -> !state.editingMode }) .autoDisposable(view.scope()) .subscribe { contact -> chipsReducer.onNext { contacts -> val result = contacts.filterNot { it == contact } if (result.isEmpty()) { - newState { copy(searching = true) } + view.showContacts(result) } result } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsActivity.kt b/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsActivity.kt new file mode 100644 index 000000000..3685941cc --- /dev/null +++ b/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsActivity.kt @@ -0,0 +1,104 @@ +/* + * 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.contacts + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import androidx.lifecycle.ViewModelProviders +import com.jakewharton.rxbinding2.widget.editorActions +import com.jakewharton.rxbinding2.widget.textChanges +import com.moez.QKSMS.R +import com.moez.QKSMS.common.ViewModelFactory +import com.moez.QKSMS.common.base.QkThemedActivity +import com.moez.QKSMS.common.widget.QkDialog +import com.moez.QKSMS.extensions.Optional +import com.moez.QKSMS.feature.compose.editing.ComposeItem +import com.moez.QKSMS.feature.compose.editing.ComposeItemAdapter +import com.moez.QKSMS.feature.compose.editing.PhoneNumberAction +import com.moez.QKSMS.feature.compose.editing.PhoneNumberPickerAdapter +import dagger.android.AndroidInjection +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 { + + companion object { + const val ChipsKey = "chips" + } + + @Inject lateinit var contactsAdapter: ComposeItemAdapter + @Inject lateinit var phoneNumberAdapter: PhoneNumberPickerAdapter + @Inject lateinit var viewModelFactory: ViewModelFactory + + override val queryChangedIntent: Observable by lazy { search.textChanges() } + override val queryBackspaceIntent: Observable<*> by lazy { search.backspaces } + override val queryEditorActionIntent: Observable by lazy { search.editorActions() } + override val composeItemPressedIntent: Subject by lazy { contactsAdapter.clicks } + override val composeItemLongPressedIntent: Subject by lazy { contactsAdapter.longClicks } + override val phoneNumberSelectedIntent: Subject> by lazy { phoneNumberAdapter.selectedItemChanges } + override val phoneNumberActionIntent: Subject = PublishSubject.create() + + private val viewModel by lazy { ViewModelProviders.of(this, viewModelFactory)[ContactsViewModel::class.java] } + + private val phoneNumberDialog by lazy { + QkDialog(this).apply { + titleRes = R.string.compose_number_picker_title + adapter = phoneNumberAdapter + positiveButton = R.string.compose_number_picker_always + positiveButtonListener = { phoneNumberActionIntent.onNext(PhoneNumberAction.ALWAYS) } + negativeButton = R.string.compose_number_picker_once + negativeButtonListener = { phoneNumberActionIntent.onNext(PhoneNumberAction.JUST_ONCE) } + cancelListener = { phoneNumberActionIntent.onNext(PhoneNumberAction.CANCEL) } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + AndroidInjection.inject(this) + super.onCreate(savedInstanceState) + setContentView(R.layout.contacts_activity) + showBackButton(true) + viewModel.bindView(this) + + contacts.itemAnimator = null + contacts.adapter = contactsAdapter + } + + override fun render(state: ContactsState) { + contactsAdapter.data = state.composeItems + + if (state.selectedContact != null && !phoneNumberDialog.isShowing) { + phoneNumberAdapter.data = state.selectedContact.numbers + phoneNumberDialog.subtitle = state.selectedContact.name + phoneNumberDialog.show() + } else if (state.selectedContact == null && phoneNumberDialog.isShowing) { + phoneNumberDialog.dismiss() + } + } + + override fun finish(result: HashMap) { + val intent = Intent().putExtra(ChipsKey, result) + setResult(Activity.RESULT_OK, intent) + finish() + } + +} diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsActivityModule.kt b/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsActivityModule.kt new file mode 100644 index 000000000..d8b832f85 --- /dev/null +++ b/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsActivityModule.kt @@ -0,0 +1,42 @@ +/* + * 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.contacts + +import androidx.lifecycle.ViewModel +import com.moez.QKSMS.injection.ViewModelKey +import dagger.Module +import dagger.Provides +import dagger.multibindings.IntoMap + +@Module +class ContactsActivityModule { + + @Provides + fun provideChips(activity: ContactsActivity): HashMap { + return activity.intent.extras?.getSerializable(ContactsActivity.ChipsKey) + ?.let { serializable -> serializable as? HashMap } + ?: hashMapOf() + } + + @Provides + @IntoMap + @ViewModelKey(ContactsViewModel::class) + fun provideContactsViewModel(viewModel: ContactsViewModel): ViewModel = viewModel + +} diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsContract.kt b/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsContract.kt new file mode 100644 index 000000000..09628b38b --- /dev/null +++ b/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsContract.kt @@ -0,0 +1,40 @@ +/* + * 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.contacts + +import com.moez.QKSMS.common.base.QkView +import com.moez.QKSMS.extensions.Optional +import com.moez.QKSMS.feature.compose.editing.ComposeItem +import com.moez.QKSMS.feature.compose.editing.PhoneNumberAction +import io.reactivex.Observable +import io.reactivex.subjects.Subject + +interface ContactsContract : QkView { + + val queryChangedIntent: Observable + val queryBackspaceIntent: Observable<*> + val queryEditorActionIntent: Observable + val composeItemPressedIntent: Subject + val composeItemLongPressedIntent: Subject + val phoneNumberSelectedIntent: Subject> + val phoneNumberActionIntent: Subject + + fun finish(result: HashMap) + +} diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsState.kt b/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsState.kt new file mode 100644 index 000000000..9d5baad08 --- /dev/null +++ b/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsState.kt @@ -0,0 +1,27 @@ +/* + * 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.contacts + +import com.moez.QKSMS.feature.compose.editing.ComposeItem +import com.moez.QKSMS.model.Contact + +data class ContactsState( + val composeItems: List = ArrayList(), + val selectedContact: Contact? = null // For phone number picker +) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsViewModel.kt b/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsViewModel.kt new file mode 100644 index 000000000..99c948134 --- /dev/null +++ b/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsViewModel.kt @@ -0,0 +1,201 @@ +/* + * 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.contacts + +import android.view.inputmethod.EditorInfo +import com.moez.QKSMS.common.base.QkViewModel +import com.moez.QKSMS.extensions.mapNotNull +import com.moez.QKSMS.extensions.removeAccents +import com.moez.QKSMS.feature.compose.editing.Chip +import com.moez.QKSMS.feature.compose.editing.ComposeItem +import com.moez.QKSMS.feature.compose.editing.PhoneNumberAction +import com.moez.QKSMS.filter.ContactFilter +import com.moez.QKSMS.filter.ContactGroupFilter +import com.moez.QKSMS.interactor.ContactSync +import com.moez.QKSMS.interactor.SetDefaultPhoneNumber +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 com.moez.QKSMS.repository.ContactRepository +import com.moez.QKSMS.repository.ConversationRepository +import com.moez.QKSMS.util.PhoneNumberUtils +import com.uber.autodispose.android.lifecycle.scope +import com.uber.autodispose.autoDisposable +import io.reactivex.Observable +import io.reactivex.rxkotlin.Observables +import io.reactivex.rxkotlin.withLatestFrom +import io.reactivex.schedulers.Schedulers +import io.realm.RealmList +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.rx2.awaitFirst +import javax.inject.Inject + +class ContactsViewModel @Inject constructor( + serializedChips: HashMap, + syncContacts: ContactSync, + private val contactFilter: ContactFilter, + private val contactGroupFilter: ContactGroupFilter, + private val contactsRepo: ContactRepository, + private val conversationRepo: ConversationRepository, + private val phoneNumberUtils: PhoneNumberUtils, + private val setDefaultPhoneNumber: SetDefaultPhoneNumber +) : QkViewModel(ContactsState()) { + + private val contactGroups: Observable> by lazy { contactsRepo.getUnmanagedContactGroups() } + private val contacts: Observable> by lazy { contactsRepo.getUnmanagedContacts() } + private val recents: Observable> by lazy { conversationRepo.getUnmanagedConversations() } + private val starredContacts: Observable> by lazy { contactsRepo.getUnmanagedContacts(true) } + + private val selectedChips = Observable.just(serializedChips) + .observeOn(Schedulers.io()) + .map { hashmap -> + hashmap.map { (address, lookupKey) -> + Chip(address, lookupKey?.let(contactsRepo::getUnmanagedContact)) + } + } + + init { + syncContacts.execute(Unit) + } + + override fun bindView(view: ContactsContract) { + super.bindView(view) + + // 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, recents, starredContacts, contactGroups, contacts, selectedChips + ) { query, recents, starredContacts, contactGroups, contacts, selectedChips -> + val composeItems = mutableListOf() + if (query.isBlank()) { + composeItems += recents + .filter { conversation -> + conversation.recipients.any { recipient -> + selectedChips.none { chip -> + if (recipient.contact == null) { + chip.address == recipient.address + } else { + chip.contact?.lookupKey == recipient.contact?.lookupKey + } + } + } + } + .map(ComposeItem::Recent) + + composeItems += starredContacts + .filter { contact -> selectedChips.none { it.contact?.lookupKey == contact.lookupKey } } + .map(ComposeItem::Starred) + + composeItems += contactGroups + .filter { group -> + group.contacts.any { contact -> + selectedChips.none { chip -> chip.contact?.lookupKey == contact.lookupKey } + } + } + .map(ComposeItem::Group) + + composeItems += contacts + .filter { contact -> selectedChips.none { it.contact?.lookupKey == contact.lookupKey } } + .map(ComposeItem::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 += ComposeItem.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() + composeItems += starredContacts + .asSequence() + .filter { contact -> selectedChips.none { it.contact?.lookupKey == contact.lookupKey } } + .filter { contact -> contactFilter.filter(contact, normalizedQuery) } + .map(ComposeItem::Starred) + + composeItems += contactGroups + .asSequence() + .filter { group -> + group.contacts.any { contact -> + selectedChips.none { chip -> chip.contact?.lookupKey == contact.lookupKey } + } + } + .filter { group -> contactGroupFilter.filter(group, normalizedQuery) } + .map(ComposeItem::Group) + + composeItems += contacts + .asSequence() + .filter { contact -> selectedChips.none { it.contact?.lookupKey == contact.lookupKey } } + .filter { contact -> contactFilter.filter(contact, normalizedQuery) } + .map(ComposeItem::Person) + } + + composeItems + } + .subscribeOn(Schedulers.computation()) + .autoDisposable(view.scope()) + .subscribe { items -> newState { copy(composeItems = items) } } + + // Listen for ComposeItems being selected, and then send them off to the number picker dialog in case + // the user needs to select a phone number + view.queryEditorActionIntent + .filter { actionId -> actionId == EditorInfo.IME_ACTION_DONE } + .withLatestFrom(state) { _, state -> state } + .mapNotNull { state -> state.composeItems.firstOrNull() } + .mergeWith(view.composeItemPressedIntent) + .map { composeItem -> composeItem to false } + .mergeWith(view.composeItemLongPressedIntent.map { composeItem -> composeItem to true }) + .observeOn(Schedulers.io()) + .autoDisposable(view.scope()) + .subscribe { (composeItem, force) -> + val contacts = composeItem.getContacts() + val newChips = contacts.map { contact -> + if (contact.numbers.size == 1 || contact.getDefaultNumber() != null && !force) { + val number = contact.getDefaultNumber() ?: contact.numbers[0]!! + Chip(number.address, contact) + } else { + runBlocking { + newState { copy(selectedContact = contact) } + val action = view.phoneNumberActionIntent.awaitFirst() + newState { copy(selectedContact = null) } + val numberId = view.phoneNumberSelectedIntent.awaitFirst().value + val number = contact.numbers.find { number -> number.id == numberId } + + if (action == PhoneNumberAction.CANCEL || number == null) { + return@runBlocking null + } + + if (action == PhoneNumberAction.ALWAYS) { + val params = SetDefaultPhoneNumber.Params(contact.lookupKey, number.id) + setDefaultPhoneNumber.execute(params) + } + + Chip(number.address, contact) + } ?: return@subscribe + } + } + + view.finish(HashMap(newChips.associate { chip -> chip.address to chip.contact?.lookupKey })) + } + } + +} diff --git a/presentation/src/main/java/com/moez/QKSMS/injection/android/ActivityBuilderModule.kt b/presentation/src/main/java/com/moez/QKSMS/injection/android/ActivityBuilderModule.kt index eb38d2f3a..416d9009c 100644 --- a/presentation/src/main/java/com/moez/QKSMS/injection/android/ActivityBuilderModule.kt +++ b/presentation/src/main/java/com/moez/QKSMS/injection/android/ActivityBuilderModule.kt @@ -22,6 +22,8 @@ import com.moez.QKSMS.feature.backup.BackupActivity import com.moez.QKSMS.feature.blocking.BlockingActivity import com.moez.QKSMS.feature.compose.ComposeActivity import com.moez.QKSMS.feature.compose.ComposeActivityModule +import com.moez.QKSMS.feature.contacts.ContactsActivity +import com.moez.QKSMS.feature.contacts.ContactsActivityModule import com.moez.QKSMS.feature.conversationinfo.ConversationInfoActivity import com.moez.QKSMS.feature.gallery.GalleryActivity import com.moez.QKSMS.feature.gallery.GalleryActivityModule @@ -55,6 +57,10 @@ abstract class ActivityBuilderModule { @ContributesAndroidInjector(modules = [ComposeActivityModule::class]) abstract fun bindComposeActivity(): ComposeActivity + @ActivityScope + @ContributesAndroidInjector(modules = [ContactsActivityModule::class]) + abstract fun bindContactsActivity(): ContactsActivity + @ActivityScope @ContributesAndroidInjector(modules = []) abstract fun bindConversationInfoActivity(): ConversationInfoActivity diff --git a/presentation/src/main/res/layout/compose_activity.xml b/presentation/src/main/res/layout/compose_activity.xml index c29637e41..e8185f7de 100644 --- a/presentation/src/main/res/layout/compose_activity.xml +++ b/presentation/src/main/res/layout/compose_activity.xml @@ -54,7 +54,7 @@ android:text="@string/compose_messages_empty" android:textColor="?android:attr/textColorTertiary" android:visibility="gone" - app:layout_constraintTop_toBottomOf="@id/toolbar" + app:layout_constraintTop_toBottomOf="@id/sendAsGroupBackground" app:textSize="secondary" /> - - + app:constraint_referenced_ids="composeBackground,messageBackground,attachments,attach,message,counter,send" /> + app:constraint_referenced_ids="scheduledTitle,scheduledTime,scheduledCancel,scheduledSeparator" /> - - @@ -418,8 +385,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:visibility="gone" - app:constraint_referenced_ids="contact, contactLabel, schedule, scheduleLabel, gallery, galleryLabel, camera, - cameraLabel, attachingBackground" /> + app:constraint_referenced_ids="contact,contactLabel,schedule,scheduleLabel,gallery,galleryLabel,camera,cameraLabel,attachingBackground" /> + + + + + + + + + + + + + + + + -- GitLab From 7513c3a77073da0573fafad80192bf1292335d6a Mon Sep 17 00:00:00 2001 From: moezbhatti Date: Sun, 17 Nov 2019 02:10:55 -0500 Subject: [PATCH 023/109] Show keyboard when selecting contact and contact selected --- .../common/util/extensions/ActivityExtensions.kt | 3 +-- .../QKSMS/common/util/extensions/ViewExtensions.kt | 9 +++++++-- .../moez/QKSMS/feature/compose/ComposeActivity.kt | 12 ++++++++---- .../com/moez/QKSMS/feature/compose/ComposeView.kt | 1 + .../moez/QKSMS/feature/compose/ComposeViewModel.kt | 1 + .../moez/QKSMS/feature/contacts/ContactsActivity.kt | 9 +++++++++ .../moez/QKSMS/feature/contacts/ContactsContract.kt | 1 + .../moez/QKSMS/feature/contacts/ContactsViewModel.kt | 7 +++++++ 8 files changed, 35 insertions(+), 8 deletions(-) diff --git a/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/ActivityExtensions.kt b/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/ActivityExtensions.kt index 83c103adf..3d4f59f59 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/ActivityExtensions.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/ActivityExtensions.kt @@ -24,8 +24,7 @@ import android.view.inputmethod.InputMethodManager fun Activity.dismissKeyboard() { window.currentFocus?.let { focus -> - val imm = getSystemService( - Context.INPUT_METHOD_SERVICE) as InputMethodManager + val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager imm.hideSoftInputFromWindow(focus.windowToken, 0) focus.clearFocus() diff --git a/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/ViewExtensions.kt b/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/ViewExtensions.kt index 3adb55dc8..3dea161c2 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/ViewExtensions.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/ViewExtensions.kt @@ -39,11 +39,16 @@ var ViewGroup.animateLayoutChanges: Boolean layoutTransition = if (value) LayoutTransition() else null } - fun EditText.showKeyboard() { requestFocus() val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - imm.showSoftInput(this, 0) + imm.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT) +} + +fun EditText.hideKeyboard() { + requestFocus() + val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(windowToken, 0) } fun ImageView.setTint(color: Int) { 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 a6e9cefb6..b06dcb5d3 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 @@ -49,6 +49,7 @@ import com.moez.QKSMS.common.Navigator import com.moez.QKSMS.common.base.QkThemedActivity import com.moez.QKSMS.common.util.DateFormatter import com.moez.QKSMS.common.util.extensions.autoScrollToStart +import com.moez.QKSMS.common.util.extensions.hideKeyboard import com.moez.QKSMS.common.util.extensions.resolveThemeColor import com.moez.QKSMS.common.util.extensions.scrapViews import com.moez.QKSMS.common.util.extensions.setBackgroundTint @@ -228,10 +229,6 @@ class ComposeActivity : QkThemedActivity(), ComposeView { 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() - if (chipsAdapter.data.isEmpty() && state.selectedChips.isNotEmpty()) { - message.showKeyboard() - } - chipsAdapter.data = state.selectedChips loading.setVisible(state.loading) @@ -310,12 +307,19 @@ class ComposeActivity : QkThemedActivity(), ComposeView { } override fun showContacts(chips: List) { + message.hideKeyboard() val serialized = HashMap(chips.associate { chip -> chip.address to chip.contact?.lookupKey }) val intent = Intent(this, ContactsActivity::class.java) .putExtra(ContactsActivity.ChipsKey, serialized) startActivityForResult(intent, SELECT_CONTACT_REQUEST_CODE) } + override fun showKeyboard() { + message.postDelayed({ + message.showKeyboard() + }, 200) + } + override fun requestCamera() { cameraDestination = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) .let { timestamp -> ContentValues().apply { put(MediaStore.Images.Media.TITLE, timestamp) } } 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 ab715f38c..dfa7195fd 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 @@ -63,6 +63,7 @@ interface ComposeView : QkView { fun requestStoragePermission() fun requestSmsPermission() fun showContacts(chips: List) + fun showKeyboard() fun requestCamera() fun requestGallery() fun requestDatePicker() 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 6f70953f4..126813e10 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 @@ -256,6 +256,7 @@ class ComposeViewModel @Inject constructor( .autoDisposable(view.scope()) .subscribe { chips -> chipsReducer.onNext { list -> list + chips } + view.showKeyboard() } // Set the contact suggestions list to visible when the add button is pressed diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsActivity.kt b/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsActivity.kt index 3685941cc..424ae592b 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsActivity.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsActivity.kt @@ -27,6 +27,8 @@ import com.jakewharton.rxbinding2.widget.textChanges import com.moez.QKSMS.R import com.moez.QKSMS.common.ViewModelFactory import com.moez.QKSMS.common.base.QkThemedActivity +import com.moez.QKSMS.common.util.extensions.hideKeyboard +import com.moez.QKSMS.common.util.extensions.showKeyboard import com.moez.QKSMS.common.widget.QkDialog import com.moez.QKSMS.extensions.Optional import com.moez.QKSMS.feature.compose.editing.ComposeItem @@ -95,7 +97,14 @@ class ContactsActivity : QkThemedActivity(), ContactsContract { } } + override fun openKeyboard() { + search.postDelayed({ + search.showKeyboard() + }, 200) + } + override fun finish(result: HashMap) { + search.hideKeyboard() val intent = Intent().putExtra(ChipsKey, result) setResult(Activity.RESULT_OK, intent) finish() diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsContract.kt b/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsContract.kt index 09628b38b..65ed5ebe7 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsContract.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsContract.kt @@ -35,6 +35,7 @@ interface ContactsContract : QkView { val phoneNumberSelectedIntent: Subject> val phoneNumberActionIntent: Subject + fun openKeyboard() fun finish(result: HashMap) } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsViewModel.kt b/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsViewModel.kt index 99c948134..3a52671d5 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsViewModel.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsViewModel.kt @@ -71,6 +71,8 @@ class ContactsViewModel @Inject constructor( } } + private var shouldOpenKeyboard: Boolean = true + init { syncContacts.execute(Unit) } @@ -78,6 +80,11 @@ class ContactsViewModel @Inject constructor( override fun bindView(view: ContactsContract) { super.bindView(view) + if (shouldOpenKeyboard) { + view.openKeyboard() + shouldOpenKeyboard = false + } + // Update the list of contact suggestions based on the query input, while also filtering out any contacts // that have already been selected Observables -- GitLab From 014bacfcdb3442d3101b552804c3aa970c7aeeca Mon Sep 17 00:00:00 2001 From: moezbhatti Date: Sun, 17 Nov 2019 02:11:06 -0500 Subject: [PATCH 024/109] Make sure group messages don't get hidden by banner --- presentation/src/main/res/layout/compose_activity.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/presentation/src/main/res/layout/compose_activity.xml b/presentation/src/main/res/layout/compose_activity.xml index e8185f7de..a4a6a3c37 100644 --- a/presentation/src/main/res/layout/compose_activity.xml +++ b/presentation/src/main/res/layout/compose_activity.xml @@ -39,7 +39,7 @@ app:layout_constraintBottom_toBottomOf="@id/composeBackground" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/toolbar" + app:layout_constraintTop_toBottomOf="@id/sendAsGroupBackground" app:stackFromEnd="true" tools:listitem="@layout/message_list_item_in" /> -- GitLab From d53a754611a02214cf17e7ac0d74bb155c203094 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Sat, 23 Nov 2019 19:47:29 -0500 Subject: [PATCH 025/109] White background for compose --- .../QKSMS/feature/compose/ComposeActivity.kt | 1 - .../QKSMS/feature/qkreply/QkReplyActivity.kt | 7 ++- .../res/drawable/compose_bar_background.xml | 28 --------- .../res/drawable/rounded_rectangle_22dp.xml | 2 +- .../src/main/res/layout/compose_activity.xml | 58 ++++++++----------- .../src/main/res/layout/contact_chip.xml | 2 +- .../src/main/res/layout/qkreply_activity.xml | 28 ++++----- .../src/main/res/values-night/themes.xml | 7 +-- presentation/src/main/res/values/colors.xml | 4 +- presentation/src/main/res/values/themes.xml | 9 +-- 10 files changed, 47 insertions(+), 99 deletions(-) delete mode 100644 presentation/src/main/res/drawable/compose_bar_background.xml 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 b06dcb5d3..43dae487d 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 @@ -159,7 +159,6 @@ class ComposeActivity : QkThemedActivity(), ComposeView { // These theme attributes don't apply themselves on API 21 if (Build.VERSION.SDK_INT <= 22) { messageBackground.setBackgroundTint(resolveThemeColor(R.attr.bubbleColor)) - composeBackground.setBackgroundTint(resolveThemeColor(R.attr.composeBackground)) } } override fun onResume() { diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/qkreply/QkReplyActivity.kt b/presentation/src/main/java/com/moez/QKSMS/feature/qkreply/QkReplyActivity.kt index ead2fe26e..9612bcce4 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/qkreply/QkReplyActivity.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/qkreply/QkReplyActivity.kt @@ -62,6 +62,7 @@ class QkReplyActivity : QkThemedActivity(), QkReplyView { setFinishOnTouchOutside(prefs.qkreplyTapDismiss.get()) setContentView(R.layout.qkreply_activity) + window.setBackgroundDrawable(null) window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) viewModel.bindView(this) @@ -77,10 +78,10 @@ class QkReplyActivity : QkThemedActivity(), QkReplyView { // These theme attributes don't apply themselves on API 21 if (Build.VERSION.SDK_INT <= 22) { toolbar.setBackgroundTint(resolveThemeColor(R.attr.colorPrimary)) - background.setBackgroundTint(resolveThemeColor(R.attr.composeBackground)) + background.setBackgroundTint(resolveThemeColor(android.R.attr.windowBackground)) messageBackground.setBackgroundTint(resolveThemeColor(R.attr.bubbleColor)) - composeBackgroundGradient.setBackgroundTint(resolveThemeColor(R.attr.composeBackground)) - composeBackgroundSolid.setBackgroundTint(resolveThemeColor(R.attr.composeBackground)) + composeBackgroundGradient.setBackgroundTint(resolveThemeColor(android.R.attr.windowBackground)) + composeBackgroundSolid.setBackgroundTint(resolveThemeColor(android.R.attr.windowBackground)) } } diff --git a/presentation/src/main/res/drawable/compose_bar_background.xml b/presentation/src/main/res/drawable/compose_bar_background.xml deleted file mode 100644 index 4c963a183..000000000 --- a/presentation/src/main/res/drawable/compose_bar_background.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/drawable/rounded_rectangle_22dp.xml b/presentation/src/main/res/drawable/rounded_rectangle_22dp.xml index d1754416e..7a6af7bb2 100755 --- a/presentation/src/main/res/drawable/rounded_rectangle_22dp.xml +++ b/presentation/src/main/res/drawable/rounded_rectangle_22dp.xml @@ -24,4 +24,4 @@ - \ No newline at end of file + diff --git a/presentation/src/main/res/layout/compose_activity.xml b/presentation/src/main/res/layout/compose_activity.xml index a4a6a3c37..d1d55b6a8 100644 --- a/presentation/src/main/res/layout/compose_activity.xml +++ b/presentation/src/main/res/layout/compose_activity.xml @@ -23,7 +23,7 @@ android:id="@+id/contentView" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="?attr/composeBackground" + android:background="?android:attr/windowBackground" android:orientation="vertical" tools:context="com.moez.QKSMS.feature.compose.ComposeActivity"> @@ -31,12 +31,12 @@ android:id="@+id/messageList" android:layout_width="0dp" android:layout_height="0dp" + android:layout_marginBottom="12dp" android:clipChildren="false" android:clipToPadding="false" android:paddingTop="8dp" - android:paddingBottom="16dp" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" - app:layout_constraintBottom_toBottomOf="@id/composeBackground" + app:layout_constraintBottom_toTopOf="@id/messageBackground" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/sendAsGroupBackground" @@ -135,31 +135,29 @@ android:id="@+id/composeBar" android:layout_width="wrap_content" android:layout_height="wrap_content" - app:constraint_referenced_ids="composeBackground,messageBackground,attachments,attach,message,counter,send" /> + app:constraint_referenced_ids="messageBackground,attachments,attach,message,counter,send" /> + android:id="@+id/composeDivider" + android:layout_width="match_parent" + android:layout_height="1dp" + android:layout_marginBottom="12dp" + android:background="?android:attr/divider" + android:visibility="gone" + app:layout_constraintBottom_toTopOf="@id/messageBackground" + app:layout_goneMarginBottom="12dp" /> @@ -174,7 +172,6 @@ android:id="@+id/scheduledTitle" android:layout_width="0dp" android:layout_height="wrap_content" - android:elevation="4dp" android:paddingStart="16dp" android:paddingEnd="16dp" android:text="@string/compose_scheduled_for" @@ -191,7 +188,6 @@ android:id="@+id/scheduledTime" android:layout_width="0dp" android:layout_height="wrap_content" - android:elevation="4dp" android:paddingStart="16dp" android:paddingEnd="16dp" android:textColor="?android:attr/textColorTertiary" @@ -206,7 +202,6 @@ android:id="@+id/scheduledCancel" android:layout_width="44dp" android:layout_height="56dp" - android:elevation="4dp" android:padding="10dp" android:src="@drawable/ic_cancel_black_24dp" android:tint="?android:attr/textColorSecondary" @@ -218,7 +213,6 @@ android:layout_width="0dp" android:layout_height="1dp" android:background="?android:attr/divider" - android:elevation="4dp" app:layout_constraintBottom_toTopOf="@id/attachments" app:layout_constraintEnd_toEndOf="@id/messageBackground" app:layout_constraintStart_toStartOf="@id/messageBackground" /> @@ -229,7 +223,6 @@ android:layout_height="wrap_content" android:clipChildren="false" android:clipToPadding="false" - android:elevation="4dp" android:orientation="horizontal" android:paddingStart="8dp" android:paddingEnd="8dp" @@ -246,7 +239,6 @@ android:layout_height="wrap_content" android:layout_weight="1" android:background="@null" - android:elevation="4dp" android:gravity="center_vertical" android:hint="@string/compose_hint" android:inputType="textLongMessage|textCapSentences|textMultiLine" @@ -263,12 +255,11 @@ + app:layout_constraintEnd_toEndOf="parent" /> diff --git a/presentation/src/main/res/layout/contact_chip.xml b/presentation/src/main/res/layout/contact_chip.xml index 76805b05d..6627a4434 100755 --- a/presentation/src/main/res/layout/contact_chip.xml +++ b/presentation/src/main/res/layout/contact_chip.xml @@ -30,7 +30,7 @@ android:layout_height="36dp" android:layout_margin="4dp" android:background="@drawable/chip_background" - android:backgroundTint="?attr/composeBackground" + android:backgroundTint="?attr/bubbleColor" android:orientation="horizontal"> + tools:context=".feature.qkreply.QkReplyActivity"> @@ -83,10 +81,9 @@ style="@style/TextPrimary" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="8dp" - android:layout_marginBottom="8dp" + android:layout_marginStart="12dp" + android:layout_marginBottom="12dp" android:background="@null" - android:elevation="4dp" android:gravity="center_vertical" android:hint="@string/compose_hint" android:inputType="textLongMessage|textCapSentences|textMultiLine" @@ -107,18 +104,16 @@ android:layout_height="44dp" android:background="?attr/selectableItemBackground" android:contentDescription="@string/compose_sim_cd" - android:elevation="4dp" android:padding="10dp" android:src="@drawable/ic_sim_card_black_24dp" android:tint="?android:attr/textColorSecondary" app:layout_constraintBottom_toBottomOf="@id/message" - app:layout_constraintEnd_toStartOf="@id/send" /> + app:layout_constraintEnd_toEndOf="@id/messageBackground" /> @color/bubbleDark @color/toolbarDark @color/statusBarDark - @color/backgroundDark @drawable/ripple_dark @color/switchThumbEnabledDark @color/switchThumbDisabledDark @@ -48,12 +47,11 @@ @color/text_primary @color/text_secondary @color/text_tertiary - @android:color/transparent + @color/backgroundDark @style/PopupTheme @color/bubbleDark @color/toolbarDark @color/statusBarDark - @color/backgroundDark @drawable/ripple_dark false @@ -70,14 +68,13 @@ @color/black @color/black @color/black - @color/black -- GitLab From ad3c4a472022e73dc261525b0e01a463a955a374 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Sat, 23 Nov 2019 19:47:41 -0500 Subject: [PATCH 026/109] More rotation for attach button --- .../main/java/com/moez/QKSMS/feature/compose/ComposeActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 43dae487d..1f2a1f0ea 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 @@ -245,7 +245,7 @@ class ComposeActivity : QkThemedActivity(), ComposeView { attachments.setVisible(state.attachments.isNotEmpty()) attachmentAdapter.data = state.attachments - attach.animate().rotation(if (state.attaching) 45f else 0f).start() + attach.animate().rotation(if (state.attaching) 135f else 0f).start() attaching.isVisible = state.attaching counter.text = state.remaining -- GitLab From d509c548c6b1753efcc2246b167365d836afac09 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Sat, 23 Nov 2019 20:06:26 -0500 Subject: [PATCH 027/109] More padding above empty messages --- presentation/src/main/res/layout/compose_activity.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/presentation/src/main/res/layout/compose_activity.xml b/presentation/src/main/res/layout/compose_activity.xml index d1d55b6a8..6e804dd6e 100644 --- a/presentation/src/main/res/layout/compose_activity.xml +++ b/presentation/src/main/res/layout/compose_activity.xml @@ -48,7 +48,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginStart="56dp" - android:layout_marginTop="16dp" + android:layout_marginTop="32dp" android:layout_marginEnd="56dp" android:gravity="center" android:text="@string/compose_messages_empty" -- GitLab From 501c89adc5512986e545fc06c39825615a5f2c43 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Sat, 23 Nov 2019 23:31:25 -0500 Subject: [PATCH 028/109] Show cancel button when searching --- .../feature/contacts/ContactsActivity.kt | 10 +++- .../feature/contacts/ContactsContract.kt | 3 +- .../QKSMS/feature/contacts/ContactsState.kt | 1 + .../feature/contacts/ContactsViewModel.kt | 10 ++++ .../src/main/res/layout/contacts_activity.xml | 46 +++++++++++++++---- .../src/main/res/layout/main_activity.xml | 2 +- .../src/main/res/values-night/themes.xml | 2 +- presentation/src/main/res/values/colors.xml | 2 - presentation/src/main/res/values/themes.xml | 2 +- 9 files changed, 61 insertions(+), 17 deletions(-) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsActivity.kt b/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsActivity.kt index 424ae592b..c0680d7e0 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsActivity.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsActivity.kt @@ -21,7 +21,9 @@ package com.moez.QKSMS.feature.contacts import android.app.Activity import android.content.Intent import android.os.Bundle +import androidx.core.view.isVisible import androidx.lifecycle.ViewModelProviders +import com.jakewharton.rxbinding2.view.clicks import com.jakewharton.rxbinding2.widget.editorActions import com.jakewharton.rxbinding2.widget.textChanges import com.moez.QKSMS.R @@ -53,7 +55,7 @@ class ContactsActivity : QkThemedActivity(), ContactsContract { @Inject lateinit var viewModelFactory: ViewModelFactory override val queryChangedIntent: Observable by lazy { search.textChanges() } - override val queryBackspaceIntent: Observable<*> by lazy { search.backspaces } + override val queryClearedIntent: Observable<*> by lazy { cancel.clicks() } override val queryEditorActionIntent: Observable by lazy { search.editorActions() } override val composeItemPressedIntent: Subject by lazy { contactsAdapter.clicks } override val composeItemLongPressedIntent: Subject by lazy { contactsAdapter.longClicks } @@ -86,6 +88,8 @@ class ContactsActivity : QkThemedActivity(), ContactsContract { } override fun render(state: ContactsState) { + cancel.isVisible = state.query.length > 1 + contactsAdapter.data = state.composeItems if (state.selectedContact != null && !phoneNumberDialog.isShowing) { @@ -97,6 +101,10 @@ class ContactsActivity : QkThemedActivity(), ContactsContract { } } + override fun clearQuery() { + search.text = null + } + override fun openKeyboard() { search.postDelayed({ search.showKeyboard() diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsContract.kt b/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsContract.kt index 65ed5ebe7..cf6c55095 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsContract.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsContract.kt @@ -28,13 +28,14 @@ import io.reactivex.subjects.Subject interface ContactsContract : QkView { val queryChangedIntent: Observable - val queryBackspaceIntent: Observable<*> + val queryClearedIntent: Observable<*> val queryEditorActionIntent: Observable val composeItemPressedIntent: Subject val composeItemLongPressedIntent: Subject val phoneNumberSelectedIntent: Subject> val phoneNumberActionIntent: Subject + fun clearQuery() fun openKeyboard() fun finish(result: HashMap) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsState.kt b/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsState.kt index 9d5baad08..9829d905b 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsState.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsState.kt @@ -22,6 +22,7 @@ import com.moez.QKSMS.feature.compose.editing.ComposeItem import com.moez.QKSMS.model.Contact data class ContactsState( + val query: String = "", val composeItems: List = ArrayList(), val selectedContact: Contact? = null // For phone number picker ) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsViewModel.kt b/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsViewModel.kt index 3a52671d5..39ad99af9 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsViewModel.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsViewModel.kt @@ -85,6 +85,16 @@ class ContactsViewModel @Inject constructor( shouldOpenKeyboard = false } + // Update the state's query, so we know if we should show the cancel button + view.queryChangedIntent + .autoDisposable(view.scope()) + .subscribe { query -> newState { copy(query = query.toString()) } } + + // Clear the query + view.queryClearedIntent + .autoDisposable(view.scope()) + .subscribe { view.clearQuery() } + // Update the list of contact suggestions based on the query input, while also filtering out any contacts // that have already been selected Observables diff --git a/presentation/src/main/res/layout/contacts_activity.xml b/presentation/src/main/res/layout/contacts_activity.xml index 1bbcbb503..89181ebff 100644 --- a/presentation/src/main/res/layout/contacts_activity.xml +++ b/presentation/src/main/res/layout/contacts_activity.xml @@ -47,21 +47,47 @@ style="@style/Toolbar" android:layout_width="match_parent" android:layout_height="wrap_content" + app:contentInsetStartWithNavigation="0dp" app:layout_constrainedHeight="true" app:layout_constraintTop_toTopOf="parent"> - + android:animateLayoutChanges="true"> + + + + + + diff --git a/presentation/src/main/res/layout/main_activity.xml b/presentation/src/main/res/layout/main_activity.xml index 0e52fde04..ab58cee80 100644 --- a/presentation/src/main/res/layout/main_activity.xml +++ b/presentation/src/main/res/layout/main_activity.xml @@ -53,7 +53,7 @@ android:layout_marginEnd="8dp" android:layout_marginBottom="8dp" android:background="@drawable/rounded_rectangle_24dp" - android:backgroundTint="?android:attr/queryBackground" + android:backgroundTint="?attr/bubbleColor" android:hint="@string/title_conversations" android:paddingStart="16dp" android:paddingEnd="16dp" diff --git a/presentation/src/main/res/values-night/themes.xml b/presentation/src/main/res/values-night/themes.xml index 37deb86e3..cf5054e5b 100644 --- a/presentation/src/main/res/values-night/themes.xml +++ b/presentation/src/main/res/values-night/themes.xml @@ -24,7 +24,7 @@ @@ -66,6 +66,7 @@ @color/black @color/black @color/black + @color/bubbleBlack @color/black @color/black @@ -79,7 +80,7 @@ diff --git a/presentation/src/main/res/values-v23/themes.xml b/presentation/src/main/res/values-v23/themes.xml index b789222d5..c27a31ed1 100644 --- a/presentation/src/main/res/values-v23/themes.xml +++ b/presentation/src/main/res/values-v23/themes.xml @@ -21,7 +21,7 @@ diff --git a/presentation/src/main/res/values-v27/themes.xml b/presentation/src/main/res/values-v27/themes.xml index 6acad9c94..1a7fd8cae 100644 --- a/presentation/src/main/res/values-v27/themes.xml +++ b/presentation/src/main/res/values-v27/themes.xml @@ -22,7 +22,7 @@ diff --git a/presentation/src/main/res/values/colors.xml b/presentation/src/main/res/values/colors.xml index 0193967ef..46f6941e1 100644 --- a/presentation/src/main/res/values/colors.xml +++ b/presentation/src/main/res/values/colors.xml @@ -24,9 +24,6 @@ #1f000000 #33ffffff - #FFFFFF - #1d262b - #FFFFFF #1d262b #88000000 @@ -47,7 +44,8 @@ #80ffffff #ECEFF1 - #242a2f + #151B1F + #0F1113 #0C000000 #1AFFFFFF diff --git a/presentation/src/main/res/values/themes.xml b/presentation/src/main/res/values/themes.xml index 4252e6b40..16db69160 100644 --- a/presentation/src/main/res/values/themes.xml +++ b/presentation/src/main/res/values/themes.xml @@ -44,8 +44,8 @@ true @style/PopupTheme @color/bubbleLight - @color/toolbarLight - @color/statusBarLight + @color/backgroundLight + @color/backgroundLight @drawable/ripple @color/switchThumbEnabledLight @color/switchThumbDisabledLight @@ -61,8 +61,8 @@ @color/backgroundLight @style/PopupTheme @color/bubbleLight - @color/toolbarLight - @color/statusBarLight + @color/backgroundLight + @color/backgroundLight @drawable/ripple false -- GitLab From 9578d76c5d8f0b0c5d8b5872664aa1ef3a8e644b Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Sun, 24 Nov 2019 03:20:46 -0500 Subject: [PATCH 038/109] Contact list animations --- .../feature/compose/editing/ComposeItemAdapter.kt | 10 +++++++++- .../moez/QKSMS/feature/contacts/ContactsActivity.kt | 1 - 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ComposeItemAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ComposeItemAdapter.kt index b411b2143..1c5cf36c2 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ComposeItemAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ComposeItemAdapter.kt @@ -168,6 +168,14 @@ class ComposeItemAdapter @Inject constructor(private val colors: Colors) : QkAda (view.numbers.adapter as PhoneNumberAdapter).data = contact.numbers } - override fun areContentsTheSame(old: ComposeItem, new: ComposeItem): Boolean = false + override fun areItemsTheSame(old: ComposeItem, new: ComposeItem): Boolean { + val oldIds = old.getContacts().map { contact -> contact.lookupKey } + val newIds = new.getContacts().map { contact -> contact.lookupKey } + return oldIds == newIds + } + + override fun areContentsTheSame(old: ComposeItem, new: ComposeItem): Boolean { + return false + } } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsActivity.kt b/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsActivity.kt index 7dda49b53..f5ca9f71d 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsActivity.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsActivity.kt @@ -87,7 +87,6 @@ class ContactsActivity : QkThemedActivity(), ContactsContract { showBackButton(true) viewModel.bindView(this) - contacts.itemAnimator = null contacts.adapter = contactsAdapter // These theme attributes don't apply themselves on API 21 -- GitLab From 601f5eaed0f3fb09d95660dda277e5a7c8b53073 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Sun, 24 Nov 2019 11:16:12 -0500 Subject: [PATCH 039/109] Support sharing to multiple addresses Fixes #1371 --- .../QKSMS/feature/compose/ComposeActivityModule.kt | 12 ++++++------ .../moez/QKSMS/feature/compose/ComposeViewModel.kt | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeActivityModule.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeActivityModule.kt index d9b0504a2..5d7b789ce 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeActivityModule.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeActivityModule.kt @@ -42,13 +42,11 @@ class ComposeActivityModule { fun provideThreadId(activity: ComposeActivity): Long = activity.intent.extras?.getLong("threadId") ?: 0L @Provides - @Named("address") - fun provideAddress(activity: ComposeActivity): String { - var address = "" - + @Named("addresses") + fun provideAddresses(activity: ComposeActivity): List { activity.intent.data?.let { val data = it.toString() - address = when { + var address = when { it.scheme?.startsWith("smsto") == true -> data.replace("smsto:", "") it.scheme?.startsWith("mmsto") == true -> data.replace("mmsto:", "") it.scheme?.startsWith("sms") == true -> data.replace("sms:", "") @@ -58,9 +56,11 @@ class ComposeActivityModule { // The dialer app on Oreo sends a URL encoded string, make sure to decode it if (address.contains('%')) address = URLDecoder.decode(address, "UTF-8") + + return address.split(",") } - return address + return listOf() } @Provides 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 0dcb438f0..e00eda063 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 @@ -86,7 +86,7 @@ import javax.inject.Named class ComposeViewModel @Inject constructor( @Named("query") private val query: String, @Named("threadId") private val threadId: Long, - @Named("address") private val address: String, + @Named("addresses") private val addresses: List, @Named("text") private val sharedText: String, @Named("attachments") private val sharedAttachments: Attachments, private val contactRepo: ContactRepository, @@ -108,7 +108,7 @@ class ComposeViewModel @Inject constructor( private val sendMessage: SendMessage, private val subscriptionManager: SubscriptionManagerCompat ) : QkViewModel(ComposeState( - editingMode = threadId == 0L && address.isBlank(), + editingMode = threadId == 0L && addresses.isEmpty(), selectedConversation = threadId, query = query) ) { @@ -121,7 +121,7 @@ class ComposeViewModel @Inject constructor( private val searchResults: Subject> = BehaviorSubject.create() private val searchSelection: Subject = BehaviorSubject.createDefault(-1) - private var shouldShowContacts = threadId == 0L && address.isBlank() + private var shouldShowContacts = threadId == 0L && addresses.isEmpty() init { val initialConversation = threadId.takeIf { it != 0L } @@ -172,8 +172,8 @@ class ComposeViewModel @Inject constructor( .filter { conversation -> conversation.isValid } .subscribe(conversation::onNext) - if (address.isNotBlank()) { - selectedChips.onNext(listOf(Chip(address))) + if (addresses.isNotEmpty()) { + selectedChips.onNext(addresses.map { address -> Chip(address) }) } disposables += chipsReducer -- GitLab From 768fb04ef56cfe686a295a793c8e8bc806d6ab0d Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Sun, 24 Nov 2019 11:48:53 -0500 Subject: [PATCH 040/109] Parse body param in uri query Fixes 1321 --- .../feature/compose/ComposeActivityModule.kt | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeActivityModule.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeActivityModule.kt index 5d7b789ce..dcf9af1fb 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeActivityModule.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeActivityModule.kt @@ -44,23 +44,12 @@ class ComposeActivityModule { @Provides @Named("addresses") fun provideAddresses(activity: ComposeActivity): List { - activity.intent.data?.let { - val data = it.toString() - var address = when { - it.scheme?.startsWith("smsto") == true -> data.replace("smsto:", "") - it.scheme?.startsWith("mmsto") == true -> data.replace("mmsto:", "") - it.scheme?.startsWith("sms") == true -> data.replace("sms:", "") - it.scheme?.startsWith("mms") == true -> data.replace("mms:", "") - else -> "" - } - - // The dialer app on Oreo sends a URL encoded string, make sure to decode it - if (address.contains('%')) address = URLDecoder.decode(address, "UTF-8") - - return address.split(",") - } - - return listOf() + return activity.intent + ?.decodedDataString() + ?.substringAfter(':') // Remove scheme + ?.replaceAfter("?", "") // Remove query + ?.split(",") + ?: listOf() } @Provides @@ -68,6 +57,11 @@ class ComposeActivityModule { fun provideSharedText(activity: ComposeActivity): String { return activity.intent.extras?.getString(Intent.EXTRA_TEXT) ?: activity.intent.extras?.getString("sms_body") + ?: activity.intent?.decodedDataString() + ?.substringAfter('?') // Query string + ?.split(',') + ?.firstOrNull { param -> param.startsWith("body") } + ?.substringAfter('=') ?: "" } @@ -85,4 +79,13 @@ class ComposeActivityModule { @ViewModelKey(ComposeViewModel::class) fun provideComposeViewModel(viewModel: ComposeViewModel): ViewModel = viewModel + // The dialer app on Oreo sends a URL encoded string, make sure to decode it + private fun Intent.decodedDataString(): String? { + val data = data?.toString() + if (data?.contains('%') == true) { + return URLDecoder.decode(data, "UTF-8") + } + return data + } + } \ No newline at end of file -- GitLab From a82ad8d0489bdd227ba4777fc13737019f6d49c2 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Sun, 24 Nov 2019 13:46:56 -0500 Subject: [PATCH 041/109] Persist camera destination uri Fixes #1457 --- .../QKSMS/feature/compose/ComposeActivity.kt | 36 ++++++++++++------- .../QKSMS/feature/compose/ComposeViewModel.kt | 2 +- 2 files changed, 25 insertions(+), 13 deletions(-) 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 447ca39bf..d3350feda 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 @@ -75,10 +75,12 @@ import kotlin.collections.HashMap class ComposeActivity : QkThemedActivity(), ComposeView { companion object { - private const val SELECT_CONTACT_REQUEST_CODE = 0 - private const val TAKE_PHOTO_REQUEST_CODE = 1 - private const val ATTACH_PHOTO_REQUEST_CODE = 2 - private const val ATTACH_CONTACT_REQUEST_CODE = 3 + private const val SelectContactRequestCode = 0 + private const val TakePhotoRequestCode = 1 + private const val AttachPhotoRequestCode = 2 + private const val AttachContactRequestCode = 3 + + private const val CameraDestinationKey = "camera_destination" } @Inject lateinit var attachmentAdapter: AttachmentAdapter @@ -302,7 +304,7 @@ class ComposeActivity : QkThemedActivity(), ComposeView { val intent = Intent(Intent.ACTION_PICK) .setType(ContactsContract.CommonDataKinds.Phone.CONTENT_TYPE) - startActivityForResult(Intent.createChooser(intent, null), ATTACH_CONTACT_REQUEST_CODE) + startActivityForResult(Intent.createChooser(intent, null), AttachContactRequestCode) } override fun showContacts(sharing: Boolean, chips: List) { @@ -311,7 +313,7 @@ class ComposeActivity : QkThemedActivity(), ComposeView { val intent = Intent(this, ContactsActivity::class.java) .putExtra(ContactsActivity.SharingKey, sharing) .putExtra(ContactsActivity.ChipsKey, serialized) - startActivityForResult(intent, SELECT_CONTACT_REQUEST_CODE) + startActivityForResult(intent, SelectContactRequestCode) } override fun showKeyboard() { @@ -327,7 +329,7 @@ class ComposeActivity : QkThemedActivity(), ComposeView { val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) .putExtra(MediaStore.EXTRA_OUTPUT, cameraDestination) - startActivityForResult(Intent.createChooser(intent, null), TAKE_PHOTO_REQUEST_CODE) + startActivityForResult(Intent.createChooser(intent, null), TakePhotoRequestCode) } override fun requestGallery() { @@ -337,7 +339,7 @@ class ComposeActivity : QkThemedActivity(), ComposeView { .putExtra(Intent.EXTRA_LOCAL_ONLY, false) .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) .setType("image/*") - startActivityForResult(Intent.createChooser(intent, null), ATTACH_PHOTO_REQUEST_CODE) + startActivityForResult(Intent.createChooser(intent, null), AttachPhotoRequestCode) } override fun setDraft(draft: String) = message.setText(draft) @@ -373,28 +375,38 @@ class ComposeActivity : QkThemedActivity(), ComposeView { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { when { - requestCode == SELECT_CONTACT_REQUEST_CODE -> { + requestCode == SelectContactRequestCode -> { chipsSelectedIntent.onNext(data?.getSerializableExtra(ContactsActivity.ChipsKey) ?.let { serializable -> serializable as? HashMap } ?: hashMapOf()) } - requestCode == TAKE_PHOTO_REQUEST_CODE && resultCode == Activity.RESULT_OK -> { + requestCode == TakePhotoRequestCode && resultCode == Activity.RESULT_OK -> { cameraDestination?.let(attachmentSelectedIntent::onNext) } - requestCode == ATTACH_PHOTO_REQUEST_CODE && resultCode == Activity.RESULT_OK -> { + requestCode == AttachPhotoRequestCode && resultCode == Activity.RESULT_OK -> { data?.clipData?.itemCount ?.let { count -> 0 until count } ?.mapNotNull { i -> data.clipData?.getItemAt(i)?.uri } ?.forEach(attachmentSelectedIntent::onNext) ?: data?.data?.let(attachmentSelectedIntent::onNext) } - requestCode == ATTACH_CONTACT_REQUEST_CODE && resultCode == Activity.RESULT_OK -> { + requestCode == AttachContactRequestCode && resultCode == Activity.RESULT_OK -> { data?.data?.let(contactSelectedIntent::onNext) } else -> super.onActivityResult(requestCode, resultCode, data) } } + override fun onSaveInstanceState(outState: Bundle) { + outState.putParcelable(CameraDestinationKey, cameraDestination) + super.onSaveInstanceState(outState) + } + + override fun onRestoreInstanceState(savedInstanceState: Bundle?) { + cameraDestination = savedInstanceState?.getParcelable(CameraDestinationKey) + super.onRestoreInstanceState(savedInstanceState) + } + override fun onBackPressed() = backPressedIntent.onNext(Unit) } 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 e00eda063..b2811a99d 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 @@ -502,7 +502,7 @@ class ComposeViewModel @Inject constructor( view.attachmentSelectedIntent.map { uri -> Attachment.Image(uri) }, view.inputContentIntent.map { inputContent -> Attachment.Image(inputContent = inputContent) }) .withLatestFrom(attachments) { attachment, attachments -> attachments + attachment } - .doOnNext { attachments.onNext(it) } + .doOnNext(attachments::onNext) .autoDisposable(view.scope()) .subscribe { newState { copy(attaching = false) } } -- GitLab From cae9bb0f924cc623e66bd6cccbdf7c0b1cc83791 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Sun, 24 Nov 2019 14:06:36 -0500 Subject: [PATCH 042/109] Copy text from multiple messages Closes #1122 --- .../QKSMS/feature/compose/ComposeActivity.kt | 2 +- .../QKSMS/feature/compose/ComposeViewModel.kt | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) 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 d3350feda..c8f373724 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 @@ -222,7 +222,7 @@ class ComposeActivity : QkThemedActivity(), ComposeView { && state.query.isEmpty() toolbar.menu.findItem(R.id.info)?.isVisible = !state.editingMode && state.selectedMessages == 0 && state.query.isEmpty() - toolbar.menu.findItem(R.id.copy)?.isVisible = !state.editingMode && state.selectedMessages == 1 + toolbar.menu.findItem(R.id.copy)?.isVisible = !state.editingMode && state.selectedMessages > 0 toolbar.menu.findItem(R.id.details)?.isVisible = !state.editingMode && state.selectedMessages == 1 toolbar.menu.findItem(R.id.delete)?.isVisible = !state.editingMode && state.selectedMessages > 0 toolbar.menu.findItem(R.id.forward)?.isVisible = !state.editingMode && state.selectedMessages == 1 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 b2811a99d..c90126394 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 @@ -313,11 +313,20 @@ class ComposeViewModel @Inject constructor( // Copy the message contents view.optionsItemIntent .filter { it == R.id.copy } - .withLatestFrom(view.messagesSelectedIntent) { _, messages -> - messages?.firstOrNull()?.let { messageRepo.getMessage(it) }?.let { message -> - ClipboardUtils.copy(context, message.getText()) - context.makeToast(R.string.toast_copied) + .withLatestFrom(view.messagesSelectedIntent) { _, messageIds -> + val messages = messageIds.mapNotNull(messageRepo::getMessage).sortedBy { it.date } + val text = when (messages.size) { + 1 -> messages.first().getText() + else -> messages.foldIndexed("") { index, acc, message -> + when { + index == 0 -> message.getText() + messages[index - 1].compareSender(message) -> "$acc\n${message.getText()}" + else -> "$acc\n\n${message.getText()}" + } + } } + + ClipboardUtils.copy(context, text) } .autoDisposable(view.scope()) .subscribe { view.clearSelection() } -- GitLab From 76e8c977ab33ba9d2663c529cf437198aaf0320b Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Sun, 24 Nov 2019 14:20:30 -0500 Subject: [PATCH 043/109] Share photos externally Closes #1442 --- .../java/com/moez/QKSMS/common/Navigator.kt | 11 +++++++++ .../QKSMS/feature/gallery/GalleryActivity.kt | 2 +- .../QKSMS/feature/gallery/GalleryViewModel.kt | 23 +++++++++++++------ presentation/src/main/res/menu/gallery.xml | 6 ++++- presentation/src/main/res/values/strings.xml | 1 + 5 files changed, 34 insertions(+), 9 deletions(-) diff --git a/presentation/src/main/java/com/moez/QKSMS/common/Navigator.kt b/presentation/src/main/java/com/moez/QKSMS/common/Navigator.kt index 5fb500979..3855b5fd7 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/Navigator.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/Navigator.kt @@ -251,6 +251,17 @@ class Navigator @Inject constructor( startActivityExternal(intent) } + fun shareFile(file: File) { + val data = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file) + val type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(file.name.split(".").last()) + val intent = Intent(Intent.ACTION_SEND) + .setType(type) + .putExtra(Intent.EXTRA_STREAM, data) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + + startActivityExternal(intent) + } + fun showNotificationSettings(threadId: Long = 0) { val intent = Intent(context, NotificationPrefsActivity::class.java) intent.putExtra("threadId", threadId) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/gallery/GalleryActivity.kt b/presentation/src/main/java/com/moez/QKSMS/feature/gallery/GalleryActivity.kt index 868fd3c2d..ff6427c6a 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/gallery/GalleryActivity.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/gallery/GalleryActivity.kt @@ -56,7 +56,7 @@ class GalleryActivity : QkActivity(), GalleryView { private val permissionResultSubject: Subject = PublishSubject.create() override fun onCreate(savedInstanceState: Bundle?) { - delegate.setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES) + delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_YES AndroidInjection.inject(this) super.onCreate(savedInstanceState) setContentView(R.layout.gallery_activity) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/gallery/GalleryViewModel.kt b/presentation/src/main/java/com/moez/QKSMS/feature/gallery/GalleryViewModel.kt index 0e487a6b0..5ece3841b 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/gallery/GalleryViewModel.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/gallery/GalleryViewModel.kt @@ -20,6 +20,7 @@ package com.moez.QKSMS.feature.gallery import android.content.Context import com.moez.QKSMS.R +import com.moez.QKSMS.common.Navigator import com.moez.QKSMS.common.base.QkViewModel import com.moez.QKSMS.common.util.extensions.makeToast import com.moez.QKSMS.extensions.mapNotNull @@ -37,11 +38,12 @@ import javax.inject.Named class GalleryViewModel @Inject constructor( conversationRepo: ConversationRepository, - messageRepo: MessageRepository, @Named("partId") private val partId: Long, private val context: Context, + private val messageRepo: MessageRepository, + private val navigator: Navigator, private val saveImage: SaveImage, - private val permissionManager: PermissionManager + private val permissions: PermissionManager ) : QkViewModel(GalleryState()) { init { @@ -60,10 +62,10 @@ class GalleryViewModel @Inject constructor( override fun bindView(view: GalleryView) { super.bindView(view) view.permissionResult() - .map { permissionManager.hasStorage() } + .map { permissions.hasStorage() } .autoDisposable(view.scope()) .subscribe{ - if(permissionManager.hasStorage()) + if(permissions.hasStorage()) saveImage.execute(partId) { context.makeToast(R.string.gallery_toast_saved) } } // When the screen is touched, toggle the visibility of the navigation UI @@ -76,16 +78,23 @@ class GalleryViewModel @Inject constructor( // Save image to device view.optionsItemSelected() .filter { itemId -> itemId == R.id.save } - .filter { permissionManager.hasStorage().also { if (!it) view.requestStoragePermission() } } + .filter { permissions.hasStorage().also { if (!it) view.requestStoragePermission() } } .withLatestFrom(view.pageChanged()) { _, part -> part.id } .autoDisposable(view.scope()) .subscribe { partId -> - if(permissionManager.hasStorage()) + if(permissions.hasStorage()) saveImage.execute(partId) { context.makeToast(R.string.gallery_toast_saved) } else view.requestStoragePermission() } + // Share image externally + view.optionsItemSelected() + .filter { itemId -> itemId == R.id.share } + .filter { permissions.hasStorage().also { if (!it) view.requestStoragePermission() } } + .withLatestFrom(view.pageChanged()) { _, part -> part.id } + .autoDisposable(view.scope()) + .subscribe { partId -> messageRepo.savePart(partId)?.let(navigator::shareFile) } } -} \ No newline at end of file +} diff --git a/presentation/src/main/res/menu/gallery.xml b/presentation/src/main/res/menu/gallery.xml index 00793241b..9cd668b49 100644 --- a/presentation/src/main/res/menu/gallery.xml +++ b/presentation/src/main/res/menu/gallery.xml @@ -23,4 +23,8 @@ android:id="@+id/save" android:title="@string/menu_save" /> - \ No newline at end of file + + + diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index e5a9ded6e..add3415e2 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -38,6 +38,7 @@ Call Details Save to gallery + Share Open navigation drawer %d selected -- GitLab From d9fba04dc4b4341ab4aef8c322df7de5b65cd42d Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Sun, 24 Nov 2019 14:38:18 -0500 Subject: [PATCH 044/109] Refactor ContactSync to SyncContacts --- .../moez/QKSMS/interactor/{ContactSync.kt => SyncContacts.kt} | 2 +- .../java/com/moez/QKSMS/feature/contacts/ContactsViewModel.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename domain/src/main/java/com/moez/QKSMS/interactor/{ContactSync.kt => SyncContacts.kt} (92%) diff --git a/domain/src/main/java/com/moez/QKSMS/interactor/ContactSync.kt b/domain/src/main/java/com/moez/QKSMS/interactor/SyncContacts.kt similarity index 92% rename from domain/src/main/java/com/moez/QKSMS/interactor/ContactSync.kt rename to domain/src/main/java/com/moez/QKSMS/interactor/SyncContacts.kt index 7cd97fc73..ba79168a8 100644 --- a/domain/src/main/java/com/moez/QKSMS/interactor/ContactSync.kt +++ b/domain/src/main/java/com/moez/QKSMS/interactor/SyncContacts.kt @@ -24,7 +24,7 @@ import timber.log.Timber import java.util.concurrent.TimeUnit import javax.inject.Inject -open class ContactSync @Inject constructor(private val syncManager: SyncRepository) : Interactor() { +class SyncContacts @Inject constructor(private val syncManager: SyncRepository) : Interactor() { override fun buildObservable(params: Unit): Flowable { return Flowable.just(System.currentTimeMillis()) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsViewModel.kt b/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsViewModel.kt index f5869ff1f..108b0ce5b 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsViewModel.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsViewModel.kt @@ -27,7 +27,7 @@ import com.moez.QKSMS.feature.compose.editing.ComposeItem import com.moez.QKSMS.feature.compose.editing.PhoneNumberAction import com.moez.QKSMS.filter.ContactFilter import com.moez.QKSMS.filter.ContactGroupFilter -import com.moez.QKSMS.interactor.ContactSync +import com.moez.QKSMS.interactor.SyncContacts import com.moez.QKSMS.interactor.SetDefaultPhoneNumber import com.moez.QKSMS.model.Contact import com.moez.QKSMS.model.ContactGroup @@ -50,7 +50,7 @@ import javax.inject.Inject class ContactsViewModel @Inject constructor( sharing: Boolean, serializedChips: HashMap, - syncContacts: ContactSync, + syncContacts: SyncContacts, private val contactFilter: ContactFilter, private val contactGroupFilter: ContactGroupFilter, private val contactsRepo: ContactRepository, -- GitLab From 92cc3b7a4cda4475cc4a094359408c03e91243e6 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Sun, 24 Nov 2019 15:30:25 -0500 Subject: [PATCH 045/109] Keep contacts in sync Fixes #329 --- .../listener/ContactAddedListenerImpl.kt | 15 ++--- .../QKSMS/repository/SyncRepositoryImpl.kt | 30 +--------- .../com/moez/QKSMS/interactor/SyncContacts.kt | 4 +- .../QKSMS/listener/ContactAddedListener.kt | 4 +- .../moez/QKSMS/repository/SyncRepository.kt | 11 +--- .../feature/contacts/ContactsViewModel.kt | 6 -- .../ConversationInfoPresenter.kt | 16 +---- .../com/moez/QKSMS/feature/main/MainState.kt | 2 +- .../moez/QKSMS/feature/main/MainViewModel.kt | 59 ++++++++++++------- .../QKSMS/feature/settings/SettingsState.kt | 2 +- 10 files changed, 54 insertions(+), 95 deletions(-) diff --git a/data/src/main/java/com/moez/QKSMS/listener/ContactAddedListenerImpl.kt b/data/src/main/java/com/moez/QKSMS/listener/ContactAddedListenerImpl.kt index 605b623c9..1d1f74339 100644 --- a/data/src/main/java/com/moez/QKSMS/listener/ContactAddedListenerImpl.kt +++ b/data/src/main/java/com/moez/QKSMS/listener/ContactAddedListenerImpl.kt @@ -23,33 +23,28 @@ import android.database.ContentObserver import android.net.Uri import android.os.Handler import android.provider.ContactsContract -import com.moez.QKSMS.repository.SyncRepository import io.reactivex.Observable -import io.reactivex.subjects.PublishSubject +import io.reactivex.subjects.BehaviorSubject import javax.inject.Inject /** * Listens for a contact being added, and then syncs it to Realm - * - * TODO: Stop listening automatically. Currently, this will only happen if the contact is added */ class ContactAddedListenerImpl @Inject constructor( - private val context: Context, - private val syncRepo: SyncRepository + private val context: Context ) : ContactAddedListener { companion object { private val URI = ContactsContract.CommonDataKinds.Phone.CONTENT_URI } - override fun listen(address: String): Observable<*> { + override fun listen(): Observable<*> { return ContactContentObserver(context).observable - .filter { syncRepo.syncContact(address) } } private class ContactContentObserver(context: Context) : ContentObserver(Handler()) { - private val subject = PublishSubject.create() + private val subject = BehaviorSubject.createDefault(Unit) val observable: Observable = subject .doOnSubscribe { context.contentResolver.registerContentObserver(URI, true, this) } @@ -66,4 +61,4 @@ class ContactAddedListenerImpl @Inject constructor( } -} \ No newline at end of file +} diff --git a/data/src/main/java/com/moez/QKSMS/repository/SyncRepositoryImpl.kt b/data/src/main/java/com/moez/QKSMS/repository/SyncRepositoryImpl.kt index 0b1f2362f..8cb37238b 100644 --- a/data/src/main/java/com/moez/QKSMS/repository/SyncRepositoryImpl.kt +++ b/data/src/main/java/com/moez/QKSMS/repository/SyncRepositoryImpl.kt @@ -65,7 +65,7 @@ class SyncRepositoryImpl @Inject constructor( ) : SyncRepository { override val syncProgress: Subject = - BehaviorSubject.createDefault(SyncRepository.SyncProgress.Idle()) + BehaviorSubject.createDefault(SyncRepository.SyncProgress.Idle) override fun syncMessages() { @@ -185,7 +185,7 @@ class SyncRepositoryImpl @Inject constructor( // Only delete this after the sync has successfully completed oldBlockedSenders.delete() - syncProgress.onNext(SyncRepository.SyncProgress.Idle()) + syncProgress.onNext(SyncRepository.SyncProgress.Idle) } override fun syncMessage(uri: Uri): Message? { @@ -259,32 +259,6 @@ class SyncRepositoryImpl @Inject constructor( } } - override fun syncContact(address: String): Boolean { - // See if there's a contact that matches this phone number - var contact = getContacts().find { contact -> - contact.numbers.any { number -> phoneNumberUtils.compare(number.address, address) } - } ?: return false - - Realm.getDefaultInstance().use { realm -> - val recipients = realm.where(Recipient::class.java).findAll().filter { recipient -> - contact.numbers.any { number -> - phoneNumberUtils.compare(recipient.address, number.address) - } - } - - realm.executeTransaction { - contact = realm.copyToRealmOrUpdate(contact) - - // Update all the matching recipients with the new contact - recipients.forEach { recipient -> recipient.contact = contact } - - realm.insertOrUpdate(recipients) - } - } - - return true - } - private fun getContacts(): List { val defaultNumberIds = Realm.getDefaultInstance().use { realm -> realm.where(PhoneNumber::class.java) diff --git a/domain/src/main/java/com/moez/QKSMS/interactor/SyncContacts.kt b/domain/src/main/java/com/moez/QKSMS/interactor/SyncContacts.kt index ba79168a8..45fd8874e 100644 --- a/domain/src/main/java/com/moez/QKSMS/interactor/SyncContacts.kt +++ b/domain/src/main/java/com/moez/QKSMS/interactor/SyncContacts.kt @@ -21,7 +21,6 @@ package com.moez.QKSMS.interactor import com.moez.QKSMS.repository.SyncRepository import io.reactivex.Flowable import timber.log.Timber -import java.util.concurrent.TimeUnit import javax.inject.Inject class SyncContacts @Inject constructor(private val syncManager: SyncRepository) : Interactor() { @@ -30,8 +29,7 @@ class SyncContacts @Inject constructor(private val syncManager: SyncRepository) return Flowable.just(System.currentTimeMillis()) .doOnNext { syncManager.syncContacts() } .map { startTime -> System.currentTimeMillis() - startTime } - .map { elapsed -> TimeUnit.MILLISECONDS.toSeconds(elapsed) } - .doOnNext { seconds -> Timber.v("Completed sync in $seconds seconds") } + .doOnNext { duration -> Timber.v("Completed sync in ${duration}ms") } } } \ No newline at end of file diff --git a/domain/src/main/java/com/moez/QKSMS/listener/ContactAddedListener.kt b/domain/src/main/java/com/moez/QKSMS/listener/ContactAddedListener.kt index 1d2d1c507..800d83460 100644 --- a/domain/src/main/java/com/moez/QKSMS/listener/ContactAddedListener.kt +++ b/domain/src/main/java/com/moez/QKSMS/listener/ContactAddedListener.kt @@ -22,6 +22,6 @@ import io.reactivex.Observable interface ContactAddedListener { - fun listen(address: String): Observable<*> + fun listen(): Observable<*> -} \ No newline at end of file +} diff --git a/domain/src/main/java/com/moez/QKSMS/repository/SyncRepository.kt b/domain/src/main/java/com/moez/QKSMS/repository/SyncRepository.kt index 0638557a8..f8f8b7def 100644 --- a/domain/src/main/java/com/moez/QKSMS/repository/SyncRepository.kt +++ b/domain/src/main/java/com/moez/QKSMS/repository/SyncRepository.kt @@ -25,7 +25,7 @@ import io.reactivex.Observable interface SyncRepository { sealed class SyncProgress { - class Idle : SyncProgress() + object Idle : SyncProgress() data class Running(val max: Int, val progress: Int, val indeterminate: Boolean) : SyncProgress() } @@ -37,11 +37,4 @@ interface SyncRepository { fun syncContacts() - /** - * Syncs a single contact to the Realm - * - * Return false if the contact couldn't be found - */ - fun syncContact(address: String): Boolean - -} \ No newline at end of file +} diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsViewModel.kt b/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsViewModel.kt index 108b0ce5b..cb9386f29 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsViewModel.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsViewModel.kt @@ -27,7 +27,6 @@ import com.moez.QKSMS.feature.compose.editing.ComposeItem import com.moez.QKSMS.feature.compose.editing.PhoneNumberAction import com.moez.QKSMS.filter.ContactFilter import com.moez.QKSMS.filter.ContactGroupFilter -import com.moez.QKSMS.interactor.SyncContacts import com.moez.QKSMS.interactor.SetDefaultPhoneNumber import com.moez.QKSMS.model.Contact import com.moez.QKSMS.model.ContactGroup @@ -50,7 +49,6 @@ import javax.inject.Inject class ContactsViewModel @Inject constructor( sharing: Boolean, serializedChips: HashMap, - syncContacts: SyncContacts, private val contactFilter: ContactFilter, private val contactGroupFilter: ContactGroupFilter, private val contactsRepo: ContactRepository, @@ -76,10 +74,6 @@ class ContactsViewModel @Inject constructor( private var shouldOpenKeyboard: Boolean = true - init { - syncContacts.execute(Unit) - } - override fun bindView(view: ContactsContract) { super.bindView(view) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoPresenter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoPresenter.kt index 4f6029021..06c44e0ad 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoPresenter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoPresenter.kt @@ -26,14 +26,12 @@ import com.moez.QKSMS.extensions.mapNotNull import com.moez.QKSMS.interactor.DeleteConversations import com.moez.QKSMS.interactor.MarkArchived import com.moez.QKSMS.interactor.MarkUnarchived -import com.moez.QKSMS.listener.ContactAddedListener import com.moez.QKSMS.manager.PermissionManager import com.moez.QKSMS.model.Conversation import com.moez.QKSMS.repository.ConversationRepository import com.moez.QKSMS.repository.MessageRepository import com.uber.autodispose.android.lifecycle.scope import com.uber.autodispose.autoDisposable -import io.reactivex.Observable import io.reactivex.rxkotlin.plusAssign import io.reactivex.rxkotlin.withLatestFrom import io.reactivex.subjects.BehaviorSubject @@ -44,7 +42,6 @@ import javax.inject.Named class ConversationInfoPresenter @Inject constructor( @Named("threadId") threadId: Long, messageRepo: MessageRepository, - private val contactAddedListener: ContactAddedListener, private val conversationRepo: ConversationRepository, private val deleteConversations: DeleteConversations, private val markArchived: MarkArchived, @@ -105,16 +102,9 @@ class ConversationInfoPresenter @Inject constructor( // Add or display the contact view.recipientClicks() .mapNotNull(conversationRepo::getRecipient) - .flatMap { recipient -> - val lookupKey = recipient.contact?.lookupKey - if (lookupKey != null) { - navigator.showContact(lookupKey) - Observable.empty() - } else { - // Allow the user to add the contact, then listen for changes - navigator.addContact(recipient.address) - contactAddedListener.listen(recipient.address) - } + .doOnNext { recipient -> + recipient.contact?.lookupKey?.let(navigator::showContact) + ?: navigator.addContact(recipient.address) } .autoDisposable(view.scope(Lifecycle.Event.ON_DESTROY)) // ... this should be the default .subscribe() diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainState.kt b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainState.kt index 39cf39aeb..432ec2dcd 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainState.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainState.kt @@ -28,7 +28,7 @@ data class MainState( val page: MainPage = Inbox(), val drawerOpen: Boolean = false, val showRating: Boolean = false, - val syncing: SyncRepository.SyncProgress = SyncRepository.SyncProgress.Idle(), + val syncing: SyncRepository.SyncProgress = SyncRepository.SyncProgress.Idle, val defaultSms: Boolean = true, val smsPermission: Boolean = true, val contactPermission: Boolean = true diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainViewModel.kt b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainViewModel.kt index 36156119c..4963cce60 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainViewModel.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainViewModel.kt @@ -25,7 +25,17 @@ import com.moez.QKSMS.common.Navigator import com.moez.QKSMS.common.base.QkViewModel import com.moez.QKSMS.common.util.BillingManager import com.moez.QKSMS.extensions.mapNotNull -import com.moez.QKSMS.interactor.* +import com.moez.QKSMS.interactor.DeleteConversations +import com.moez.QKSMS.interactor.MarkAllSeen +import com.moez.QKSMS.interactor.MarkArchived +import com.moez.QKSMS.interactor.MarkPinned +import com.moez.QKSMS.interactor.MarkRead +import com.moez.QKSMS.interactor.MarkUnarchived +import com.moez.QKSMS.interactor.MarkUnpinned +import com.moez.QKSMS.interactor.MarkUnread +import com.moez.QKSMS.interactor.MigratePreferences +import com.moez.QKSMS.interactor.SyncContacts +import com.moez.QKSMS.interactor.SyncMessages import com.moez.QKSMS.listener.ContactAddedListener import com.moez.QKSMS.manager.ChangelogManager import com.moez.QKSMS.manager.PermissionManager @@ -46,26 +56,26 @@ import javax.inject.Inject import javax.inject.Named class MainViewModel @Inject constructor( - @Named("threadId") private val threadId: Long, - markAllSeen: MarkAllSeen, - migratePreferences: MigratePreferences, - syncRepository: SyncRepository, - private val contactAddedListener: ContactAddedListener, - private val changelogManager: ChangelogManager, - private val conversationRepo: ConversationRepository, - private val deleteConversations: DeleteConversations, - private val markArchived: MarkArchived, - private val markBlocked: MarkBlocked, - private val markPinned: MarkPinned, - private val markRead: MarkRead, - private val markUnarchived: MarkUnarchived, - private val markUnpinned: MarkUnpinned, - private val markUnread: MarkUnread, - private val navigator: Navigator, - private val permissionManager: PermissionManager, - private val prefs: Preferences, - private val syncMessages: SyncMessages, - private val syncContacts: ContactSync + @Named("threadId") private val threadId: Long, + billingManager: BillingManager, + contactAddedListener: ContactAddedListener, + markAllSeen: MarkAllSeen, + migratePreferences: MigratePreferences, + syncRepository: SyncRepository, + private val changelogManager: ChangelogManager, + private val conversationRepo: ConversationRepository, + private val deleteConversations: DeleteConversations, + private val markArchived: MarkArchived, + private val markPinned: MarkPinned, + private val markRead: MarkRead, + private val markUnarchived: MarkUnarchived, + private val markUnpinned: MarkUnpinned, + private val markUnread: MarkUnread, + private val navigator: Navigator, + private val permissionManager: PermissionManager, + private val prefs: Preferences, + private val syncContacts: SyncContacts, + private val syncMessages: SyncMessages ) : QkViewModel(MainState(page = Inbox(data = conversationRepo.getConversations()))) { init { @@ -74,6 +84,7 @@ class MainViewModel @Inject constructor( disposables += markArchived disposables += markUnarchived disposables += migratePreferences + disposables += syncContacts disposables += syncMessages // Show the syncing UI @@ -95,6 +106,11 @@ class MainViewModel @Inject constructor( syncMessages.execute(Unit) } + // Sync contacts when we detect a change + disposables += contactAddedListener.listen() + .debounce(1, TimeUnit.SECONDS) + .subscribeOn(Schedulers.io()) + .subscribe { syncContacts.execute(Unit) } markAllSeen.execute(Unit) } @@ -272,7 +288,6 @@ class MainViewModel @Inject constructor( .map { conversation -> conversation.recipients } .mapNotNull { recipients -> recipients[0]?.address?.takeIf { recipients.size == 1 } } .doOnNext(navigator::addContact) - .flatMap(contactAddedListener::listen) .autoDisposable(view.scope()) .subscribe() diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsState.kt b/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsState.kt index d2a359957..f78a841f5 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsState.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsState.kt @@ -42,5 +42,5 @@ data class SettingsState( val mobileOnly: Boolean = false, val maxMmsSizeSummary: String = "100KB", val maxMmsSizeId: Int = 100, - val syncProgress: SyncRepository.SyncProgress = SyncRepository.SyncProgress.Idle() + val syncProgress: SyncRepository.SyncProgress = SyncRepository.SyncProgress.Idle ) \ No newline at end of file -- GitLab From b4f562f9526922a2dea9d20a862fc2fa015127f3 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Sun, 24 Nov 2019 16:37:39 -0500 Subject: [PATCH 046/109] Fix notification sounds not working on 7.0 --- .../QKSMS/common/util/NotificationManagerImpl.kt | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/presentation/src/main/java/com/moez/QKSMS/common/util/NotificationManagerImpl.kt b/presentation/src/main/java/com/moez/QKSMS/common/util/NotificationManagerImpl.kt index 4fa3a3921..1f142e79f 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/util/NotificationManagerImpl.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/util/NotificationManagerImpl.kt @@ -19,6 +19,7 @@ package com.moez.QKSMS.common.util import android.annotation.SuppressLint +import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent @@ -26,6 +27,7 @@ import android.content.ContentUris import android.content.Context import android.content.Intent import android.graphics.Color +import android.media.AudioAttributes import android.net.Uri import android.os.Build import android.provider.ContactsContract @@ -112,8 +114,8 @@ class NotificationManagerImpl @Inject constructor( val contentIntent = Intent(context, ComposeActivity::class.java).putExtra("threadId", threadId) val taskStackBuilder = TaskStackBuilder.create(context) - taskStackBuilder.addParentStack(ComposeActivity::class.java) - taskStackBuilder.addNextIntent(contentIntent) + .addParentStack(ComposeActivity::class.java) + .addNextIntent(contentIntent) val contentPI = taskStackBuilder.getPendingIntent(threadId.toInt() + 10000, PendingIntent.FLAG_UPDATE_CURRENT) val seenIntent = Intent(context, MarkSeenReceiver::class.java).putExtra("threadId", threadId) @@ -123,6 +125,10 @@ class NotificationManagerImpl @Inject constructor( val ringtone = prefs.ringtone(threadId).get() .takeIf { it.isNotEmpty() } ?.let(Uri::parse) + ?.also { uri -> + // https://commonsware.com/blog/2016/09/07/notifications-sounds-android-7p0-aggravation.html + context.grantUriPermission("com.android.systemui", uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) + } val notification = NotificationCompat.Builder(context, getChannelIdForNotification(threadId)) .setCategory(NotificationCompat.CATEGORY_MESSAGE) @@ -342,6 +348,11 @@ class NotificationManagerImpl @Inject constructor( lightColor = Color.WHITE enableVibration(true) vibrationPattern = VIBRATE_PATTERN + lockscreenVisibility = Notification.VISIBILITY_PUBLIC + setSound(prefs.ringtone().get().let(Uri::parse), AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_NOTIFICATION) + .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) + .build()) } notificationManager.createNotificationChannel(channel) -- GitLab From 4cd78463311f8fe42c227fc3756df9e5c4c35bbe Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Sun, 24 Nov 2019 16:53:40 -0500 Subject: [PATCH 047/109] Darker bubbles --- presentation/src/main/res/values/colors.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/presentation/src/main/res/values/colors.xml b/presentation/src/main/res/values/colors.xml index 46f6941e1..f359c4c05 100644 --- a/presentation/src/main/res/values/colors.xml +++ b/presentation/src/main/res/values/colors.xml @@ -44,7 +44,7 @@ #80ffffff #ECEFF1 - #151B1F + #11171B #0F1113 #0C000000 -- GitLab From b1f3d430350286cd9570a6c054f7c0bf7e562e5b Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Sun, 24 Nov 2019 17:20:04 -0500 Subject: [PATCH 048/109] Add setting to wake screen Closes #1048 --- .../main/java/com/moez/QKSMS/util/Preferences.kt | 9 +++++++++ .../QKSMS/common/util/NotificationManagerImpl.kt | 13 +++++++++++++ .../notificationprefs/NotificationPrefsActivity.kt | 1 + .../notificationprefs/NotificationPrefsState.kt | 1 + .../notificationprefs/NotificationPrefsViewModel.kt | 6 ++++++ .../main/res/layout/notification_prefs_activity.xml | 7 +++++++ presentation/src/main/res/values/strings.xml | 1 + 7 files changed, 38 insertions(+) diff --git a/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt b/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt index 5e73c2896..2acfe37f3 100644 --- a/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt +++ b/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt @@ -145,6 +145,15 @@ class Preferences @Inject constructor(context: Context, private val rxPrefs: RxS } } + fun wakeScreen(threadId: Long = 0): Preference { + val default = rxPrefs.getBoolean("wake", false) + + return when (threadId) { + 0L -> default + else -> rxPrefs.getBoolean("wake_$threadId", default.get()) + } + } + fun vibration(threadId: Long = 0): Preference { val default = rxPrefs.getBoolean("vibration", true) diff --git a/presentation/src/main/java/com/moez/QKSMS/common/util/NotificationManagerImpl.kt b/presentation/src/main/java/com/moez/QKSMS/common/util/NotificationManagerImpl.kt index 1f142e79f..7bbcb6aeb 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/util/NotificationManagerImpl.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/util/NotificationManagerImpl.kt @@ -30,12 +30,14 @@ import android.graphics.Color import android.media.AudioAttributes import android.net.Uri import android.os.Build +import android.os.PowerManager import android.provider.ContactsContract import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.app.Person import androidx.core.app.RemoteInput import androidx.core.app.TaskStackBuilder +import androidx.core.content.getSystemService import androidx.core.graphics.drawable.IconCompat import com.moez.QKSMS.R import com.moez.QKSMS.common.util.extensions.dpToPx @@ -279,6 +281,17 @@ class NotificationManagerImpl @Inject constructor( } notificationManager.notify(threadId.toInt(), notification.build()) + + // Wake screen + if (prefs.wakeScreen(threadId).get()) { + context.getSystemService()?.let { powerManager -> + if (!powerManager.isInteractive) { + val flags = PowerManager.SCREEN_DIM_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP + val wakeLock = powerManager.newWakeLock(flags, context.packageName) + wakeLock.acquire(5000) + } + } + } } override fun notifyFailed(msgId: Long) { diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/notificationprefs/NotificationPrefsActivity.kt b/presentation/src/main/java/com/moez/QKSMS/feature/notificationprefs/NotificationPrefsActivity.kt index b998c1d97..210b5d3fe 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/notificationprefs/NotificationPrefsActivity.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/notificationprefs/NotificationPrefsActivity.kt @@ -98,6 +98,7 @@ class NotificationPrefsActivity : QkThemedActivity(), NotificationPrefsView { notifications.checkbox.isChecked = state.notificationsEnabled previews.summary = state.previewSummary previewModeDialog.adapter.selectedItem = state.previewId + wake.checkbox.isChecked = state.wakeEnabled vibration.checkbox.isChecked = state.vibrationEnabled ringtone.summary = state.ringtoneName diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/notificationprefs/NotificationPrefsState.kt b/presentation/src/main/java/com/moez/QKSMS/feature/notificationprefs/NotificationPrefsState.kt index e5e077ea2..c0dcd0f24 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/notificationprefs/NotificationPrefsState.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/notificationprefs/NotificationPrefsState.kt @@ -27,6 +27,7 @@ data class NotificationPrefsState( val notificationsEnabled: Boolean = true, val previewSummary: String = "", val previewId: Int = Preferences.NOTIFICATION_PREVIEWS_ALL, + val wakeEnabled: Boolean = false, val action1Summary: String = "", val action2Summary: String = "", val action3Summary: String = "", diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/notificationprefs/NotificationPrefsViewModel.kt b/presentation/src/main/java/com/moez/QKSMS/feature/notificationprefs/NotificationPrefsViewModel.kt index 6639d4f70..596423bb3 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/notificationprefs/NotificationPrefsViewModel.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/notificationprefs/NotificationPrefsViewModel.kt @@ -46,6 +46,7 @@ class NotificationPrefsViewModel @Inject constructor( private val notifications = prefs.notifications(threadId) private val previews = prefs.notificationPreviews(threadId) + private val wake = prefs.wakeScreen(threadId) private val vibration = prefs.vibration(threadId) private val ringtone = prefs.ringtone(threadId) @@ -75,6 +76,9 @@ class NotificationPrefsViewModel @Inject constructor( disposables += prefs.notifAction3.asObservable() .subscribe { previewId -> newState { copy(action3Summary = actionLabels[previewId]) } } + disposables += wake.asObservable() + .subscribe { enabled -> newState { copy(wakeEnabled = enabled) } } + disposables += vibration.asObservable() .subscribe { enabled -> newState { copy(vibrationEnabled = enabled) } } @@ -107,6 +111,8 @@ class NotificationPrefsViewModel @Inject constructor( R.id.previews -> view.showPreviewModeDialog() + R.id.wake -> wake.set(!wake.get()) + R.id.vibration -> vibration.set(!vibration.get()) R.id.ringtone -> view.showRingtonePicker(ringtone.get().takeIf { it.isNotEmpty() }?.let(Uri::parse)) diff --git a/presentation/src/main/res/layout/notification_prefs_activity.xml b/presentation/src/main/res/layout/notification_prefs_activity.xml index 5f130300d..497490339 100644 --- a/presentation/src/main/res/layout/notification_prefs_activity.xml +++ b/presentation/src/main/res/layout/notification_prefs_activity.xml @@ -72,6 +72,13 @@ app:title="@string/settings_notification_previews_title" tools:summary="Show name and message" /> + + Button 2 Button 3 Notification previews + Wake screen Vibration Sound None -- GitLab From 3a544bc755a8736236185846de61e6363684d59d Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Sun, 15 Dec 2019 20:46:33 -0500 Subject: [PATCH 049/109] Per-contact colours Closes #1120 --- .../moez/QKSMS/migration/QkRealmMigration.kt | 23 ++++- .../QKSMS/repository/MessageRepositoryImpl.kt | 95 +++++++++++-------- .../moez/QKSMS/manager/NotificationManager.kt | 2 +- .../QKSMS/repository/MessageRepository.kt | 2 + .../java/com/moez/QKSMS/util/Preferences.kt | 32 ++++++- .../QKSMS/common/base/QkThemedActivity.kt | 42 +++++++- .../java/com/moez/QKSMS/common/util/Colors.kt | 6 +- .../common/util/NotificationManagerImpl.kt | 20 +++- .../moez/QKSMS/common/widget/AvatarView.kt | 8 +- .../QKSMS/common/widget/GroupAvatarView.kt | 20 ++-- .../QKSMS/common/widget/PagerTitleView.kt | 10 +- .../messages/BlockedMessagesAdapter.kt | 2 +- .../QKSMS/feature/compose/ComposeActivity.kt | 6 +- .../QKSMS/feature/compose/ComposeState.kt | 2 +- .../QKSMS/feature/compose/ComposeViewModel.kt | 8 +- .../QKSMS/feature/compose/MessagesAdapter.kt | 18 ++-- .../compose/editing/ComposeItemAdapter.kt | 10 +- .../ConversationInfoController.kt | 19 ++-- .../ConversationInfoPresenter.kt | 13 ++- .../conversationinfo/ConversationInfoView.kt | 6 +- .../ConversationRecipientAdapter.kt | 29 +++--- .../conversations/ConversationsAdapter.kt | 20 +++- .../moez/QKSMS/feature/main/MainActivity.kt | 5 +- .../moez/QKSMS/feature/main/SearchAdapter.kt | 2 +- .../QKSMS/feature/qkreply/QkReplyActivity.kt | 2 +- .../QKSMS/feature/qkreply/QkReplyState.kt | 2 +- .../QKSMS/feature/qkreply/QkReplyViewModel.kt | 2 +- .../scheduled/ScheduledMessageAdapter.kt | 2 +- .../themepicker/ThemePickerController.kt | 6 +- .../themepicker/ThemePickerPresenter.kt | 12 ++- .../feature/themepicker/ThemePickerState.kt | 2 +- .../injection/ThemePickerModule.kt | 4 +- .../com/moez/QKSMS/injection/AppModule.kt | 10 +- .../layout/conversation_info_controller.xml | 9 +- .../conversation_recipient_list_item.xml | 19 +++- 35 files changed, 317 insertions(+), 153 deletions(-) diff --git a/data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt b/data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt index 5dcfffc0e..be9f96872 100644 --- a/data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt +++ b/data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt @@ -18,6 +18,8 @@ */ package com.moez.QKSMS.migration +import android.annotation.SuppressLint +import com.f2prateek.rx.preferences2.RxSharedPreferences import com.moez.QKSMS.extensions.map import com.moez.QKSMS.mapper.CursorToContactImpl import io.realm.DynamicRealm @@ -29,13 +31,15 @@ import io.realm.Sort import javax.inject.Inject class QkRealmMigration @Inject constructor( - private val cursorToContact: CursorToContactImpl + private val cursorToContact: CursorToContactImpl, + private val prefs: RxSharedPreferences ) : RealmMigration { companion object { const val SchemaVersion: Long = 9 } + @SuppressLint("ApplySharedPref") override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { var version = oldVersion @@ -169,6 +173,23 @@ class QkRealmMigration @Inject constructor( realmContact.setString("photoUri", photoUri) } + // Migrate conversation themes + val recipients = mutableMapOf() // Map of recipientId:theme + realm.where("Conversation").findAll().forEach { conversation -> + val pref = prefs.getInteger("theme_${conversation.getLong("id")}") + if (pref.isSet) { + conversation.getList("Recipient").forEach { recipient -> + recipients[recipient.getLong("id")] = pref.get() + } + + pref.delete() + } + } + + recipients.forEach { (recipientId, theme) -> + prefs.getInteger("theme_$recipientId").set(theme) + } + version++ } diff --git a/data/src/main/java/com/moez/QKSMS/repository/MessageRepositoryImpl.kt b/data/src/main/java/com/moez/QKSMS/repository/MessageRepositoryImpl.kt index 9ec414cea..2b4ba0687 100644 --- a/data/src/main/java/com/moez/QKSMS/repository/MessageRepositoryImpl.kt +++ b/data/src/main/java/com/moez/QKSMS/repository/MessageRepositoryImpl.kt @@ -28,6 +28,8 @@ import android.media.MediaScannerConnection import android.os.Build import android.os.Environment import android.provider.Telephony +import android.provider.Telephony.Mms +import android.provider.Telephony.Sms import android.telephony.SmsManager import android.webkit.MimeTypeMap import androidx.core.content.contentValuesOf @@ -110,6 +112,25 @@ class MessageRepositoryImpl @Inject constructor( .findFirst() } + override fun getLastIncomingMessage(threadId: Long): RealmResults { + return Realm.getDefaultInstance() + .where(Message::class.java) + .equalTo("threadId", threadId) + .beginGroup() + .beginGroup() + .equalTo("type", "sms") + .`in`("boxId", arrayOf(Sms.MESSAGE_TYPE_INBOX, Sms.MESSAGE_TYPE_ALL)) + .endGroup() + .or() + .beginGroup() + .equalTo("type", "mms") + .`in`("boxId", arrayOf(Mms.MESSAGE_BOX_INBOX, Mms.MESSAGE_BOX_ALL)) + .endGroup() + .endGroup() + .sort("date", Sort.DESCENDING) + .findAll() + } + override fun getUnreadCount(): Long { return Realm.getDefaultInstance().use { realm -> realm.refresh() @@ -239,13 +260,13 @@ class MessageRepositoryImpl @Inject constructor( } val values = ContentValues() - values.put(Telephony.Sms.SEEN, true) - values.put(Telephony.Sms.READ, true) + values.put(Sms.SEEN, true) + values.put(Sms.READ, true) threadIds.forEach { threadId -> try { val uri = ContentUris.withAppendedId(Telephony.MmsSms.CONTENT_CONVERSATIONS_URI, threadId) - context.contentResolver.update(uri, values, "${Telephony.Sms.READ} = 0", null) + context.contentResolver.update(uri, values, "${Sms.READ} = 0", null) } catch (exception: Exception) { Timber.w(exception) } @@ -421,7 +442,7 @@ class MessageRepositoryImpl @Inject constructor( this.subId = subId id = messageIds.newId() - boxId = Telephony.Sms.MESSAGE_TYPE_OUTBOX + boxId = Sms.MESSAGE_TYPE_OUTBOX type = "sms" read = true seen = true @@ -432,20 +453,20 @@ class MessageRepositoryImpl @Inject constructor( // Insert the message to the native content provider val values = contentValuesOf( - Telephony.Sms.ADDRESS to address, - Telephony.Sms.BODY to body, - Telephony.Sms.DATE to System.currentTimeMillis(), - Telephony.Sms.READ to true, - Telephony.Sms.SEEN to true, - Telephony.Sms.TYPE to Telephony.Sms.MESSAGE_TYPE_OUTBOX, - Telephony.Sms.THREAD_ID to threadId + Sms.ADDRESS to address, + Sms.BODY to body, + Sms.DATE to System.currentTimeMillis(), + Sms.READ to true, + Sms.SEEN to true, + Sms.TYPE to Sms.MESSAGE_TYPE_OUTBOX, + Sms.THREAD_ID to threadId ) if (prefs.canUseSubId.get()) { - values.put(Telephony.Sms.SUBSCRIPTION_ID, message.subId) + values.put(Sms.SUBSCRIPTION_ID, message.subId) } - val uri = context.contentResolver.insert(Telephony.Sms.CONTENT_URI, values) + val uri = context.contentResolver.insert(Sms.CONTENT_URI, values) // Update the contentId after the message has been inserted to the content provider // The message might have been deleted by now, so only proceed if it's valid @@ -479,7 +500,7 @@ class MessageRepositoryImpl @Inject constructor( id = messageIds.newId() threadId = TelephonyCompat.getOrCreateThreadId(context, address) - boxId = Telephony.Sms.MESSAGE_TYPE_INBOX + boxId = Sms.MESSAGE_TYPE_INBOX type = "sms" read = activeConversationManager.getActiveConversation() == threadId } @@ -489,16 +510,16 @@ class MessageRepositoryImpl @Inject constructor( // Insert the message to the native content provider val values = contentValuesOf( - Telephony.Sms.ADDRESS to address, - Telephony.Sms.BODY to body, - Telephony.Sms.DATE_SENT to sentTime + Sms.ADDRESS to address, + Sms.BODY to body, + Sms.DATE_SENT to sentTime ) if (prefs.canUseSubId.get()) { - values.put(Telephony.Sms.SUBSCRIPTION_ID, message.subId) + values.put(Sms.SUBSCRIPTION_ID, message.subId) } - context.contentResolver.insert(Telephony.Sms.Inbox.CONTENT_URI, values)?.lastPathSegment?.toLong()?.let { id -> + context.contentResolver.insert(Sms.Inbox.CONTENT_URI, values)?.lastPathSegment?.toLong()?.let { id -> // Update the contentId after the message has been inserted to the content provider realm.executeTransaction { managedMessage?.contentId = id } } @@ -520,15 +541,15 @@ class MessageRepositoryImpl @Inject constructor( // Update the message in realm realm.executeTransaction { message.boxId = when (message.isSms()) { - true -> Telephony.Sms.MESSAGE_TYPE_OUTBOX - false -> Telephony.Mms.MESSAGE_BOX_OUTBOX + true -> Sms.MESSAGE_TYPE_OUTBOX + false -> Mms.MESSAGE_BOX_OUTBOX } } // Update the message in the native ContentProvider val values = when (message.isSms()) { - true -> contentValuesOf(Telephony.Sms.TYPE to Telephony.Sms.MESSAGE_TYPE_OUTBOX) - false -> contentValuesOf(Telephony.Mms.MESSAGE_BOX to Telephony.Mms.MESSAGE_BOX_OUTBOX) + true -> contentValuesOf(Sms.TYPE to Sms.MESSAGE_TYPE_OUTBOX) + false -> contentValuesOf(Mms.MESSAGE_BOX to Mms.MESSAGE_BOX_OUTBOX) } context.contentResolver.update(message.getUri(), values, null, null) } @@ -543,12 +564,12 @@ class MessageRepositoryImpl @Inject constructor( message?.let { // Update the message in realm realm.executeTransaction { - message.boxId = Telephony.Sms.MESSAGE_TYPE_SENT + message.boxId = Sms.MESSAGE_TYPE_SENT } // Update the message in the native ContentProvider val values = ContentValues() - values.put(Telephony.Sms.TYPE, Telephony.Sms.MESSAGE_TYPE_SENT) + values.put(Sms.TYPE, Sms.MESSAGE_TYPE_SENT) context.contentResolver.update(message.getUri(), values, null, null) } } @@ -562,14 +583,14 @@ class MessageRepositoryImpl @Inject constructor( message?.let { // Update the message in realm realm.executeTransaction { - message.boxId = Telephony.Sms.MESSAGE_TYPE_FAILED + message.boxId = Sms.MESSAGE_TYPE_FAILED message.errorCode = resultCode } // Update the message in the native ContentProvider val values = ContentValues() - values.put(Telephony.Sms.TYPE, Telephony.Sms.MESSAGE_TYPE_FAILED) - values.put(Telephony.Sms.ERROR_CODE, resultCode) + values.put(Sms.TYPE, Sms.MESSAGE_TYPE_FAILED) + values.put(Sms.ERROR_CODE, resultCode) context.contentResolver.update(message.getUri(), values, null, null) } } @@ -583,16 +604,16 @@ class MessageRepositoryImpl @Inject constructor( message?.let { // Update the message in realm realm.executeTransaction { - message.deliveryStatus = Telephony.Sms.STATUS_COMPLETE + message.deliveryStatus = Sms.STATUS_COMPLETE message.dateSent = System.currentTimeMillis() message.read = true } // Update the message in the native ContentProvider val values = ContentValues() - values.put(Telephony.Sms.STATUS, Telephony.Sms.STATUS_COMPLETE) - values.put(Telephony.Sms.DATE_SENT, System.currentTimeMillis()) - values.put(Telephony.Sms.READ, true) + values.put(Sms.STATUS, Sms.STATUS_COMPLETE) + values.put(Sms.DATE_SENT, System.currentTimeMillis()) + values.put(Sms.READ, true) context.contentResolver.update(message.getUri(), values, null, null) } } @@ -606,7 +627,7 @@ class MessageRepositoryImpl @Inject constructor( message?.let { // Update the message in realm realm.executeTransaction { - message.deliveryStatus = Telephony.Sms.STATUS_FAILED + message.deliveryStatus = Sms.STATUS_FAILED message.dateSent = System.currentTimeMillis() message.read = true message.errorCode = resultCode @@ -614,10 +635,10 @@ class MessageRepositoryImpl @Inject constructor( // Update the message in the native ContentProvider val values = ContentValues() - values.put(Telephony.Sms.STATUS, Telephony.Sms.STATUS_FAILED) - values.put(Telephony.Sms.DATE_SENT, System.currentTimeMillis()) - values.put(Telephony.Sms.READ, true) - values.put(Telephony.Sms.ERROR_CODE, resultCode) + values.put(Sms.STATUS, Sms.STATUS_FAILED) + values.put(Sms.DATE_SENT, System.currentTimeMillis()) + values.put(Sms.READ, true) + values.put(Sms.ERROR_CODE, resultCode) context.contentResolver.update(message.getUri(), values, null, null) } } diff --git a/domain/src/main/java/com/moez/QKSMS/manager/NotificationManager.kt b/domain/src/main/java/com/moez/QKSMS/manager/NotificationManager.kt index 0a31695a7..7499d82ec 100644 --- a/domain/src/main/java/com/moez/QKSMS/manager/NotificationManager.kt +++ b/domain/src/main/java/com/moez/QKSMS/manager/NotificationManager.kt @@ -32,4 +32,4 @@ interface NotificationManager { fun getNotificationForBackup(): NotificationCompat.Builder -} \ No newline at end of file +} diff --git a/domain/src/main/java/com/moez/QKSMS/repository/MessageRepository.kt b/domain/src/main/java/com/moez/QKSMS/repository/MessageRepository.kt index afb1a339d..217e312a0 100644 --- a/domain/src/main/java/com/moez/QKSMS/repository/MessageRepository.kt +++ b/domain/src/main/java/com/moez/QKSMS/repository/MessageRepository.kt @@ -32,6 +32,8 @@ interface MessageRepository { fun getMessageForPart(id: Long): Message? + fun getLastIncomingMessage(threadId: Long): RealmResults + fun getUnreadCount(): Long fun getPart(id: Long): MmsPart? diff --git a/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt b/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt index 2acfe37f3..4dee89005 100644 --- a/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt +++ b/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt @@ -19,16 +19,22 @@ package com.moez.QKSMS.util import android.content.Context +import android.content.SharedPreferences import android.os.Build import android.provider.Settings import com.f2prateek.rx.preferences2.Preference import com.f2prateek.rx.preferences2.RxSharedPreferences import com.moez.QKSMS.common.util.extensions.versionCode +import io.reactivex.Observable import javax.inject.Inject import javax.inject.Singleton @Singleton -class Preferences @Inject constructor(context: Context, private val rxPrefs: RxSharedPreferences) { +class Preferences @Inject constructor( + context: Context, + private val rxPrefs: RxSharedPreferences, + private val sharedPrefs: SharedPreferences +) { companion object { const val NIGHT_MODE_SYSTEM = 0 @@ -118,12 +124,28 @@ class Preferences @Inject constructor(context: Context, private val rxPrefs: RxS } } - fun theme(threadId: Long = 0): Preference { + /** + * Returns a stream of preference keys for changing preferences + */ + val keyChanges: Observable = Observable.create { emitter -> + // Making this a lambda would cause it to be GCd + val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> + emitter.onNext(key) + } + + emitter.setCancellable { + sharedPrefs.unregisterOnSharedPreferenceChangeListener(listener) + } + + sharedPrefs.registerOnSharedPreferenceChangeListener(listener) + }.share() + + fun theme(recipientId: Long = 0): Preference { val default = rxPrefs.getInteger("theme", 0xFF0097A7.toInt()) - return when (threadId) { + return when (recipientId) { 0L -> default - else -> rxPrefs.getInteger("theme_$threadId", default.get()) + else -> rxPrefs.getInteger("theme_$recipientId", default.get()) } } @@ -171,4 +193,4 @@ class Preferences @Inject constructor(context: Context, private val rxPrefs: RxS else -> rxPrefs.getString("ringtone_$threadId", default.get()) } } -} \ No newline at end of file +} diff --git a/presentation/src/main/java/com/moez/QKSMS/common/base/QkThemedActivity.kt b/presentation/src/main/java/com/moez/QKSMS/common/base/QkThemedActivity.kt index 660881e2a..93a39aa33 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/base/QkThemedActivity.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/base/QkThemedActivity.kt @@ -30,6 +30,11 @@ import com.moez.QKSMS.R import com.moez.QKSMS.common.util.Colors import com.moez.QKSMS.common.util.extensions.resolveThemeBoolean import com.moez.QKSMS.common.util.extensions.resolveThemeColor +import com.moez.QKSMS.extensions.asObservable +import com.moez.QKSMS.extensions.mapNotNull +import com.moez.QKSMS.repository.ConversationRepository +import com.moez.QKSMS.repository.MessageRepository +import com.moez.QKSMS.util.PhoneNumberUtils import com.moez.QKSMS.util.Preferences import com.uber.autodispose.android.lifecycle.scope import com.uber.autodispose.autoDisposable @@ -51,6 +56,9 @@ import javax.inject.Inject abstract class QkThemedActivity : QkActivity() { @Inject lateinit var colors: Colors + @Inject lateinit var conversationRepo: ConversationRepository + @Inject lateinit var messageRepo: MessageRepository + @Inject lateinit var phoneNumberUtils: PhoneNumberUtils @Inject lateinit var prefs: Preferences /** @@ -61,10 +69,40 @@ abstract class QkThemedActivity : QkActivity() { /** * Switch the theme if the threadId changes + * Set it based on the latest message in the conversation */ - val theme = threadId + val theme: Observable = threadId .distinctUntilChanged() - .switchMap { threadId -> colors.themeObservable(threadId) } + .switchMap { threadId -> + val conversation = conversationRepo.getConversation(threadId) + when { + conversation == null -> Observable.just(0L) + + conversation.recipients.size == 1 -> Observable.just(conversation.recipients.first()?.id ?: 0L) + + else -> messageRepo.getLastIncomingMessage(conversation.id) + .asObservable() + .mapNotNull { messages -> messages.firstOrNull() } + .distinctUntilChanged { message -> message.address } + .mapNotNull { message -> + conversation.recipients.find { recipient -> + phoneNumberUtils.compare(recipient.address, message.address) + } + } + .map { recipient -> recipient.id } + .startWith(conversation.recipients.firstOrNull()?.id ?: 0) + .distinctUntilChanged() + } + } + .switchMap { colors.themeObservable(it) } + + /** + * Emits an event whenever any theme changes, whether it be global or for some recipient. This is useful + * for invalidating recyclerviews + */ + val allThemes by lazy { + prefs.keyChanges.filter { key -> key.contains("theme") } + } @SuppressLint("InlinedApi") override fun onCreate(savedInstanceState: Bundle?) { diff --git a/presentation/src/main/java/com/moez/QKSMS/common/util/Colors.kt b/presentation/src/main/java/com/moez/QKSMS/common/util/Colors.kt index fd0d9f1b4..5c94561d9 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/util/Colors.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/util/Colors.kt @@ -66,10 +66,10 @@ class Colors @Inject constructor(private val context: Context, private val prefs private val secondaryTextLuminance = measureLuminance(context.getColorCompat(R.color.textSecondaryDark)) private val tertiaryTextLuminance = measureLuminance(context.getColorCompat(R.color.textTertiaryDark)) - fun theme(threadId: Long = 0): Theme = Theme(prefs.theme(threadId).get(), this) + fun theme(recipientId: Long = 0): Theme = Theme(prefs.theme(recipientId).get(), this) - fun themeObservable(threadId: Long = 0): Observable { - return prefs.theme(threadId).asObservable() + fun themeObservable(recipientId: Long = 0): Observable { + return prefs.theme(recipientId).asObservable() .map { color -> Theme(color, this) } } diff --git a/presentation/src/main/java/com/moez/QKSMS/common/util/NotificationManagerImpl.kt b/presentation/src/main/java/com/moez/QKSMS/common/util/NotificationManagerImpl.kt index 7bbcb6aeb..3d6625fee 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/util/NotificationManagerImpl.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/util/NotificationManagerImpl.kt @@ -113,6 +113,11 @@ class NotificationManagerImpl @Inject constructor( } val conversation = conversationRepo.getConversation(threadId) ?: return + val lastRecipient = conversation.lastMessage?.let { lastMessage -> + conversation.recipients.find { recipient -> + phoneNumberUtils.compare(recipient.address, lastMessage.address) + } + } ?: conversation.recipients.firstOrNull() val contentIntent = Intent(context, ComposeActivity::class.java).putExtra("threadId", threadId) val taskStackBuilder = TaskStackBuilder.create(context) @@ -134,7 +139,7 @@ class NotificationManagerImpl @Inject constructor( val notification = NotificationCompat.Builder(context, getChannelIdForNotification(threadId)) .setCategory(NotificationCompat.CATEGORY_MESSAGE) - .setColor(colors.theme(threadId).theme) + .setColor(colors.theme(lastRecipient?.id ?: 0).theme) .setPriority(NotificationCompat.PRIORITY_MAX) .setSmallIcon(R.drawable.ic_notification) .setNumber(messages.size) @@ -158,8 +163,9 @@ class NotificationManagerImpl @Inject constructor( val person = Person.Builder() if (!message.isMe()) { - val recipient = conversation.recipients - .firstOrNull { phoneNumberUtils.compare(it.address, message.address) } + val recipient = conversation.recipients.find { recipient -> + phoneNumberUtils.compare(recipient.address, message.address) + } person.setName(recipient?.getDisplayName() ?: message.address) person.setIcon(GlideApp.with(context) @@ -302,6 +308,12 @@ class NotificationManagerImpl @Inject constructor( } val conversation = conversationRepo.getConversation(message.threadId) ?: return + val lastRecipient = conversation.lastMessage?.let { lastMessage -> + conversation.recipients.find { recipient -> + phoneNumberUtils.compare(recipient.address, lastMessage.address) + } + } ?: conversation.recipients.firstOrNull() + val threadId = conversation.id val contentIntent = Intent(context, ComposeActivity::class.java).putExtra("threadId", threadId) @@ -313,7 +325,7 @@ class NotificationManagerImpl @Inject constructor( val notification = NotificationCompat.Builder(context, getChannelIdForNotification(threadId)) .setContentTitle(context.getString(R.string.notification_message_failed_title)) .setContentText(context.getString(R.string.notification_message_failed_text, conversation.getTitle())) - .setColor(colors.theme(threadId).theme) + .setColor(colors.theme(lastRecipient?.id ?: 0).theme) .setPriority(NotificationManagerCompat.IMPORTANCE_MAX) .setSmallIcon(R.drawable.ic_notification_failed) .setAutoCancel(true) diff --git a/presentation/src/main/java/com/moez/QKSMS/common/widget/AvatarView.kt b/presentation/src/main/java/com/moez/QKSMS/common/widget/AvatarView.kt index 70fc0b39d..1a289b118 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/widget/AvatarView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/widget/AvatarView.kt @@ -43,7 +43,7 @@ class AvatarView @JvmOverloads constructor( /** * This value can be changes if we should use the theme from a particular conversation */ - var threadId: Long = 0 + var recipientId: Long = 0 set(value) { if (field == value) return field = value @@ -81,13 +81,13 @@ class AvatarView @JvmOverloads constructor( super.onFinishInflate() if (!isInEditMode) { - applyTheme(threadId) + applyTheme(recipientId) updateView() } } - private fun applyTheme(threadId: Long) { - colors.theme(threadId).run { + private fun applyTheme(recipientId: Long) { + colors.theme(recipientId).run { setBackgroundTint(theme) initial.setTextColor(textPrimary) icon.setTint(textPrimary) diff --git a/presentation/src/main/java/com/moez/QKSMS/common/widget/GroupAvatarView.kt b/presentation/src/main/java/com/moez/QKSMS/common/widget/GroupAvatarView.kt index f00c65619..695abe817 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/widget/GroupAvatarView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/widget/GroupAvatarView.kt @@ -35,7 +35,7 @@ class GroupAvatarView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null ) : ConstraintLayout(context, attrs) { - var contacts: List = ArrayList() + var recipients: List = ArrayList() set(value) { field = value.sortedWith(compareByDescending { contact -> contact.contact?.lookupKey }) updateView() @@ -54,17 +54,25 @@ class GroupAvatarView @JvmOverloads constructor( } private fun updateView() { - avatar1Frame.setBackgroundTint(when (contacts.size > 1) { + avatar1Frame.setBackgroundTint(when (recipients.size > 1) { true -> context.resolveThemeColor(android.R.attr.windowBackground) false -> context.getColorCompat(android.R.color.transparent) }) avatar1Frame.updateLayoutParams { - matchConstraintPercentWidth = if (contacts.size > 1) 0.75f else 1.0f + matchConstraintPercentWidth = if (recipients.size > 1) 0.75f else 1.0f } - avatar2.isVisible = contacts.size > 1 + avatar2.isVisible = recipients.size > 1 - avatar1.setContact(contacts.getOrNull(0)?.contact) - avatar2.setContact(contacts.getOrNull(1)?.contact) + + recipients.getOrNull(0).let { recipient -> + avatar1.recipientId = recipient?.id ?: 0 + avatar1.setContact(recipient?.contact) + } + + recipients.getOrNull(1).let { recipient -> + avatar2.recipientId = recipient?.id ?: 0 + avatar2.setContact(recipient?.contact) + } } } diff --git a/presentation/src/main/java/com/moez/QKSMS/common/widget/PagerTitleView.kt b/presentation/src/main/java/com/moez/QKSMS/common/widget/PagerTitleView.kt index 3f2ff65fe..aab9fccc5 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/widget/PagerTitleView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/widget/PagerTitleView.kt @@ -41,7 +41,7 @@ class PagerTitleView @JvmOverloads constructor(context: Context, attrs: Attribut @Inject lateinit var colors: Colors - private val threadId: Subject = BehaviorSubject.create() + private val recipientId: Subject = BehaviorSubject.create() var pager: ViewPager? = null set(value) { @@ -55,8 +55,8 @@ class PagerTitleView @JvmOverloads constructor(context: Context, attrs: Attribut if (!isInEditMode) appComponent.inject(this) } - fun setThreadId(id: Long) { - threadId.onNext(id) + fun setRecipientId(id: Long) { + recipientId.onNext(id) } private fun recreate() { @@ -90,9 +90,9 @@ class PagerTitleView @JvmOverloads constructor(context: Context, attrs: Attribut intArrayOf(android.R.attr.state_activated), intArrayOf(-android.R.attr.state_activated)) - threadId + recipientId .distinctUntilChanged() - .switchMap { threadId -> colors.themeObservable(threadId) } + .switchMap { recipientId -> colors.themeObservable(recipientId) } .map { theme -> val textSecondary = context.resolveThemeColor(android.R.attr.textColorSecondary) ColorStateList(states, intArrayOf(theme.theme, textSecondary)) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/blocking/messages/BlockedMessagesAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/blocking/messages/BlockedMessagesAdapter.kt index 222e6d362..af73405ed 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/blocking/messages/BlockedMessagesAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/blocking/messages/BlockedMessagesAdapter.kt @@ -73,7 +73,7 @@ class BlockedMessagesAdapter @Inject constructor( view.isActivated = isSelected(conversation.id) - view.avatars.contacts = conversation.recipients + view.avatars.recipients = conversation.recipients view.title.collapseEnabled = conversation.recipients.size > 1 view.title.text = conversation.getTitle() view.date.text = dateFormatter.getConversationTimestamp(conversation.date) 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 c8f373724..0c63503d4 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 @@ -153,6 +153,10 @@ class ComposeActivity : QkThemedActivity(), ComposeView { .doOnNext { attach.setBackgroundTint(it.theme) } .doOnNext { attach.setTint(it.textPrimary) } .doOnNext { messageAdapter.theme = it } + .autoDisposable(scope()) + .subscribe() + + allThemes .autoDisposable(scope()) .subscribe { messageList.scrapViews() } @@ -198,7 +202,7 @@ class ComposeActivity : QkThemedActivity(), ComposeView { return } - threadId.onNext(state.selectedConversation) + threadId.onNext(state.threadId) title = when { state.selectedMessages > 0 -> getString(R.string.compose_title_selected, state.selectedMessages) 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 3d6babaf0..0de28b089 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 selectedConversation: Long = 0, + val threadId: Long = 0, val selectedChips: List = ArrayList(), val sendAsGroup: Boolean = true, val conversationtitle: String = "", 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 c90126394..227f0a46c 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 @@ -32,6 +32,7 @@ import com.moez.QKSMS.common.Navigator import com.moez.QKSMS.common.base.QkViewModel import com.moez.QKSMS.common.util.BillingManager import com.moez.QKSMS.common.util.ClipboardUtils +import com.moez.QKSMS.common.util.Colors import com.moez.QKSMS.common.util.MessageDetailsFormatter import com.moez.QKSMS.common.util.extensions.makeToast import com.moez.QKSMS.compat.SubscriptionManagerCompat @@ -89,6 +90,7 @@ class ComposeViewModel @Inject constructor( @Named("addresses") private val addresses: List, @Named("text") private val sharedText: String, @Named("attachments") private val sharedAttachments: Attachments, + private val colors: Colors, private val contactRepo: ContactRepository, private val context: Context, private val activeConversationManager: ActiveConversationManager, @@ -109,7 +111,7 @@ class ComposeViewModel @Inject constructor( private val subscriptionManager: SubscriptionManagerCompat ) : QkViewModel(ComposeState( editingMode = threadId == 0L && addresses.isEmpty(), - selectedConversation = threadId, + threadId = threadId, query = query) ) { @@ -183,13 +185,13 @@ class ComposeViewModel @Inject constructor( .takeUntil(state.filter { state -> !state.editingMode }) .subscribe(selectedChips::onNext) - // When the conversation changes, mark read, and update the threadId and the messages for the adapter + // When the conversation changes, mark read, and update the recipientId and the messages for the adapter disposables += conversation .distinctUntilChanged { conversation -> conversation.id } .observeOn(AndroidSchedulers.mainThread()) .map { conversation -> val messages = messageRepo.getMessages(conversation.id) - newState { copy(selectedConversation = conversation.id, messages = Pair(conversation, messages)) } + newState { copy(threadId = conversation.id, messages = Pair(conversation, messages)) } messages } .switchMap { messages -> messages.asObservable() } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/MessagesAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/MessagesAdapter.kt index de24eff13..ffd83a4bb 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/MessagesAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/MessagesAdapter.kt @@ -98,9 +98,6 @@ class MessagesAdapter @Inject constructor( field = value contactCache.clear() - // Update the theme - theme = colors.theme(value?.first?.id ?: 0) - updateData(value?.second) } @@ -145,9 +142,6 @@ class MessagesAdapter @Inject constructor( view.findViewById(R.id.cancel).setTint(theme.theme) } else { view = layoutInflater.inflate(R.layout.message_list_item_in, parent, false) - view.avatar.threadId = conversation?.id ?: 0 - view.body.setTextColor(theme.textPrimary) - view.body.setBackgroundTint(theme.theme) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { @@ -187,6 +181,11 @@ class MessagesAdapter @Inject constructor( val next = if (position == itemCount - 1) null else getItem(position + 1) val view = viewHolder.containerView + val theme = when (message.isOutgoingMessage()) { + true -> colors.theme() + false -> colors.theme(contactCache[message.address]?.id ?: 0) + } + // Update the selected state view.isActivated = isSelected(message.id) || highlight == message.id @@ -231,11 +230,14 @@ class MessagesAdapter @Inject constructor( val media = message.parts.filter { !it.isSmil() && !it.isText() } view.setPadding(bottom = if (canGroup(message, next)) 0 else 16.dpToPx(context)) - // Bind the avatar + // Bind the avatar and bubble colour if (!message.isMe()) { - view.avatar.threadId = conversation?.id ?: 0 + view.avatar.recipientId = contactCache[message.address]?.id ?: 0 view.avatar.setContact(contactCache[message.address]?.contact) view.avatar.setVisible(!canGroup(message, next), View.INVISIBLE) + + view.body.setTextColor(theme.textPrimary) + view.body.setBackgroundTint(theme.theme) } // Bind the body text diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ComposeItemAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ComposeItemAdapter.kt index 1c5cf36c2..101e7ba59 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ComposeItemAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ComposeItemAdapter.kt @@ -87,7 +87,7 @@ class ComposeItemAdapter @Inject constructor(private val colors: Colors) : QkAda view.icon.isVisible = false - view.avatar.contacts = listOf(Recipient(contact = contact)) + view.avatar.recipients = listOf(Recipient(contact = contact)) view.title.text = contact.numbers.joinToString { it.address } @@ -102,7 +102,7 @@ class ComposeItemAdapter @Inject constructor(private val colors: Colors) : QkAda view.icon.isVisible = prev !is ComposeItem.Recent view.icon.setImageResource(R.drawable.ic_history_black_24dp) - view.avatar.contacts = conversation.recipients + view.avatar.recipients = conversation.recipients view.title.text = conversation.getTitle() @@ -123,7 +123,7 @@ class ComposeItemAdapter @Inject constructor(private val colors: Colors) : QkAda view.icon.isVisible = prev !is ComposeItem.Starred view.icon.setImageResource(R.drawable.ic_star_black_24dp) - view.avatar.contacts = listOf(Recipient(contact = contact)) + view.avatar.recipients = listOf(Recipient(contact = contact)) view.title.text = contact.name @@ -139,7 +139,7 @@ class ComposeItemAdapter @Inject constructor(private val colors: Colors) : QkAda 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.avatar.recipients = group.contacts.map { contact -> Recipient(contact = contact) } view.title.text = group.title @@ -158,7 +158,7 @@ class ComposeItemAdapter @Inject constructor(private val colors: Colors) : QkAda view.icon.isVisible = false - view.avatar.contacts = listOf(Recipient(contact = contact)) + view.avatar.recipients = listOf(Recipient(contact = contact)) view.title.text = contact.name diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoController.kt b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoController.kt index 45a010e31..af8a345e8 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoController.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoController.kt @@ -78,10 +78,9 @@ class ConversationInfoController( media.adapter = mediaAdapter media.addItemDecoration(itemDecoration) - themedActivity - ?.theme + themedActivity?.theme ?.autoDisposable(scope()) - ?.subscribe { recipients?.scrapViews() } + ?.subscribe { recipients.scrapViews() } } override fun onAttach(view: View) { @@ -91,7 +90,9 @@ class ConversationInfoController( showBackButton(true) } - override fun recipientClicks(): Observable = recipientAdapter.clicks + override fun contactClicks(): Observable = recipientAdapter.contactClicks + + override fun themeClicks(): Observable = recipientAdapter.themeClicks override fun nameClicks(): Observable<*> = name.clicks() @@ -99,8 +100,6 @@ class ConversationInfoController( override fun notificationClicks(): Observable<*> = notifications.clicks() - override fun themeClicks(): Observable<*> = themePrefs.clicks() - override fun archiveClicks(): Observable<*> = archive.clicks() override fun blockClicks(): Observable<*> = block.clicks() @@ -115,8 +114,6 @@ class ConversationInfoController( return } - themedActivity?.threadId?.onNext(state.threadId) - recipientAdapter.threadId = state.threadId recipientAdapter.updateData(state.recipients) name.setVisible(state.recipients?.size ?: 0 >= 2) @@ -124,8 +121,6 @@ class ConversationInfoController( notifications.isEnabled = !state.blocked - themePrefs.isEnabled = !state.blocked - archive.isEnabled = !state.blocked archive.title = activity?.getString(when (state.archived) { true -> R.string.info_unarchive @@ -142,8 +137,8 @@ class ConversationInfoController( override fun showNameDialog(name: String) = nameDialog.setText(name).show() - override fun showThemePicker(threadId: Long) { - router.pushController(RouterTransaction.with(ThemePickerController(threadId)) + override fun showThemePicker(recipientId: Long) { + router.pushController(RouterTransaction.with(ThemePickerController(recipientId)) .pushChangeHandler(QkChangeHandler()) .popChangeHandler(QkChangeHandler())) } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoPresenter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoPresenter.kt index 06c44e0ad..10c7b7814 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoPresenter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoPresenter.kt @@ -100,7 +100,7 @@ class ConversationInfoPresenter @Inject constructor( super.bindIntents(view) // Add or display the contact - view.recipientClicks() + view.contactClicks() .mapNotNull(conversationRepo::getRecipient) .doOnNext { recipient -> recipient.contact?.lookupKey?.let(navigator::showContact) @@ -109,6 +109,11 @@ class ConversationInfoPresenter @Inject constructor( .autoDisposable(view.scope(Lifecycle.Event.ON_DESTROY)) // ... this should be the default .subscribe() + // Show the theme settings for the conversation + view.themeClicks() + .autoDisposable(view.scope()) + .subscribe(view::showThemePicker) + // Show the conversation title dialog view.nameClicks() .withLatestFrom(conversation) { _, conversation -> conversation } @@ -130,12 +135,6 @@ class ConversationInfoPresenter @Inject constructor( .autoDisposable(view.scope()) .subscribe { conversation -> navigator.showNotificationSettings(conversation.id) } - // Show the theme settings for the conversation - view.themeClicks() - .withLatestFrom(conversation) { _, conversation -> conversation } - .autoDisposable(view.scope()) - .subscribe { conversation -> view.showThemePicker(conversation.id) } - // Toggle the archived state of the conversation view.archiveClicks() .withLatestFrom(conversation) { _, conversation -> conversation } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoView.kt b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoView.kt index e2c1d9713..1b71de22b 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoView.kt @@ -23,18 +23,18 @@ import io.reactivex.Observable interface ConversationInfoView : QkViewContract { - fun recipientClicks(): Observable + fun contactClicks(): Observable + fun themeClicks(): Observable fun nameClicks(): Observable<*> fun nameChanges(): Observable fun notificationClicks(): Observable<*> - fun themeClicks(): Observable<*> fun archiveClicks(): Observable<*> fun blockClicks(): Observable<*> fun deleteClicks(): Observable<*> fun confirmDelete(): Observable<*> fun showNameDialog(name: String) - fun showThemePicker(threadId: Long) + fun showThemePicker(recipientId: Long) fun showBlockingDialog(conversations: List, block: Boolean) fun requestDefaultSms() fun showDeleteDialog() diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationRecipientAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationRecipientAdapter.kt index 92f0b2cf6..9a6eb3997 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationRecipientAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationRecipientAdapter.kt @@ -20,24 +20,24 @@ package com.moez.QKSMS.feature.conversationinfo import android.view.LayoutInflater import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView import com.moez.QKSMS.R import com.moez.QKSMS.common.base.QkRealmAdapter 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.model.Recipient -import io.reactivex.disposables.CompositeDisposable import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.Subject import kotlinx.android.synthetic.main.conversation_recipient_list_item.view.* import javax.inject.Inject -class ConversationRecipientAdapter @Inject constructor() : QkRealmAdapter() { +class ConversationRecipientAdapter @Inject constructor( + private val colors: Colors +) : QkRealmAdapter() { - var threadId: Long = 0L - val clicks: Subject = PublishSubject.create() - - private val disposables = CompositeDisposable() + val contactClicks: Subject = PublishSubject.create() + val themeClicks: Subject = PublishSubject.create() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkViewHolder { val layoutInflater = LayoutInflater.from(parent.context) @@ -45,7 +45,12 @@ class ConversationRecipientAdapter @Inject constructor() : QkRealmAdapter() { init { @@ -60,7 +62,6 @@ class ConversationsAdapter @Inject constructor( view.snippet.maxLines = 5 view.unread.isVisible = true - view.unread.setTint(colors.theme().theme) view.date.setTypeface(view.date.typeface, Typeface.BOLD) view.date.setTextColor(textColorPrimary) @@ -85,11 +86,12 @@ class ConversationsAdapter @Inject constructor( override fun onBindViewHolder(viewHolder: QkViewHolder, position: Int) { val conversation = getItem(position) ?: return + val lastMessage = conversation.lastMessage ?: return val view = viewHolder.containerView view.isActivated = isSelected(conversation.id) - view.avatars.contacts = conversation.recipients + view.avatars.recipients = conversation.recipients view.title.collapseEnabled = conversation.recipients.size > 1 view.title.text = conversation.getTitle() view.date.text = dateFormatter.getConversationTimestamp(conversation.date) @@ -99,6 +101,16 @@ class ConversationsAdapter @Inject constructor( else -> conversation.snippet } view.pinned.isVisible = conversation.pinned + + // If the last message wasn't incoming, then the colour doesn't really matter anyway + val recipient = when (conversation.recipients.size) { + 1 -> conversation.recipients.firstOrNull() + else -> conversation.recipients.find { recipient -> + phoneNumberUtils.compare(recipient.address, lastMessage.address) + } + } + + view.unread.setTint(colors.theme(recipient?.id ?: 0).theme) } override fun getItemId(index: Int): Long { @@ -108,4 +120,4 @@ class ConversationsAdapter @Inject constructor( override fun getItemViewType(position: Int): Int { return if (getItem(position)?.unread == false) 0 else 1 } -} \ No newline at end of file +} diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainActivity.kt b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainActivity.kt index 808112498..bc797db36 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainActivity.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainActivity.kt @@ -151,7 +151,6 @@ class MainActivity : QkThemedActivity(), MainView { // Set the theme color tint to the recyclerView, progressbar, and FAB theme - .doOnNext { recyclerView.scrapViews() } .autoDisposable(scope()) .subscribe { theme -> // Set the color for the drawer icons @@ -173,6 +172,10 @@ class MainActivity : QkThemedActivity(), MainView { compose.setTint(theme.textPrimary) } + allThemes + .autoDisposable(scope()) + .subscribe { recyclerView.scrapViews() } + itemTouchCallback.adapter = conversationsAdapter conversationsAdapter.autoScrollToStart(recyclerView) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/main/SearchAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/main/SearchAdapter.kt index 6e429aa59..6d03af13f 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/main/SearchAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/main/SearchAdapter.kt @@ -73,7 +73,7 @@ class SearchAdapter @Inject constructor( } view.title.text = title - view.avatars.contacts = result.conversation.recipients + view.avatars.recipients = result.conversation.recipients when (result.messages == 0) { true -> { diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/qkreply/QkReplyActivity.kt b/presentation/src/main/java/com/moez/QKSMS/feature/qkreply/QkReplyActivity.kt index 9612bcce4..392196d41 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/qkreply/QkReplyActivity.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/qkreply/QkReplyActivity.kt @@ -90,7 +90,7 @@ class QkReplyActivity : QkThemedActivity(), QkReplyView { finish() } - threadId.onNext(state.selectedConversation) + threadId.onNext(state.threadId) title = state.title diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/qkreply/QkReplyState.kt b/presentation/src/main/java/com/moez/QKSMS/feature/qkreply/QkReplyState.kt index 573dde17e..34dae4c97 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/qkreply/QkReplyState.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/qkreply/QkReplyState.kt @@ -25,7 +25,7 @@ import io.realm.RealmResults data class QkReplyState( val hasError: Boolean = false, - val selectedConversation: Long = 0, + val threadId: Long = 0, val title: String = "", val expanded: Boolean = false, val data: Pair>? = null, diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/qkreply/QkReplyViewModel.kt b/presentation/src/main/java/com/moez/QKSMS/feature/qkreply/QkReplyViewModel.kt index d29c2068c..4ef550cd1 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/qkreply/QkReplyViewModel.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/qkreply/QkReplyViewModel.kt @@ -54,7 +54,7 @@ class QkReplyViewModel @Inject constructor( private val navigator: Navigator, private val sendMessage: SendMessage, private val subscriptionManager: SubscriptionManagerCompat -) : QkViewModel(QkReplyState(selectedConversation = threadId)) { +) : QkViewModel(QkReplyState(threadId = threadId)) { private val conversation by lazy { conversationRepo.getConversationAsync(threadId) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/scheduled/ScheduledMessageAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/scheduled/ScheduledMessageAdapter.kt index 43dfcab71..e23c2a687 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/scheduled/ScheduledMessageAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/scheduled/ScheduledMessageAdapter.kt @@ -68,7 +68,7 @@ class ScheduledMessageAdapter @Inject constructor( val view = holder.containerView // GroupAvatarView only accepts recipients, so map the phone numbers to recipients - view.avatars.contacts = message.recipients.map { address -> Recipient(address = address) } + view.avatars.recipients = message.recipients.map { address -> Recipient(address = address) } view.recipients.text = message.recipients.joinToString(",") { address -> contactCache[address]?.name?.takeIf { it.isNotBlank() } ?: address diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/themepicker/ThemePickerController.kt b/presentation/src/main/java/com/moez/QKSMS/feature/themepicker/ThemePickerController.kt index 1ef80a3b5..ae4dc56f2 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/themepicker/ThemePickerController.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/themepicker/ThemePickerController.kt @@ -38,7 +38,9 @@ import kotlinx.android.synthetic.main.theme_picker_controller.* import kotlinx.android.synthetic.main.theme_picker_hsv.* import javax.inject.Inject -class ThemePickerController(val threadId: Long = 0L) : QkController(), ThemePickerView { +class ThemePickerController( + val recipientId: Long = 0L +) : QkController(), ThemePickerView { @Inject override lateinit var presenter: ThemePickerPresenter @@ -107,7 +109,7 @@ class ThemePickerController(val threadId: Long = 0L) : QkController = viewQksmsPlusSubject override fun render(state: ThemePickerState) { - tabs.setThreadId(state.threadId) + tabs.setRecipientId(state.recipientId) hex.setText(Integer.toHexString(state.newColor).takeLast(6)) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/themepicker/ThemePickerPresenter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/themepicker/ThemePickerPresenter.kt index 20c7e358b..835695c76 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/themepicker/ThemePickerPresenter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/themepicker/ThemePickerPresenter.kt @@ -21,6 +21,7 @@ package com.moez.QKSMS.feature.themepicker import com.f2prateek.rx.preferences2.Preference import com.moez.QKSMS.common.Navigator import com.moez.QKSMS.common.base.QkPresenter +import com.moez.QKSMS.common.util.BillingManager import com.moez.QKSMS.common.util.Colors import com.moez.QKSMS.manager.WidgetManager import com.moez.QKSMS.util.Preferences @@ -33,13 +34,14 @@ import javax.inject.Named class ThemePickerPresenter @Inject constructor( prefs: Preferences, - @Named("threadId") private val threadId: Long, + @Named("recipientId") private val recipientId: Long, + private val billingManager: BillingManager, private val colors: Colors, private val navigator: Navigator, private val widgetManager: WidgetManager -) : QkPresenter(ThemePickerState(threadId = threadId)) { +) : QkPresenter(ThemePickerState(recipientId = recipientId)) { - private val theme: Preference = prefs.theme(threadId) + private val theme: Preference = prefs.theme(recipientId) override fun bindIntents(view: ThemePickerView) { super.bindIntents(view) @@ -53,7 +55,7 @@ class ThemePickerPresenter @Inject constructor( .autoDisposable(view.scope()) .subscribe { color -> theme.set(color) - if (threadId == 0L) { + if (recipientId == 0L) { widgetManager.updateTheme() } } @@ -75,7 +77,7 @@ class ThemePickerPresenter @Inject constructor( view.applyHsvThemeClicks() .withLatestFrom(view.hsvThemeSelected()) { _, color -> theme.set(color) - if (threadId == 0L) { + if (recipientId == 0L) { widgetManager.updateTheme() } } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/themepicker/ThemePickerState.kt b/presentation/src/main/java/com/moez/QKSMS/feature/themepicker/ThemePickerState.kt index 2aa731a0b..81c44dcf1 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/themepicker/ThemePickerState.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/themepicker/ThemePickerState.kt @@ -19,7 +19,7 @@ package com.moez.QKSMS.feature.themepicker data class ThemePickerState( - val threadId: Long = 0, + val recipientId: Long = 0, val applyThemeVisible: Boolean = false, val newColor: Int = -1, val newTextColor: Int = -1 diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/themepicker/injection/ThemePickerModule.kt b/presentation/src/main/java/com/moez/QKSMS/feature/themepicker/injection/ThemePickerModule.kt index c320bdfc2..c5c9b8d15 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/themepicker/injection/ThemePickerModule.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/themepicker/injection/ThemePickerModule.kt @@ -29,7 +29,7 @@ class ThemePickerModule(private val controller: ThemePickerController) { @Provides @ControllerScope - @Named("threadId") - fun provideThreadId(): Long = controller.threadId + @Named("recipientId") + fun provideThreadId(): Long = controller.recipientId } \ No newline at end of file diff --git a/presentation/src/main/java/com/moez/QKSMS/injection/AppModule.kt b/presentation/src/main/java/com/moez/QKSMS/injection/AppModule.kt index 1192b9e76..5cd607c04 100644 --- a/presentation/src/main/java/com/moez/QKSMS/injection/AppModule.kt +++ b/presentation/src/main/java/com/moez/QKSMS/injection/AppModule.kt @@ -21,6 +21,7 @@ package com.moez.QKSMS.injection import android.app.Application import android.content.ContentResolver import android.content.Context +import android.content.SharedPreferences import android.preference.PreferenceManager import androidx.lifecycle.ViewModelProvider import com.f2prateek.rx.preferences2.RxSharedPreferences @@ -99,8 +100,13 @@ class AppModule(private var application: Application) { @Provides @Singleton - fun provideRxPreferences(context: Context): RxSharedPreferences { - val preferences = PreferenceManager.getDefaultSharedPreferences(context) + fun provideSharedPreferences(context: Context): SharedPreferences { + return PreferenceManager.getDefaultSharedPreferences(context) + } + + @Provides + @Singleton + fun provideRxPreferences(preferences: SharedPreferences): RxSharedPreferences { return RxSharedPreferences.create(preferences) } diff --git a/presentation/src/main/res/layout/conversation_info_controller.xml b/presentation/src/main/res/layout/conversation_info_controller.xml index 40670ba94..0ccb95d7f 100644 --- a/presentation/src/main/res/layout/conversation_info_controller.xml +++ b/presentation/src/main/res/layout/conversation_info_controller.xml @@ -65,13 +65,6 @@ app:icon="@drawable/ic_notifications_black_24dp" app:title="@string/info_notifications" /> - - - \ No newline at end of file + diff --git a/presentation/src/main/res/layout/conversation_recipient_list_item.xml b/presentation/src/main/res/layout/conversation_recipient_list_item.xml index 959413c4e..970d478ec 100644 --- a/presentation/src/main/res/layout/conversation_recipient_list_item.xml +++ b/presentation/src/main/res/layout/conversation_recipient_list_item.xml @@ -42,7 +42,10 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="16dp" + android:layout_marginEnd="8dp" app:layout_constraintBottom_toTopOf="@id/address" + app:layout_constraintEnd_toStartOf="@id/add" + app:layout_constraintHorizontal_bias="0" app:layout_constraintStart_toEndOf="@id/avatar" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_chainStyle="packed" @@ -54,6 +57,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="@id/name" app:layout_constraintStart_toStartOf="@id/name" app:layout_constraintTop_toBottomOf="@id/name" tools:text="(123) 456-7890" /> @@ -62,11 +66,24 @@ android:id="@+id/add" android:layout_width="40dp" android:layout_height="40dp" + android:layout_marginEnd="16dp" android:padding="8dp" android:src="@drawable/ic_person_add_black_24dp" android:tint="?android:attr/textColorSecondary" app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@id/theme" + app:layout_constraintTop_toTopOf="parent" /> + + - \ No newline at end of file + -- GitLab From c5bbbec2dcaee42a2a7bb2d1382a9b98605fca03 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Thu, 26 Dec 2019 18:19:41 -0500 Subject: [PATCH 050/109] Automatic contact colours Closes #133 --- .../moez/QKSMS/migration/QkRealmMigration.kt | 11 +- .../java/com/moez/QKSMS/util/Preferences.kt | 12 +- .../QKSMS/common/base/QkThemedActivity.kt | 19 +- .../java/com/moez/QKSMS/common/util/Colors.kt | 83 ++++-- .../common/util/NotificationManagerImpl.kt | 4 +- .../moez/QKSMS/common/widget/AvatarView.kt | 41 +-- .../QKSMS/common/widget/GroupAvatarView.kt | 11 +- .../QKSMS/common/widget/PagerTitleView.kt | 6 +- .../QKSMS/feature/compose/ComposeActivity.kt | 8 +- .../moez/QKSMS/feature/compose/ComposeView.kt | 1 + .../QKSMS/feature/compose/ComposeViewModel.kt | 7 + .../QKSMS/feature/compose/MessagesAdapter.kt | 5 +- .../feature/compose/editing/ChipsAdapter.kt | 3 +- .../compose/editing/DetailedChipView.kt | 3 +- .../ConversationRecipientAdapter.kt | 5 +- .../conversations/ConversationsAdapter.kt | 2 +- .../moez/QKSMS/feature/main/MainActivity.kt | 17 +- .../com/moez/QKSMS/feature/main/MainView.kt | 3 +- .../moez/QKSMS/feature/main/MainViewModel.kt | 15 + .../feature/settings/SettingsController.kt | 3 + .../feature/settings/SettingsPresenter.kt | 5 + .../QKSMS/feature/settings/SettingsState.kt | 1 + .../main/res/layout/settings_controller.xml | 8 + presentation/src/main/res/values/colors.xml | 260 ++++++++++++++++++ presentation/src/main/res/values/strings.xml | 1 + 25 files changed, 428 insertions(+), 106 deletions(-) diff --git a/data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt b/data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt index be9f96872..6e19486fe 100644 --- a/data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt +++ b/data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt @@ -19,9 +19,9 @@ package com.moez.QKSMS.migration import android.annotation.SuppressLint -import com.f2prateek.rx.preferences2.RxSharedPreferences import com.moez.QKSMS.extensions.map import com.moez.QKSMS.mapper.CursorToContactImpl +import com.moez.QKSMS.util.Preferences import io.realm.DynamicRealm import io.realm.DynamicRealmObject import io.realm.FieldAttribute @@ -32,7 +32,7 @@ import javax.inject.Inject class QkRealmMigration @Inject constructor( private val cursorToContact: CursorToContactImpl, - private val prefs: RxSharedPreferences + private val prefs: Preferences ) : RealmMigration { companion object { @@ -176,7 +176,7 @@ class QkRealmMigration @Inject constructor( // Migrate conversation themes val recipients = mutableMapOf() // Map of recipientId:theme realm.where("Conversation").findAll().forEach { conversation -> - val pref = prefs.getInteger("theme_${conversation.getLong("id")}") + val pref = prefs.theme(conversation.getLong("id")) if (pref.isSet) { conversation.getList("Recipient").forEach { recipient -> recipients[recipient.getLong("id")] = pref.get() @@ -187,9 +187,12 @@ class QkRealmMigration @Inject constructor( } recipients.forEach { (recipientId, theme) -> - prefs.getInteger("theme_$recipientId").set(theme) + prefs.theme(recipientId).set(theme) } + // This is enabled for new users, but the behaviour shouldn't change automatically for old users + prefs.autoColor.set(false) + version++ } diff --git a/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt b/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt index 4dee89005..f21adcc96 100644 --- a/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt +++ b/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt @@ -90,6 +90,7 @@ class Preferences @Inject constructor( val nightStart = rxPrefs.getString("nightStart", "18:00") val nightEnd = rxPrefs.getString("nightEnd", "6:00") val black = rxPrefs.getBoolean("black", false) + val autoColor = rxPrefs.getBoolean("autoColor", true) val systemFont = rxPrefs.getBoolean("systemFont", false) val textSize = rxPrefs.getInteger("textSize", TEXT_SIZE_NORMAL) val blockingManager = rxPrefs.getInteger("blockingManager", BLOCKING_MANAGER_QKSMS) @@ -140,12 +141,13 @@ class Preferences @Inject constructor( sharedPrefs.registerOnSharedPreferenceChangeListener(listener) }.share() - fun theme(recipientId: Long = 0): Preference { - val default = rxPrefs.getInteger("theme", 0xFF0097A7.toInt()) - + fun theme( + recipientId: Long = 0, + default: Int = rxPrefs.getInteger("theme", 0xFF0097A7.toInt()).get() + ): Preference { return when (recipientId) { - 0L -> default - else -> rxPrefs.getInteger("theme_$recipientId", default.get()) + 0L -> rxPrefs.getInteger("theme", 0xFF0097A7.toInt()) + else -> rxPrefs.getInteger("theme_$recipientId", default) } } diff --git a/presentation/src/main/java/com/moez/QKSMS/common/base/QkThemedActivity.kt b/presentation/src/main/java/com/moez/QKSMS/common/base/QkThemedActivity.kt index 93a39aa33..14b858b2c 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/base/QkThemedActivity.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/base/QkThemedActivity.kt @@ -30,6 +30,7 @@ import com.moez.QKSMS.R import com.moez.QKSMS.common.util.Colors import com.moez.QKSMS.common.util.extensions.resolveThemeBoolean import com.moez.QKSMS.common.util.extensions.resolveThemeColor +import com.moez.QKSMS.extensions.Optional import com.moez.QKSMS.extensions.asObservable import com.moez.QKSMS.extensions.mapNotNull import com.moez.QKSMS.repository.ConversationRepository @@ -76,9 +77,9 @@ abstract class QkThemedActivity : QkActivity() { .switchMap { threadId -> val conversation = conversationRepo.getConversation(threadId) when { - conversation == null -> Observable.just(0L) + conversation == null -> Observable.just(Optional(null)) - conversation.recipients.size == 1 -> Observable.just(conversation.recipients.first()?.id ?: 0L) + conversation.recipients.size == 1 -> Observable.just(Optional(conversation.recipients.first())) else -> messageRepo.getLastIncomingMessage(conversation.id) .asObservable() @@ -89,20 +90,12 @@ abstract class QkThemedActivity : QkActivity() { phoneNumberUtils.compare(recipient.address, message.address) } } - .map { recipient -> recipient.id } - .startWith(conversation.recipients.firstOrNull()?.id ?: 0) + .map { recipient -> Optional(recipient) } + .startWith(Optional(conversation.recipients.firstOrNull())) .distinctUntilChanged() } } - .switchMap { colors.themeObservable(it) } - - /** - * Emits an event whenever any theme changes, whether it be global or for some recipient. This is useful - * for invalidating recyclerviews - */ - val allThemes by lazy { - prefs.keyChanges.filter { key -> key.contains("theme") } - } + .switchMap { colors.themeObservable(it.value) } @SuppressLint("InlinedApi") override fun onCreate(savedInstanceState: Bundle?) { diff --git a/presentation/src/main/java/com/moez/QKSMS/common/util/Colors.kt b/presentation/src/main/java/com/moez/QKSMS/common/util/Colors.kt index 5c94561d9..7eed92601 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/util/Colors.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/util/Colors.kt @@ -20,15 +20,23 @@ package com.moez.QKSMS.common.util import android.content.Context import android.graphics.Color +import androidx.core.content.res.getColorOrThrow import com.moez.QKSMS.R import com.moez.QKSMS.common.util.extensions.getColorCompat +import com.moez.QKSMS.model.Recipient +import com.moez.QKSMS.util.PhoneNumberUtils import com.moez.QKSMS.util.Preferences import io.reactivex.Observable import javax.inject.Inject import javax.inject.Singleton +import kotlin.math.absoluteValue @Singleton -class Colors @Inject constructor(private val context: Context, private val prefs: Preferences) { +class Colors @Inject constructor( + private val context: Context, + private val phoneNumberUtils: PhoneNumberUtils, + private val prefs: Preferences +) { data class Theme(val theme: Int, private val colors: Colors) { val highlight by lazy { colors.highlightColorForTheme(theme) } @@ -37,27 +45,31 @@ class Colors @Inject constructor(private val context: Context, private val prefs val textTertiary by lazy { colors.textTertiaryOnThemeForColor(theme) } } - val materialColors = listOf( - listOf(0xffffebee, 0xffffcdd2, 0xffef9a9a, 0xffe57373, 0xffef5350, 0xfff44336, 0xffe53935, 0xffd32f2f, 0xffc62828, 0xffb71c1c), - listOf(0xffFCE4EC, 0xffF8BBD0, 0xffF48FB1, 0xffF06292, 0xffEC407A, 0xffE91E63, 0xffD81B60, 0xffC2185B, 0xffAD1457, 0xff880E4F), - listOf(0xffF3E5F5, 0xffE1BEE7, 0xffCE93D8, 0xffBA68C8, 0xffAB47BC, 0xff9C27B0, 0xff8E24AA, 0xff7B1FA2, 0xff6A1B9A, 0xff4A148C), - listOf(0xffEDE7F6, 0xffD1C4E9, 0xffB39DDB, 0xff9575CD, 0xff7E57C2, 0xff673AB7, 0xff5E35B1, 0xff512DA8, 0xff4527A0, 0xff311B92), - listOf(0xffE8EAF6, 0xffC5CAE9, 0xff9FA8DA, 0xff7986CB, 0xff5C6BC0, 0xff3F51B5, 0xff3949AB, 0xff303F9F, 0xff283593, 0xff1A237E), - listOf(0xffE3F2FD, 0xffBBDEFB, 0xff90CAF9, 0xff64B5F6, 0xff42A5F5, 0xff2196F3, 0xff1E88E5, 0xff1976D2, 0xff1565C0, 0xff0D47A1), - listOf(0xffE1F5FE, 0xffB3E5FC, 0xff81D4FA, 0xff4FC3F7, 0xff29B6F6, 0xff03A9F4, 0xff039BE5, 0xff0288D1, 0xff0277BD, 0xff01579B), - listOf(0xffE0F7FA, 0xffB2EBF2, 0xff80DEEA, 0xff4DD0E1, 0xff26C6DA, 0xff00BCD4, 0xff00ACC1, 0xff0097A7, 0xff00838F, 0xff006064), - listOf(0xffE0F2F1, 0xffB2DFDB, 0xff80CBC4, 0xff4DB6AC, 0xff26A69A, 0xff009688, 0xff00897B, 0xff00796B, 0xff00695C, 0xff004D40), - listOf(0xffE8F5E9, 0xffC8E6C9, 0xffA5D6A7, 0xff81C784, 0xff66BB6A, 0xff4CAF50, 0xff43A047, 0xff388E3C, 0xff2E7D32, 0xff1B5E20), - listOf(0xffF1F8E9, 0xffDCEDC8, 0xffC5E1A5, 0xffAED581, 0xff9CCC65, 0xff8BC34A, 0xff7CB342, 0xff689F38, 0xff558B2F, 0xff33691E), - listOf(0xffF9FBE7, 0xffF0F4C3, 0xffE6EE9C, 0xffDCE775, 0xffD4E157, 0xffCDDC39, 0xffC0CA33, 0xffAFB42B, 0xff9E9D24, 0xff827717), - listOf(0xffFFFDE7, 0xffFFF9C4, 0xffFFF59D, 0xffFFF176, 0xffFFEE58, 0xffFFEB3B, 0xffFDD835, 0xffFBC02D, 0xffF9A825, 0xffF57F17), - listOf(0xffFFF8E1, 0xffFFECB3, 0xffFFE082, 0xffFFD54F, 0xffFFCA28, 0xffFFC107, 0xffFFB300, 0xffFFA000, 0xffFF8F00, 0xffFF6F00), - listOf(0xffFFF3E0, 0xffFFE0B2, 0xffFFCC80, 0xffFFB74D, 0xffFFA726, 0xffFF9800, 0xffFB8C00, 0xffF57C00, 0xffEF6C00, 0xffE65100), - listOf(0xffFBE9E7, 0xffFFCCBC, 0xffFFAB91, 0xffFF8A65, 0xffFF7043, 0xffFF5722, 0xffF4511E, 0xffE64A19, 0xffD84315, 0xffBF360C), - listOf(0xffEFEBE9, 0xffD7CCC8, 0xffBCAAA4, 0xffA1887F, 0xff8D6E63, 0xff795548, 0xff6D4C41, 0xff5D4037, 0xff4E342E, 0xff3E2723), - listOf(0xffFAFAFA, 0xffF5F5F5, 0xffEEEEEE, 0xffE0E0E0, 0xffBDBDBD, 0xff9E9E9E, 0xff757575, 0xff616161, 0xff424242, 0xff212121), - listOf(0xffECEFF1, 0xffCFD8DC, 0xffB0BEC5, 0xff90A4AE, 0xff78909C, 0xff607D8B, 0xff546E7A, 0xff455A64, 0xff37474F, 0xff263238)) - .map { it.map { it.toInt() } } + val materialColors: List> = listOf( + R.array.material_red, + R.array.material_pink, + R.array.material_purple, + R.array.material_deep_purple, + R.array.material_indigo, + R.array.material_blue, + R.array.material_light_blue, + R.array.material_cyan, + R.array.material_teal, + R.array.material_green, + R.array.material_light_green, + R.array.material_lime, + R.array.material_yellow, + R.array.material_amber, + R.array.material_orange, + R.array.material_deep_orange, + R.array.material_brown, + R.array.material_gray, + R.array.material_blue_gray) + .map { res -> context.resources.obtainTypedArray(res) } + .map { typedArray -> (0 until typedArray.length()).map(typedArray::getColorOrThrow) } + + private val randomColors: List = context.resources.obtainTypedArray(R.array.random_colors) + .let { typedArray -> (0 until typedArray.length()).map(typedArray::getColorOrThrow) } private val minimumContrastRatio = 2 @@ -66,10 +78,21 @@ class Colors @Inject constructor(private val context: Context, private val prefs private val secondaryTextLuminance = measureLuminance(context.getColorCompat(R.color.textSecondaryDark)) private val tertiaryTextLuminance = measureLuminance(context.getColorCompat(R.color.textTertiaryDark)) - fun theme(recipientId: Long = 0): Theme = Theme(prefs.theme(recipientId).get(), this) + fun theme(recipient: Recipient? = null): Theme { + val pref = prefs.theme(recipient?.id ?: 0) + val color = when { + recipient == null || !prefs.autoColor.get() || pref.isSet -> pref.get() + else -> generateColor(recipient) + } + return Theme(color, this) + } - fun themeObservable(recipientId: Long = 0): Observable { - return prefs.theme(recipientId).asObservable() + fun themeObservable(recipient: Recipient? = null): Observable { + val pref = when { + recipient == null || !prefs.autoColor.get() -> prefs.theme() + else -> prefs.theme(recipient.id, generateColor(recipient)) + } + return pref.asObservable() .map { color -> Theme(color, this) } } @@ -110,4 +133,12 @@ class Colors @Inject constructor(private val context: Context, private val prefs return 0.2126 * array[0] + 0.7152 * array[1] + 0.0722 * array[2] + 0.05 } -} \ No newline at end of file + private fun generateColor(recipient: Recipient): Int { + val first = recipient.contact?.name?.firstOrNull() + ?: phoneNumberUtils.normalizeNumber(recipient.address).firstOrNull() + ?: '#' + + val index = first.hashCode().absoluteValue % randomColors.size + return randomColors[index] + } +} diff --git a/presentation/src/main/java/com/moez/QKSMS/common/util/NotificationManagerImpl.kt b/presentation/src/main/java/com/moez/QKSMS/common/util/NotificationManagerImpl.kt index 3d6625fee..22b47ff54 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/util/NotificationManagerImpl.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/util/NotificationManagerImpl.kt @@ -139,7 +139,7 @@ class NotificationManagerImpl @Inject constructor( val notification = NotificationCompat.Builder(context, getChannelIdForNotification(threadId)) .setCategory(NotificationCompat.CATEGORY_MESSAGE) - .setColor(colors.theme(lastRecipient?.id ?: 0).theme) + .setColor(colors.theme(lastRecipient).theme) .setPriority(NotificationCompat.PRIORITY_MAX) .setSmallIcon(R.drawable.ic_notification) .setNumber(messages.size) @@ -325,7 +325,7 @@ class NotificationManagerImpl @Inject constructor( val notification = NotificationCompat.Builder(context, getChannelIdForNotification(threadId)) .setContentTitle(context.getString(R.string.notification_message_failed_title)) .setContentText(context.getString(R.string.notification_message_failed_text, conversation.getTitle())) - .setColor(colors.theme(lastRecipient?.id ?: 0).theme) + .setColor(colors.theme(lastRecipient).theme) .setPriority(NotificationManagerCompat.IMPORTANCE_MAX) .setSmallIcon(R.drawable.ic_notification_failed) .setAutoCancel(true) diff --git a/presentation/src/main/java/com/moez/QKSMS/common/widget/AvatarView.kt b/presentation/src/main/java/com/moez/QKSMS/common/widget/AvatarView.kt index 1a289b118..054f20958 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/widget/AvatarView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/widget/AvatarView.kt @@ -28,7 +28,7 @@ import com.moez.QKSMS.common.util.Colors import com.moez.QKSMS.common.util.extensions.setBackgroundTint import com.moez.QKSMS.common.util.extensions.setTint import com.moez.QKSMS.injection.appComponent -import com.moez.QKSMS.model.Contact +import com.moez.QKSMS.model.Recipient import com.moez.QKSMS.util.GlideApp import kotlinx.android.synthetic.main.avatar_view.view.* import javax.inject.Inject @@ -40,28 +40,20 @@ class AvatarView @JvmOverloads constructor( @Inject lateinit var colors: Colors @Inject lateinit var navigator: Navigator - /** - * This value can be changes if we should use the theme from a particular conversation - */ - var recipientId: Long = 0 - set(value) { - if (field == value) return - field = value - applyTheme(value) - } - private var lookupKey: String? = null private var name: String? = null private var photoUri: String? = null private var lastUpdated: Long? = null + private var theme: Colors.Theme init { if (!isInEditMode) { appComponent.inject(this) } - View.inflate(context, R.layout.avatar_view, this) + theme = colors.theme() + View.inflate(context, R.layout.avatar_view, this) setBackgroundResource(R.drawable.circle) clipToOutline = true } @@ -69,11 +61,12 @@ class AvatarView @JvmOverloads constructor( /** * Use the [contact] information to display the avatar. */ - fun setContact(contact: Contact?) { - lookupKey = contact?.lookupKey - name = contact?.name - photoUri = contact?.photoUri - lastUpdated = contact?.lastUpdate + fun setRecipient(recipient: Recipient?) { + lookupKey = recipient?.contact?.lookupKey + name = recipient?.contact?.name + photoUri = recipient?.contact?.photoUri + lastUpdated = recipient?.contact?.lastUpdate + theme = colors.theme(recipient) updateView() } @@ -81,20 +74,16 @@ class AvatarView @JvmOverloads constructor( super.onFinishInflate() if (!isInEditMode) { - applyTheme(recipientId) updateView() } } - private fun applyTheme(recipientId: Long) { - colors.theme(recipientId).run { - setBackgroundTint(theme) - initial.setTextColor(textPrimary) - icon.setTint(textPrimary) - } - } - private fun updateView() { + // Apply theme + setBackgroundTint(theme.theme) + initial.setTextColor(theme.textPrimary) + icon.setTint(theme.textPrimary) + if (name?.isNotEmpty() == true) { val initials = name?.split(" ").orEmpty() .filter { name -> name.isNotEmpty() } diff --git a/presentation/src/main/java/com/moez/QKSMS/common/widget/GroupAvatarView.kt b/presentation/src/main/java/com/moez/QKSMS/common/widget/GroupAvatarView.kt index 695abe817..85b240a15 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/widget/GroupAvatarView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/widget/GroupAvatarView.kt @@ -64,15 +64,8 @@ class GroupAvatarView @JvmOverloads constructor( avatar2.isVisible = recipients.size > 1 - recipients.getOrNull(0).let { recipient -> - avatar1.recipientId = recipient?.id ?: 0 - avatar1.setContact(recipient?.contact) - } - - recipients.getOrNull(1).let { recipient -> - avatar2.recipientId = recipient?.id ?: 0 - avatar2.setContact(recipient?.contact) - } + recipients.getOrNull(0).run(avatar1::setRecipient) + recipients.getOrNull(1).run(avatar2::setRecipient) } } diff --git a/presentation/src/main/java/com/moez/QKSMS/common/widget/PagerTitleView.kt b/presentation/src/main/java/com/moez/QKSMS/common/widget/PagerTitleView.kt index aab9fccc5..0d3d5db6b 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/widget/PagerTitleView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/widget/PagerTitleView.kt @@ -29,7 +29,9 @@ import com.moez.QKSMS.R import com.moez.QKSMS.common.util.Colors import com.moez.QKSMS.common.util.extensions.forEach import com.moez.QKSMS.common.util.extensions.resolveThemeColor +import com.moez.QKSMS.extensions.Optional import com.moez.QKSMS.injection.appComponent +import com.moez.QKSMS.repository.ConversationRepository import com.uber.autodispose.android.ViewScopeProvider import com.uber.autodispose.autoDisposable import io.reactivex.subjects.BehaviorSubject @@ -40,6 +42,7 @@ import javax.inject.Inject class PagerTitleView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : LinearLayout(context, attrs) { @Inject lateinit var colors: Colors + @Inject lateinit var conversationRepo: ConversationRepository private val recipientId: Subject = BehaviorSubject.create() @@ -92,7 +95,8 @@ class PagerTitleView @JvmOverloads constructor(context: Context, attrs: Attribut recipientId .distinctUntilChanged() - .switchMap { recipientId -> colors.themeObservable(recipientId) } + .map { recipientId -> Optional(conversationRepo.getRecipient(recipientId)) } + .switchMap { recipient -> colors.themeObservable(recipient.value) } .map { theme -> val textSecondary = context.resolveThemeColor(android.R.attr.textColorSecondary) ColorStateList(states, intArrayOf(theme.theme, textSecondary)) 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 0c63503d4..fbf41f536 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 @@ -156,10 +156,6 @@ class ComposeActivity : QkThemedActivity(), ComposeView { .autoDisposable(scope()) .subscribe() - allThemes - .autoDisposable(scope()) - .subscribe { messageList.scrapViews() } - window.callback = ComposeWindowCallback(window.callback, this) // These theme attributes don't apply themselves on API 21 @@ -320,6 +316,10 @@ class ComposeActivity : QkThemedActivity(), ComposeView { startActivityForResult(intent, SelectContactRequestCode) } + override fun themeChanged() { + messageList.scrapViews() + } + override fun showKeyboard() { message.postDelayed({ message.showKeyboard() 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 691207b53..687235369 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 @@ -63,6 +63,7 @@ interface ComposeView : QkView { fun requestStoragePermission() fun requestSmsPermission() fun showContacts(sharing: Boolean, chips: List) + fun themeChanged() fun showKeyboard() fun requestCamera() fun requestGallery() 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 227f0a46c..5470729e7 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 @@ -407,6 +407,13 @@ class ComposeViewModel @Inject constructor( .autoDisposable(view.scope()) .subscribe(view::scrollToMessage) + // Theme changes + prefs.keyChanges + .filter { key -> key.contains("theme") } + .doOnNext { view.themeChanged() } + .autoDisposable(view.scope()) + .subscribe() + // Retry sending view.messageClickIntent .mapNotNull(messageRepo::getMessage) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/MessagesAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/MessagesAdapter.kt index ffd83a4bb..88fbfb0ba 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/MessagesAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/MessagesAdapter.kt @@ -183,7 +183,7 @@ class MessagesAdapter @Inject constructor( val theme = when (message.isOutgoingMessage()) { true -> colors.theme() - false -> colors.theme(contactCache[message.address]?.id ?: 0) + false -> colors.theme(contactCache[message.address]) } // Update the selected state @@ -232,8 +232,7 @@ class MessagesAdapter @Inject constructor( // Bind the avatar and bubble colour if (!message.isMe()) { - view.avatar.recipientId = contactCache[message.address]?.id ?: 0 - view.avatar.setContact(contactCache[message.address]?.contact) + view.avatar.setRecipient(contactCache[message.address]) view.avatar.setVisible(!canGroup(message, next), View.INVISIBLE) view.body.setTextColor(theme.textPrimary) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ChipsAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ChipsAdapter.kt index 50cc7eb27..0bd7f0847 100755 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ChipsAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ChipsAdapter.kt @@ -30,6 +30,7 @@ import com.moez.QKSMS.common.base.QkViewHolder import com.moez.QKSMS.common.util.extensions.dpToPx import com.moez.QKSMS.common.util.extensions.resolveThemeColor import com.moez.QKSMS.common.util.extensions.setBackgroundTint +import com.moez.QKSMS.model.Recipient import io.reactivex.subjects.PublishSubject import kotlinx.android.synthetic.main.contact_chip.* import kotlinx.android.synthetic.main.contact_chip.view.* @@ -60,7 +61,7 @@ class ChipsAdapter @Inject constructor() : QkAdapter() { val chip = getItem(position) val view = holder.containerView - view.avatar.setContact(chip.contact) + view.avatar.setRecipient(Recipient(id = -1, contact = chip.contact)) view.name.text = chip.contact?.name?.takeIf { it.isNotBlank() } ?: chip.address } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/DetailedChipView.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/DetailedChipView.kt index ce35202aa..370d1a59c 100755 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/DetailedChipView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/DetailedChipView.kt @@ -27,6 +27,7 @@ import com.moez.QKSMS.common.util.Colors import com.moez.QKSMS.common.util.extensions.setBackgroundTint import com.moez.QKSMS.common.util.extensions.setTint import com.moez.QKSMS.injection.appComponent +import com.moez.QKSMS.model.Recipient import kotlinx.android.synthetic.main.contact_chip_detailed.view.* import javax.inject.Inject @@ -54,7 +55,7 @@ class DetailedChipView(context: Context) : RelativeLayout(context) { } fun setChip(chip: Chip) { - avatar.setContact(chip.contact) + avatar.setRecipient(Recipient(id = -1, contact = chip.contact)) name.text = chip.contact?.name?.takeIf { it.isNotBlank() } ?: chip.address info.text = chip.address } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationRecipientAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationRecipientAdapter.kt index 9a6eb3997..35baf7bed 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationRecipientAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationRecipientAdapter.kt @@ -59,8 +59,7 @@ class ConversationRecipientAdapter @Inject constructor( val recipient = getItem(position) ?: return val view = holder.containerView - view.avatar.recipientId = recipient.id - view.avatar.setContact(recipient.contact) + view.avatar.setRecipient(recipient) view.name.text = recipient.contact?.name ?: recipient.address @@ -69,7 +68,7 @@ class ConversationRecipientAdapter @Inject constructor( view.add.setVisible(recipient.contact == null) - val theme = colors.theme(recipient.id) + val theme = colors.theme(recipient) view.theme.setTint(theme.theme) } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/conversations/ConversationsAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/conversations/ConversationsAdapter.kt index de51d8b50..0b32f81dd 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/conversations/ConversationsAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/conversations/ConversationsAdapter.kt @@ -110,7 +110,7 @@ class ConversationsAdapter @Inject constructor( } } - view.unread.setTint(colors.theme(recipient?.id ?: 0).theme) + view.unread.setTint(colors.theme(recipient).theme) } override fun getItemId(index: Int): Long { diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainActivity.kt b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainActivity.kt index bc797db36..c8339c21b 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainActivity.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainActivity.kt @@ -83,7 +83,7 @@ class MainActivity : QkThemedActivity(), MainView { @Inject lateinit var viewModelFactory: ViewModelProvider.Factory override val onNewIntentIntent: Subject = PublishSubject.create() - override val activityResumedIntent: Subject = PublishSubject.create() + override val activityResumedIntent: Subject = PublishSubject.create() override val queryChangedIntent by lazy { toolbarSearch.textChanges() } override val composeIntent by lazy { compose.clicks() } override val drawerOpenIntent: Observable by lazy { @@ -172,10 +172,6 @@ class MainActivity : QkThemedActivity(), MainView { compose.setTint(theme.textPrimary) } - allThemes - .autoDisposable(scope()) - .subscribe { recyclerView.scrapViews() } - itemTouchCallback.adapter = conversationsAdapter conversationsAdapter.autoScrollToStart(recyclerView) @@ -313,7 +309,12 @@ class MainActivity : QkThemedActivity(), MainView { override fun onResume() { super.onResume() - activityResumedIntent.onNext(Unit) + activityResumedIntent.onNext(true) + } + + override fun onPause() { + super.onPause() + activityResumedIntent.onNext(false) } override fun onDestroy() { @@ -349,6 +350,10 @@ class MainActivity : QkThemedActivity(), MainView { conversationsAdapter.clearSelection() } + override fun themeChanged() { + recyclerView.scrapViews() + } + override fun showBlockingDialog(conversations: List, block: Boolean) { blockingDialog.show(this, conversations, block) } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainView.kt b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainView.kt index 79ad8aef5..ab8852303 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainView.kt @@ -26,7 +26,7 @@ import io.reactivex.Observable interface MainView : QkView { val onNewIntentIntent: Observable - val activityResumedIntent: Observable<*> + val activityResumedIntent: Observable val queryChangedIntent: Observable val composeIntent: Observable val drawerOpenIntent: Observable @@ -44,6 +44,7 @@ interface MainView : QkView { fun requestPermissions() fun clearSearch() fun clearSelection() + fun themeChanged() fun showBlockingDialog(conversations: List, block: Boolean) fun showDeleteDialog(conversations: List) fun showChangelog(changelog: ChangelogManager.Changelog) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainViewModel.kt b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainViewModel.kt index 4963cce60..e00a71592 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainViewModel.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainViewModel.kt @@ -123,6 +123,7 @@ class MainViewModel @Inject constructor( } val permissions = view.activityResumedIntent + .filter { resumed -> resumed } .observeOn(Schedulers.io()) .map { Triple(permissionManager.isDefaultSms(), permissionManager.hasReadSms(), permissionManager.hasContacts()) } .distinctUntilChanged() @@ -194,6 +195,20 @@ class MainViewModel @Inject constructor( .autoDisposable(view.scope()) .subscribe { data -> newState { copy(page = Searching(loading = false, data = data)) } } + view.activityResumedIntent + .filter { resumed -> !resumed } + .switchMap { + // Take until the activity is resumed + prefs.keyChanges + .filter { key -> key.contains("theme") } + .map { true } + .mergeWith(prefs.autoColor.asObservable()) + .doOnNext { view.themeChanged() } + .takeUntil(view.activityResumedIntent.filter { resumed -> resumed }) + } + .autoDisposable(view.scope()) + .subscribe() + view.composeIntent .autoDisposable(view.scope()) .subscribe { navigator.showCompose() } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsController.kt b/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsController.kt index 092840270..ce5294a8f 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsController.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsController.kt @@ -160,6 +160,9 @@ class SettingsController : QkController newState { copy(autoColor = autoColor) } } + disposables += prefs.systemFont.asObservable() .subscribe { enabled -> newState { copy(systemFontEnabled = enabled) } } @@ -178,6 +181,8 @@ class SettingsPresenter @Inject constructor( R.id.textSize -> view.showTextSizePicker() + R.id.autoColor -> prefs.autoColor.set(!prefs.autoColor.get()) + R.id.systemFont -> prefs.systemFont.set(!prefs.systemFont.get()) R.id.unicode -> prefs.unicode.set(!prefs.unicode.get()) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsState.kt b/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsState.kt index f78a841f5..1119b737a 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsState.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsState.kt @@ -28,6 +28,7 @@ data class SettingsState( val nightStart: String = "", val nightEnd: String = "", val black: Boolean = false, + val autoColor: Boolean = true, val autoEmojiEnabled: Boolean = true, val notificationsEnabled: Boolean = true, val sendDelaySummary: String = "", diff --git a/presentation/src/main/res/layout/settings_controller.xml b/presentation/src/main/res/layout/settings_controller.xml index b270d38f3..d0f4ab86b 100644 --- a/presentation/src/main/res/layout/settings_controller.xml +++ b/presentation/src/main/res/layout/settings_controller.xml @@ -84,6 +84,14 @@ app:icon="@drawable/ic_format_size_black_24dp" app:title="@string/settings_text_size_title" /> + + #00838F + + + #e57373 + #7986CB + #00BFA5 + #81C784 + #FFAB40 + #FF8A65 + #8d6e63 + #448AFF + #78909c + + + + #FFEBEE + #FFCDD2 + #EF9A9A + #E57373 + #EF5350 + #F44336 + #E53935 + #D32F2F + #C62828 + #B71C1C + + + + #FCE4EC + #F8BBD0 + #F48FB1 + #F06292 + #EC407A + #E91E63 + #D81B60 + #C2185B + #AD1457 + #880E4F + + + + #F3E5F5 + #E1BEE7 + #CE93D8 + #BA68C8 + #AB47BC + #9C27B0 + #8E24AA + #7B1FA2 + #6A1B9A + #4A148C + + + + #EDE7F6 + #D1C4E9 + #B39DDB + #9575CD + #7E57C2 + #673AB7 + #5E35B1 + #512DA8 + #4527A0 + #311B92 + + + + #E8EAF6 + #C5CAE9 + #9FA8DA + #7986CB + #5C6BC0 + #3F51B5 + #3949AB + #303F9F + #283593 + #1A237E + + + + #E3F2FD + #BBDEFB + #90CAF9 + #64B5F6 + #42A5F5 + #2196F3 + #1E88E5 + #1976D2 + #1565C0 + #0D47A1 + + + + #E1F5FE + #B3E5FC + #81D4FA + #4FC3F7 + #29B6F6 + #03A9F4 + #039BE5 + #0288D1 + #0277BD + #01579B + + + + #E0F7FA + #B2EBF2 + #80DEEA + #4DD0E1 + #26C6DA + #00BCD4 + #00ACC1 + #0097A7 + #00838F + #006064 + + + + #E0F2F1 + #B2DFDB + #80CBC4 + #4DB6AC + #26A69A + #009688 + #00897B + #00796B + #00695C + #004D40 + + + + #E8F5E9 + #C8E6C9 + #A5D6A7 + #81C784 + #66BB6A + #4CAF50 + #43A047 + #388E3C + #2E7D32 + #1B5E20 + + + + #F1F8E9 + #DCEDC8 + #C5E1A5 + #AED581 + #9CCC65 + #8BC34A + #7CB342 + #689F38 + #558B2F + #33691E + + + + #F9FBE7 + #F0F4C3 + #E6EE9C + #DCE775 + #D4E157 + #CDDC39 + #C0CA33 + #AFB42B + #9E9D24 + #827717 + + + + #FFFDE7 + #FFF9C4 + #FFF59D + #FFF176 + #FFEE58 + #FFEB3B + #FDD835 + #FBC02D + #F9A825 + #F57F17 + + + + #FFF8E1 + #FFECB3 + #FFE082 + #FFD54F + #FFCA28 + #FFC107 + #FFB300 + #FFA000 + #FF8F00 + #FF6F00 + + + + #FFF3E0 + #FFE0B2 + #FFCC80 + #FFB74D + #FFA726 + #FF9800 + #FB8C00 + #F57C00 + #EF6C00 + #E65100 + + + + #FBE9E7 + #FFCCBC + #FFAB91 + #FF8A65 + #FF7043 + #FF5722 + #F4511E + #E64A19 + #D84315 + #BF360C + + + + #EFEBE9 + #D7CCC8 + #BCAAA4 + #A1887F + #8D6E63 + #795548 + #6D4C41 + #5D4037 + #4E342E + #3E2723 + + + + #FAFAFA + #F5F5F5 + #EEEEEE + #E0E0E0 + #BDBDBD + #9E9E9E + #757575 + #616161 + #424242 + #212121 + + + + #ECEFF1 + #CFD8DC + #B0BEC5 + #90A4AE + #78909C + #607D8B + #546E7A + #455A64 + #37474F + #263238 + + diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 157043be6..c482009c2 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -203,6 +203,7 @@ Pure black night mode Start time End time + Automatic contact colors Font size Use system font Automatic emoji -- GitLab From aef8c22ea6acbc16a129a9bc724598d837b8e96c Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Thu, 26 Dec 2019 19:34:39 -0500 Subject: [PATCH 051/109] Show custom/random colours in compose screen --- .../QKSMS/extensions/CollectionExtensions.kt | 13 ++++++ .../repository/ConversationRepositoryImpl.kt | 11 +++++ .../repository/ConversationRepository.kt | 2 + .../compose/editing/ComposeItemAdapter.kt | 40 ++++++++++++++++--- 4 files changed, 61 insertions(+), 5 deletions(-) create mode 100644 data/src/main/java/com/moez/QKSMS/extensions/CollectionExtensions.kt diff --git a/data/src/main/java/com/moez/QKSMS/extensions/CollectionExtensions.kt b/data/src/main/java/com/moez/QKSMS/extensions/CollectionExtensions.kt new file mode 100644 index 000000000..e22d70dd8 --- /dev/null +++ b/data/src/main/java/com/moez/QKSMS/extensions/CollectionExtensions.kt @@ -0,0 +1,13 @@ +package com.moez.QKSMS.extensions + +inline fun Iterable.associateByNotNull(keySelector: (T) -> K?): Map { + val map = hashMapOf() + forEach { value -> + val key = keySelector(value) + if (key != null) { + map[key] = value + } + } + + return map +} 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 24935ee6b..02ac38f7e 100644 --- a/data/src/main/java/com/moez/QKSMS/repository/ConversationRepositoryImpl.kt +++ b/data/src/main/java/com/moez/QKSMS/repository/ConversationRepositoryImpl.kt @@ -206,6 +206,17 @@ class ConversationRepositoryImpl @Inject constructor( .observeOn(Schedulers.io()) } + override fun getUnmanagedRecipients(): Observable> { + val realm = Realm.getDefaultInstance() + return realm.where(Recipient::class.java) + .isNotNull("contact") + .findAllAsync() + .asObservable() + .filter { it.isLoaded && it.isValid } + .map { realm.copyFromRealm(it) } + .subscribeOn(AndroidSchedulers.mainThread()) + } + override fun getRecipient(recipientId: Long): Recipient? { return Realm.getDefaultInstance() .where(Recipient::class.java) 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 a6f1a0711..0377c6770 100644 --- a/domain/src/main/java/com/moez/QKSMS/repository/ConversationRepository.kt +++ b/domain/src/main/java/com/moez/QKSMS/repository/ConversationRepository.kt @@ -54,6 +54,8 @@ interface ConversationRepository { fun getUnmanagedConversations(): Observable> + fun getUnmanagedRecipients(): Observable> + fun getRecipient(recipientId: Long): Recipient? fun getThreadId(recipient: String): Long? diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ComposeItemAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ComposeItemAdapter.kt index 101e7ba59..718edcc13 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ComposeItemAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ComposeItemAdapter.kt @@ -29,21 +29,35 @@ 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.extensions.associateByNotNull 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 com.moez.QKSMS.repository.ConversationRepository +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.rxkotlin.plusAssign 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() { +class ComposeItemAdapter @Inject constructor( + private val colors: Colors, + private val conversationRepo: ConversationRepository +) : QkAdapter() { val clicks: Subject = PublishSubject.create() val longClicks: Subject = PublishSubject.create() private val numbersViewPool = RecyclerView.RecycledViewPool() + private val disposables = CompositeDisposable() + + var recipients: Map = mapOf() + set(value) { + field = value + notifyDataSetChanged() + } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkViewHolder { val layoutInflater = LayoutInflater.from(parent.context) @@ -87,7 +101,7 @@ class ComposeItemAdapter @Inject constructor(private val colors: Colors) : QkAda view.icon.isVisible = false - view.avatar.recipients = listOf(Recipient(contact = contact)) + view.avatar.recipients = listOf(createRecipient(contact)) view.title.text = contact.numbers.joinToString { it.address } @@ -123,7 +137,7 @@ class ComposeItemAdapter @Inject constructor(private val colors: Colors) : QkAda view.icon.isVisible = prev !is ComposeItem.Starred view.icon.setImageResource(R.drawable.ic_star_black_24dp) - view.avatar.recipients = listOf(Recipient(contact = contact)) + view.avatar.recipients = listOf(createRecipient(contact)) view.title.text = contact.name @@ -139,7 +153,7 @@ class ComposeItemAdapter @Inject constructor(private val colors: Colors) : QkAda view.icon.isVisible = prev !is ComposeItem.Group view.icon.setImageResource(R.drawable.ic_people_black_24dp) - view.avatar.recipients = group.contacts.map { contact -> Recipient(contact = contact) } + view.avatar.recipients = group.contacts.map(::createRecipient) view.title.text = group.title @@ -158,7 +172,7 @@ class ComposeItemAdapter @Inject constructor(private val colors: Colors) : QkAda view.icon.isVisible = false - view.avatar.recipients = listOf(Recipient(contact = contact)) + view.avatar.recipients = listOf(createRecipient(contact)) view.title.text = contact.name @@ -168,6 +182,22 @@ class ComposeItemAdapter @Inject constructor(private val colors: Colors) : QkAda (view.numbers.adapter as PhoneNumberAdapter).data = contact.numbers } + private fun createRecipient(contact: Contact): Recipient { + return recipients[contact.lookupKey] ?: Recipient( + address = contact.numbers.firstOrNull()?.address ?: "", + contact = contact) + } + + override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { + disposables += conversationRepo.getUnmanagedRecipients() + .map { recipients -> recipients.associateByNotNull { recipient -> recipient.contact?.lookupKey } } + .subscribe { recipients -> this@ComposeItemAdapter.recipients = recipients } + } + + override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { + disposables.clear() + } + override fun areItemsTheSame(old: ComposeItem, new: ComposeItem): Boolean { val oldIds = old.getContacts().map { contact -> contact.lookupKey } val newIds = new.getContacts().map { contact -> contact.lookupKey } -- GitLab From e70269b7e5c0c542724497b3a80ed4a243d36be2 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Fri, 27 Dec 2019 00:39:00 -0500 Subject: [PATCH 052/109] Long-press to copy phone number Closes #1348 --- .../ConversationInfoController.kt | 4 +++- .../ConversationInfoPresenter.kt | 19 ++++++++++++++++++- .../conversationinfo/ConversationInfoView.kt | 3 ++- .../ConversationRecipientAdapter.kt | 11 +++++++++-- presentation/src/main/res/values/strings.xml | 1 + 5 files changed, 33 insertions(+), 5 deletions(-) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoController.kt b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoController.kt index af8a345e8..d618b344d 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoController.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoController.kt @@ -90,7 +90,9 @@ class ConversationInfoController( showBackButton(true) } - override fun contactClicks(): Observable = recipientAdapter.contactClicks + override fun recipientClicks(): Observable = recipientAdapter.recipientClicks + + override fun recipientLongClicks(): Observable = recipientAdapter.recipientLongClicks override fun themeClicks(): Observable = recipientAdapter.themeClicks diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoPresenter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoPresenter.kt index 10c7b7814..f595b0623 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoPresenter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoPresenter.kt @@ -18,9 +18,13 @@ */ package com.moez.QKSMS.feature.conversationinfo +import android.content.Context import androidx.lifecycle.Lifecycle +import com.moez.QKSMS.R import com.moez.QKSMS.common.Navigator import com.moez.QKSMS.common.base.QkPresenter +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.interactor.DeleteConversations @@ -32,6 +36,7 @@ import com.moez.QKSMS.repository.ConversationRepository 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.plusAssign import io.reactivex.rxkotlin.withLatestFrom import io.reactivex.subjects.BehaviorSubject @@ -42,6 +47,7 @@ import javax.inject.Named class ConversationInfoPresenter @Inject constructor( @Named("threadId") threadId: Long, messageRepo: MessageRepository, + private val context: Context, private val conversationRepo: ConversationRepository, private val deleteConversations: DeleteConversations, private val markArchived: MarkArchived, @@ -100,7 +106,7 @@ class ConversationInfoPresenter @Inject constructor( super.bindIntents(view) // Add or display the contact - view.contactClicks() + view.recipientClicks() .mapNotNull(conversationRepo::getRecipient) .doOnNext { recipient -> recipient.contact?.lookupKey?.let(navigator::showContact) @@ -109,6 +115,17 @@ class ConversationInfoPresenter @Inject constructor( .autoDisposable(view.scope(Lifecycle.Event.ON_DESTROY)) // ... this should be the default .subscribe() + // Copy phone number + view.recipientLongClicks() + .mapNotNull(conversationRepo::getRecipient) + .map { recipient -> recipient.address } + .observeOn(AndroidSchedulers.mainThread()) + .autoDisposable(view.scope()) + .subscribe { address -> + ClipboardUtils.copy(context, address) + context.makeToast(R.string.info_copied_address) + } + // Show the theme settings for the conversation view.themeClicks() .autoDisposable(view.scope()) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoView.kt b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoView.kt index 1b71de22b..343dd8d03 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoView.kt @@ -23,7 +23,8 @@ import io.reactivex.Observable interface ConversationInfoView : QkViewContract { - fun contactClicks(): Observable + fun recipientClicks(): Observable + fun recipientLongClicks(): Observable fun themeClicks(): Observable fun nameClicks(): Observable<*> fun nameChanges(): Observable diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationRecipientAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationRecipientAdapter.kt index 35baf7bed..41b6cffa4 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationRecipientAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationRecipientAdapter.kt @@ -36,7 +36,8 @@ class ConversationRecipientAdapter @Inject constructor( private val colors: Colors ) : QkRealmAdapter() { - val contactClicks: Subject = PublishSubject.create() + val recipientClicks: Subject = PublishSubject.create() + val recipientLongClicks: Subject = PublishSubject.create() val themeClicks: Subject = PublishSubject.create() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkViewHolder { @@ -45,7 +46,13 @@ class ConversationRecipientAdapter @Inject constructor( return QkViewHolder(view).apply { view.setOnClickListener { val recipient = getItem(adapterPosition) ?: return@setOnClickListener - contactClicks.onNext(recipient.id) + recipientClicks.onNext(recipient.id) + } + + view.setOnLongClickListener { + val recipient = getItem(adapterPosition) ?: return@setOnLongClickListener false + recipientLongClicks.onNext(recipient.id) + return@setOnLongClickListener true } view.theme.setOnClickListener { diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index c482009c2..76aff3851 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -136,6 +136,7 @@ Failed to send. Tap to try again Details + Address copied Conversation title Notifications Theme -- GitLab From 55e4d6f6a598230bf7be0fac09d31a1001cc7c53 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Fri, 27 Dec 2019 01:20:10 -0500 Subject: [PATCH 053/109] Use install referrer api --- data/build.gradle | 1 + .../moez/QKSMS/manager/ReferralManagerImpl.kt | 57 +++++++++++++++++++ .../com/moez/QKSMS/manager/ReferralManager.kt | 7 +++ .../java/com/moez/QKSMS/util/Preferences.kt | 1 + .../com/moez/QKSMS/common/QKApplication.kt | 9 ++- .../com/moez/QKSMS/injection/AppModule.kt | 5 ++ 6 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 data/src/main/java/com/moez/QKSMS/manager/ReferralManagerImpl.kt create mode 100644 domain/src/main/java/com/moez/QKSMS/manager/ReferralManager.kt diff --git a/data/build.gradle b/data/build.gradle index bb2f87704..0425532ae 100644 --- a/data/build.gradle +++ b/data/build.gradle @@ -86,6 +86,7 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-reactive:$coroutines_version" + implementation 'com.android.installreferrer:installreferrer:1.1' implementation 'com.callcontrol:datashare:1.2.0' implementation "com.f2prateek.rx.preferences2:rx-preferences:$rx_preferences_version" implementation "com.jakewharton.timber:timber:$timber_version" diff --git a/data/src/main/java/com/moez/QKSMS/manager/ReferralManagerImpl.kt b/data/src/main/java/com/moez/QKSMS/manager/ReferralManagerImpl.kt new file mode 100644 index 000000000..4446d6740 --- /dev/null +++ b/data/src/main/java/com/moez/QKSMS/manager/ReferralManagerImpl.kt @@ -0,0 +1,57 @@ +package com.moez.QKSMS.manager + +import android.content.Context +import com.android.installreferrer.api.InstallReferrerClient +import com.android.installreferrer.api.InstallReferrerStateListener +import com.moez.QKSMS.util.Preferences +import kotlinx.coroutines.suspendCancellableCoroutine +import javax.inject.Inject +import kotlin.coroutines.resume + +class ReferralManagerImpl @Inject constructor( + private val analytics: AnalyticsManager, + private val context: Context, + private val prefs: Preferences +) : ReferralManager { + + override suspend fun trackReferrer() { + if (prefs.didSetReferrer.get()) { + return + } + + context.packageManager.getInstallerPackageName(context.packageName)?.let { installer -> + analytics.setUserProperty("Installer", installer) + } + + val referrerClient = InstallReferrerClient.newBuilder(context).build() + val responseCode = suspendCancellableCoroutine { cont -> + referrerClient.startConnection(object : InstallReferrerStateListener { + override fun onInstallReferrerSetupFinished(responseCode: Int) { + cont.resume(responseCode) + } + + override fun onInstallReferrerServiceDisconnected() { + cont.resume(InstallReferrerClient.InstallReferrerResponse.SERVICE_DISCONNECTED) + } + }) + + cont.invokeOnCancellation { + referrerClient.endConnection() + } + } + + when (responseCode) { + InstallReferrerClient.InstallReferrerResponse.OK -> { + analytics.setUserProperty("Referrer", referrerClient.installReferrer.installReferrer) + prefs.didSetReferrer.set(true) + } + + InstallReferrerClient.InstallReferrerResponse.FEATURE_NOT_SUPPORTED -> { + prefs.didSetReferrer.set(true) + } + } + + referrerClient.endConnection() + } + +} diff --git a/domain/src/main/java/com/moez/QKSMS/manager/ReferralManager.kt b/domain/src/main/java/com/moez/QKSMS/manager/ReferralManager.kt new file mode 100644 index 000000000..30bcfff40 --- /dev/null +++ b/domain/src/main/java/com/moez/QKSMS/manager/ReferralManager.kt @@ -0,0 +1,7 @@ +package com.moez.QKSMS.manager + +interface ReferralManager { + + suspend fun trackReferrer() + +} diff --git a/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt b/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt index f21adcc96..dbb796123 100644 --- a/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt +++ b/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt @@ -75,6 +75,7 @@ class Preferences @Inject constructor( } // Internal + val didSetReferrer = rxPrefs.getBoolean("didSetReferrer", false) val night = rxPrefs.getBoolean("night", false) val canUseSubId = rxPrefs.getBoolean("canUseSubId", true) val version = rxPrefs.getInteger("version", context.versionCode) diff --git a/presentation/src/main/java/com/moez/QKSMS/common/QKApplication.kt b/presentation/src/main/java/com/moez/QKSMS/common/QKApplication.kt index 48faf2c4c..8f94dbe03 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/QKApplication.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/QKApplication.kt @@ -31,6 +31,7 @@ import com.moez.QKSMS.common.util.FileLoggingTree import com.moez.QKSMS.injection.AppComponentManager import com.moez.QKSMS.injection.appComponent import com.moez.QKSMS.manager.AnalyticsManager +import com.moez.QKSMS.manager.ReferralManager import com.moez.QKSMS.migration.QkMigration import com.moez.QKSMS.migration.QkRealmMigration import com.moez.QKSMS.util.NightModeManager @@ -43,6 +44,9 @@ import dagger.android.HasBroadcastReceiverInjector import dagger.android.HasServiceInjector import io.realm.Realm import io.realm.RealmConfiguration +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -63,6 +67,7 @@ class QKApplication : Application(), HasActivityInjector, HasBroadcastReceiverIn lateinit var fileLoggingTree: FileLoggingTree @Inject lateinit var nightModeManager: NightModeManager @Inject lateinit var realmMigration: QkRealmMigration + @Inject lateinit var referralManager: ReferralManager override fun onCreate() { super.onCreate() @@ -77,8 +82,8 @@ class QKApplication : Application(), HasActivityInjector, HasBroadcastReceiverIn .schemaVersion(QkRealmMigration.SchemaVersion) .build()) - packageManager.getInstallerPackageName(packageName)?.let { installer -> - analyticsManager.setUserProperty("Installer", installer) + GlobalScope.launch(Dispatchers.IO) { + referralManager.trackReferrer() } nightModeManager.updateCurrentTheme() diff --git a/presentation/src/main/java/com/moez/QKSMS/injection/AppModule.kt b/presentation/src/main/java/com/moez/QKSMS/injection/AppModule.kt index 5cd607c04..edc51c508 100644 --- a/presentation/src/main/java/com/moez/QKSMS/injection/AppModule.kt +++ b/presentation/src/main/java/com/moez/QKSMS/injection/AppModule.kt @@ -47,6 +47,8 @@ import com.moez.QKSMS.manager.KeyManagerImpl import com.moez.QKSMS.manager.NotificationManager import com.moez.QKSMS.manager.PermissionManager import com.moez.QKSMS.manager.PermissionManagerImpl +import com.moez.QKSMS.manager.ReferralManager +import com.moez.QKSMS.manager.ReferralManagerImpl import com.moez.QKSMS.manager.ShortcutManager import com.moez.QKSMS.manager.WidgetManager import com.moez.QKSMS.manager.WidgetManagerImpl @@ -155,6 +157,9 @@ class AppModule(private var application: Application) { @Provides fun provideShortcutManager(manager: ShortcutManagerImpl): ShortcutManager = manager + @Provides + fun provideReferralManager(manager: ReferralManagerImpl): ReferralManager = manager + @Provides fun provideWidgetManager(manager: WidgetManagerImpl): WidgetManager = manager -- GitLab From b961c6d33772716a46cd029393f2cb4eb8b6c9b4 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Fri, 27 Dec 2019 02:13:33 -0500 Subject: [PATCH 054/109] Include version in output file name --- presentation/build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/presentation/build.gradle b/presentation/build.gradle index ccd4f721c..44873ea83 100644 --- a/presentation/build.gradle +++ b/presentation/build.gradle @@ -34,6 +34,8 @@ android { versionCode 2209 versionName "3.7.10" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + setProperty("archivesBaseName", "QKSMS-v${versionName}") } /* signingConfigs { -- GitLab From d342d2c7e70e096a7cd55862f57ca5bd9d3a5ad4 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Fri, 27 Dec 2019 18:58:34 -0500 Subject: [PATCH 055/109] Upgrade to SDK 29, use requestLegacyExternalStorage --- common/build.gradle | 2 +- data/build.gradle | 2 +- domain/build.gradle | 2 +- presentation/build.gradle | 2 +- presentation/src/main/AndroidManifest.xml | 1 + 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/common/build.gradle b/common/build.gradle index ec0f93d96..a893d1c18 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -24,7 +24,7 @@ android { defaultConfig { minSdkVersion 21 - targetSdkVersion 28 // Don't upgrade to 29 until we support Scoped storage + targetSdkVersion 29 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/data/build.gradle b/data/build.gradle index 0425532ae..a78666df9 100644 --- a/data/build.gradle +++ b/data/build.gradle @@ -32,7 +32,7 @@ android { defaultConfig { minSdkVersion 21 - targetSdkVersion 28 // Don't upgrade to 29 until we support Scoped storage + targetSdkVersion 29 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" buildConfigField "String", "AMPLITUDE_API_KEY", "\"${System.getenv("AMPLITUDE_API_KEY")}\"" diff --git a/domain/build.gradle b/domain/build.gradle index b7ab9bc62..5c7277b38 100644 --- a/domain/build.gradle +++ b/domain/build.gradle @@ -32,7 +32,7 @@ android { defaultConfig { minSdkVersion 21 - targetSdkVersion 28 // Don't upgrade to 29 until we support Scoped storage + targetSdkVersion 29 } compileOptions { diff --git a/presentation/build.gradle b/presentation/build.gradle index 44873ea83..f9f19e07e 100644 --- a/presentation/build.gradle +++ b/presentation/build.gradle @@ -30,7 +30,7 @@ android { defaultConfig { applicationId "foundation.e.message" minSdkVersion 21 - targetSdkVersion 28 // Don't upgrade to 29 until we support Scoped storage + targetSdkVersion 29 versionCode 2209 versionName "3.7.10" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/presentation/src/main/AndroidManifest.xml b/presentation/src/main/AndroidManifest.xml index a64c8d86b..1f79498e3 100644 --- a/presentation/src/main/AndroidManifest.xml +++ b/presentation/src/main/AndroidManifest.xml @@ -40,6 +40,7 @@ android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" + android:requestLegacyExternalStorage="true" android:supportsRtl="true" android:theme="@style/AppLaunchTheme"> -- GitLab From 91f548aab1868de9d6087dd8c29f83a8f78dce46 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Fri, 27 Dec 2019 19:11:25 -0500 Subject: [PATCH 056/109] Max width for message bubbles --- presentation/src/main/res/layout/message_list_item_in.xml | 3 ++- presentation/src/main/res/layout/message_list_item_out.xml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/presentation/src/main/res/layout/message_list_item_in.xml b/presentation/src/main/res/layout/message_list_item_in.xml index 2201260d8..0c5d7f90a 100644 --- a/presentation/src/main/res/layout/message_list_item_in.xml +++ b/presentation/src/main/res/layout/message_list_item_in.xml @@ -103,8 +103,9 @@ app:layout_constraintHorizontal_bias="0" app:layout_constraintStart_toEndOf="@id/avatar" app:layout_constraintTop_toBottomOf="@id/attachments" + app:layout_constraintWidth_max="384dp" tools:backgroundTint="@color/tools_theme" - tools:text="@tools:sample/lorem" /> + tools:text="@tools:sample/lorem/random" /> Date: Sat, 28 Dec 2019 00:41:11 -0500 Subject: [PATCH 057/109] Store stripped message #1149 --- .../com/moez/QKSMS/repository/MessageRepositoryImpl.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/data/src/main/java/com/moez/QKSMS/repository/MessageRepositoryImpl.kt b/data/src/main/java/com/moez/QKSMS/repository/MessageRepositoryImpl.kt index 2b4ba0687..805dda88c 100644 --- a/data/src/main/java/com/moez/QKSMS/repository/MessageRepositoryImpl.kt +++ b/data/src/main/java/com/moez/QKSMS/repository/MessageRepositoryImpl.kt @@ -302,10 +302,16 @@ class MessageRepositoryImpl @Inject constructor( else -> prefs.signature.get() } + // We only care about stripping SMS + val strippedBody = when (prefs.unicode.get()) { + true -> StripAccents.stripAccents(signedBody) + false -> signedBody + } + if (addresses.size == 1 && attachments.isEmpty()) { // SMS if (delay > 0) { // With delay val sendTime = System.currentTimeMillis() + delay - val message = insertSentSms(subId, threadId, addresses.first(), signedBody, sendTime) + val message = insertSentSms(subId, threadId, addresses.first(), strippedBody, sendTime) val intent = getIntentForDelayedSms(message.id) @@ -316,7 +322,7 @@ class MessageRepositoryImpl @Inject constructor( alarmManager.setExact(AlarmManager.RTC_WAKEUP, sendTime, intent) } } else { // No delay - val message = insertSentSms(subId, threadId, addresses.first(), signedBody, System.currentTimeMillis()) + val message = insertSentSms(subId, threadId, addresses.first(), strippedBody, System.currentTimeMillis()) sendSms(message) } } else { // MMS -- GitLab From 838a26628a265d9583b9feb471e5bb9b79c15252 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Sat, 28 Dec 2019 01:00:56 -0500 Subject: [PATCH 058/109] Add option to send long messages as MMS Closes #1104 --- .../com/moez/QKSMS/repository/MessageRepositoryImpl.kt | 9 ++++++++- domain/src/main/java/com/moez/QKSMS/util/Preferences.kt | 1 + .../moez/QKSMS/feature/settings/SettingsController.kt | 1 + .../com/moez/QKSMS/feature/settings/SettingsPresenter.kt | 5 +++++ .../com/moez/QKSMS/feature/settings/SettingsState.kt | 1 + .../src/main/res/drawable/ic_message_black_24dp.xml | 9 +++++++++ presentation/src/main/res/layout/settings_controller.xml | 9 +++++++++ presentation/src/main/res/values/strings.xml | 2 ++ 8 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 presentation/src/main/res/drawable/ic_message_black_24dp.xml diff --git a/data/src/main/java/com/moez/QKSMS/repository/MessageRepositoryImpl.kt b/data/src/main/java/com/moez/QKSMS/repository/MessageRepositoryImpl.kt index 805dda88c..af81e68af 100644 --- a/data/src/main/java/com/moez/QKSMS/repository/MessageRepositoryImpl.kt +++ b/data/src/main/java/com/moez/QKSMS/repository/MessageRepositoryImpl.kt @@ -302,13 +302,20 @@ class MessageRepositoryImpl @Inject constructor( else -> prefs.signature.get() } + val smsManager = subId.takeIf { it != -1 } + ?.let(SmsManagerFactory::createSmsManager) + ?: SmsManager.getDefault() + // We only care about stripping SMS val strippedBody = when (prefs.unicode.get()) { true -> StripAccents.stripAccents(signedBody) false -> signedBody } - if (addresses.size == 1 && attachments.isEmpty()) { // SMS + val parts = smsManager.divideMessage(strippedBody).orEmpty() + val forceMms = prefs.longAsMms.get() && parts.size > 1 + + if (addresses.size == 1 && attachments.isEmpty() && !forceMms) { // SMS if (delay > 0) { // With delay val sendTime = System.currentTimeMillis() + delay val message = insertSentSms(subId, threadId, addresses.first(), strippedBody, sendTime) diff --git a/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt b/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt index dbb796123..d783e0249 100644 --- a/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt +++ b/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt @@ -109,6 +109,7 @@ class Preferences @Inject constructor( val signature = rxPrefs.getString("signature", "") val unicode = rxPrefs.getBoolean("unicode", false) val mobileOnly = rxPrefs.getBoolean("mobileOnly", false) + val longAsMms = rxPrefs.getBoolean("longAsMms", false) val mmsSize = rxPrefs.getInteger("mmsSize", 300) val logging = rxPrefs.getBoolean("logging", false) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsController.kt b/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsController.kt index ce5294a8f..f7629b0bd 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsController.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsController.kt @@ -167,6 +167,7 @@ class SettingsController : QkController newState { copy(mobileOnly = enabled) } } + disposables += prefs.longAsMms.asObservable() + .subscribe { enabled -> newState { copy(longAsMms = enabled) } } + val mmsSizeLabels = context.resources.getStringArray(R.array.mms_sizes) val mmsSizeIds = context.resources.getIntArray(R.array.mms_sizes_ids) disposables += prefs.mmsSize.asObservable() @@ -189,6 +192,8 @@ class SettingsPresenter @Inject constructor( R.id.mobileOnly -> prefs.mobileOnly.set(!prefs.mobileOnly.get()) + R.id.longAsMms -> prefs.longAsMms.set(!prefs.longAsMms.get()) + R.id.mmsSize -> view.showMmsSizePicker() R.id.sync -> syncMessages.execute(Unit) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsState.kt b/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsState.kt index 1119b737a..f18ccd800 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsState.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsState.kt @@ -41,6 +41,7 @@ data class SettingsState( val splitSmsEnabled: Boolean = false, val stripUnicodeEnabled: Boolean = false, val mobileOnly: Boolean = false, + val longAsMms: Boolean = false, val maxMmsSizeSummary: String = "100KB", val maxMmsSizeId: Int = 100, val syncProgress: SyncRepository.SyncProgress = SyncRepository.SyncProgress.Idle diff --git a/presentation/src/main/res/drawable/ic_message_black_24dp.xml b/presentation/src/main/res/drawable/ic_message_black_24dp.xml new file mode 100644 index 000000000..d2876bfad --- /dev/null +++ b/presentation/src/main/res/drawable/ic_message_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/presentation/src/main/res/layout/settings_controller.xml b/presentation/src/main/res/layout/settings_controller.xml index d0f4ab86b..cc7259032 100644 --- a/presentation/src/main/res/layout/settings_controller.xml +++ b/presentation/src/main/res/layout/settings_controller.xml @@ -177,6 +177,15 @@ app:title="@string/settings_mobile_title" app:widget="@layout/settings_switch_widget" /> + + Remove accents from characters in outgoing SMS messages Mobile numbers only When composing a message, only show mobile numbers + Send long messages as MMS + If your longer text messages are failing to send, or sending in the wrong order, you can send them as MMS messages instead. Additional charges may apply Auto-compress MMS attachments Sync messages Re-sync your messages with the native Android SMS database -- GitLab From 43d64a57c5d41d601d772a0c78723c3ec92a676d Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Sat, 28 Dec 2019 03:06:35 -0500 Subject: [PATCH 059/109] Sort drafts first, save drafts for new conversations, show drafts in widget Closes #1296 --- .../repository/ConversationRepositoryImpl.kt | 22 +++++++++++++++---- .../conversations/ConversationsAdapter.kt | 8 +++---- .../QKSMS/feature/widget/WidgetAdapter.kt | 9 ++++++-- 3 files changed, 29 insertions(+), 10 deletions(-) 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 02ac38f7e..9cb9aad1a 100644 --- a/data/src/main/java/com/moez/QKSMS/repository/ConversationRepositoryImpl.kt +++ b/data/src/main/java/com/moez/QKSMS/repository/ConversationRepositoryImpl.kt @@ -58,11 +58,18 @@ class ConversationRepositoryImpl @Inject constructor( return Realm.getDefaultInstance() .where(Conversation::class.java) .notEqualTo("id", 0L) - .isNotNull("lastMessage") .equalTo("archived", archived) .equalTo("blocked", false) .isNotEmpty("recipients") - .sort("pinned", Sort.DESCENDING, "lastMessage.date", Sort.DESCENDING) + .beginGroup() + .isNotNull("lastMessage") + .or() + .isNotEmpty("draft") + .endGroup() + .sort( + arrayOf("pinned", "draft", "lastMessage.date"), + arrayOf(Sort.DESCENDING, Sort.DESCENDING, Sort.DESCENDING) + ) .findAllAsync() } @@ -71,11 +78,18 @@ class ConversationRepositoryImpl @Inject constructor( realm.refresh() realm.copyFromRealm(realm.where(Conversation::class.java) .notEqualTo("id", 0L) - .isNotNull("lastMessage") .equalTo("archived", false) .equalTo("blocked", false) .isNotEmpty("recipients") - .sort("pinned", Sort.DESCENDING, "lastMessage.date", Sort.DESCENDING) + .beginGroup() + .isNotNull("lastMessage") + .or() + .isNotEmpty("draft") + .endGroup() + .sort( + arrayOf("pinned", "draft", "lastMessage.date"), + arrayOf(Sort.DESCENDING, Sort.DESCENDING, Sort.DESCENDING) + ) .findAll()) } } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/conversations/ConversationsAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/conversations/ConversationsAdapter.kt index 0b32f81dd..6285346f5 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/conversations/ConversationsAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/conversations/ConversationsAdapter.kt @@ -86,7 +86,6 @@ class ConversationsAdapter @Inject constructor( override fun onBindViewHolder(viewHolder: QkViewHolder, position: Int) { val conversation = getItem(position) ?: return - val lastMessage = conversation.lastMessage ?: return val view = viewHolder.containerView view.isActivated = isSelected(conversation.id) @@ -94,7 +93,7 @@ class ConversationsAdapter @Inject constructor( view.avatars.recipients = conversation.recipients view.title.collapseEnabled = conversation.recipients.size > 1 view.title.text = conversation.getTitle() - view.date.text = dateFormatter.getConversationTimestamp(conversation.date) + view.date.text = conversation.date.takeIf { it > 0 }?.let(dateFormatter::getConversationTimestamp) view.snippet.text = when { conversation.draft.isNotEmpty() -> context.getString(R.string.main_draft, conversation.draft) conversation.me -> context.getString(R.string.main_sender_you, conversation.snippet) @@ -103,8 +102,9 @@ class ConversationsAdapter @Inject constructor( view.pinned.isVisible = conversation.pinned // If the last message wasn't incoming, then the colour doesn't really matter anyway - val recipient = when (conversation.recipients.size) { - 1 -> conversation.recipients.firstOrNull() + val lastMessage = conversation.lastMessage + val recipient = when { + conversation.recipients.size == 1 || lastMessage == null -> conversation.recipients.firstOrNull() else -> conversation.recipients.find { recipient -> phoneNumberUtils.compare(recipient.address, lastMessage.address) } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/widget/WidgetAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/widget/WidgetAdapter.kt index 410a9fd6d..f8011ef8a 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/widget/WidgetAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/widget/WidgetAdapter.kt @@ -142,13 +142,18 @@ class WidgetAdapter(intent: Intent) : RemoteViewsService.RemoteViewsFactory { remoteViews.setTextViewText(R.id.name, boldText(conversation.getTitle(), conversation.unread)) // Date - val timestamp = dateFormatter.getConversationTimestamp(conversation.date) + val timestamp = conversation.date.takeIf { it > 0 }?.let(dateFormatter::getConversationTimestamp) remoteViews.setTextColor(R.id.date, if (conversation.unread) textPrimary else textTertiary) remoteViews.setTextViewText(R.id.date, boldText(timestamp, conversation.unread)) // Snippet + val snippet = when { + conversation.draft.isNotEmpty() -> context.getString(R.string.main_draft, conversation.draft) + conversation.me -> context.getString(R.string.main_sender_you, conversation.snippet) + else -> conversation.snippet + } remoteViews.setTextColor(R.id.snippet, if (conversation.unread) textPrimary else textTertiary) - remoteViews.setTextViewText(R.id.snippet, boldText(conversation.snippet, conversation.unread)) + remoteViews.setTextViewText(R.id.snippet, boldText(snippet, conversation.unread)) // Launch conversation on click val clickIntent = Intent().putExtra("threadId", conversation.id) -- GitLab From b1a94143dbeabdaa0c42c26e0d3d98b8c72fecb4 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Sun, 29 Dec 2019 17:07:37 -0500 Subject: [PATCH 060/109] Correctly use ContainerView with ViewHolder --- .../com/moez/QKSMS/common/MenuItemAdapter.kt | 8 +- .../QKSMS/feature/backup/BackupAdapter.kt | 9 +- .../messages/BlockedMessagesAdapter.kt | 20 ++-- .../feature/compose/AttachmentAdapter.kt | 8 +- .../QKSMS/feature/compose/MessagesAdapter.kt | 57 +++++----- .../feature/compose/editing/ChipsAdapter.kt | 6 +- .../compose/editing/ComposeItemAdapter.kt | 103 +++++++++--------- .../compose/editing/PhoneNumberAdapter.kt | 7 +- .../QKSMS/feature/compose/part/FileBinder.kt | 36 +++--- .../QKSMS/feature/compose/part/MediaBinder.kt | 14 +-- .../QKSMS/feature/compose/part/PartBinder.kt | 4 +- .../feature/compose/part/PartsAdapter.kt | 15 ++- .../QKSMS/feature/compose/part/VCardBinder.kt | 34 +++--- .../ConversationMediaAdapter.kt | 6 +- .../ConversationRecipientAdapter.kt | 14 +-- .../conversations/ConversationsAdapter.kt | 20 ++-- .../feature/gallery/GalleryPagerAdapter.kt | 10 +- .../moez/QKSMS/feature/main/SearchAdapter.kt | 20 ++-- .../scheduled/ScheduledMessageAdapter.kt | 20 ++-- .../ScheduledMessageAttachmentAdapter.kt | 11 +- .../QKSMS/feature/themepicker/ThemeAdapter.kt | 10 +- 21 files changed, 218 insertions(+), 214 deletions(-) diff --git a/presentation/src/main/java/com/moez/QKSMS/common/MenuItemAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/common/MenuItemAdapter.kt index bc506cd80..656305482 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/MenuItemAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/MenuItemAdapter.kt @@ -33,6 +33,7 @@ import com.moez.QKSMS.common.util.extensions.setVisible import io.reactivex.disposables.CompositeDisposable import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.Subject +import kotlinx.android.synthetic.main.menu_list_item.* import kotlinx.android.synthetic.main.menu_list_item.view.* import javax.inject.Inject @@ -83,11 +84,10 @@ class MenuItemAdapter @Inject constructor(private val context: Context, private override fun onBindViewHolder(holder: QkViewHolder, position: Int) { val menuItem = getItem(position) - val view = holder.containerView - view.title.text = menuItem.title - view.check.isActivated = (menuItem.actionId == selectedItem) - view.check.setVisible(selectedItem != null) + holder.title.text = menuItem.title + holder.check.isActivated = (menuItem.actionId == selectedItem) + holder.check.setVisible(selectedItem != null) } override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupAdapter.kt index ff9270e8f..038295dc6 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupAdapter.kt @@ -29,7 +29,7 @@ import com.moez.QKSMS.common.util.DateFormatter import com.moez.QKSMS.model.BackupFile import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.Subject -import kotlinx.android.synthetic.main.backup_list_item.view.* +import kotlinx.android.synthetic.main.backup_list_item.* import javax.inject.Inject class BackupAdapter @Inject constructor( @@ -50,13 +50,12 @@ class BackupAdapter @Inject constructor( override fun onBindViewHolder(holder: QkViewHolder, position: Int) { val backup = getItem(position) - val view = holder.containerView val count = backup.messages - view.title.text = dateFormatter.getDetailedTimestamp(backup.date) - view.messages.text = context.resources.getQuantityString(R.plurals.backup_message_count, count, count) - view.size.text = Formatter.formatFileSize(context, backup.size) + holder.title.text = dateFormatter.getDetailedTimestamp(backup.date) + holder.messages.text = context.resources.getQuantityString(R.plurals.backup_message_count, count, count) + holder.size.text = Formatter.formatFileSize(context, backup.size) } } \ No newline at end of file diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/blocking/messages/BlockedMessagesAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/blocking/messages/BlockedMessagesAdapter.kt index af73405ed..9a4c37e38 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/blocking/messages/BlockedMessagesAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/blocking/messages/BlockedMessagesAdapter.kt @@ -31,6 +31,7 @@ import com.moez.QKSMS.common.util.extensions.resolveThemeColor import com.moez.QKSMS.model.Conversation import com.moez.QKSMS.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 @@ -69,24 +70,23 @@ class BlockedMessagesAdapter @Inject constructor( override fun onBindViewHolder(holder: QkViewHolder, position: Int) { val conversation = getItem(position) ?: return - val view = holder.containerView - view.isActivated = isSelected(conversation.id) + holder.containerView.isActivated = isSelected(conversation.id) - view.avatars.recipients = conversation.recipients - view.title.collapseEnabled = conversation.recipients.size > 1 - view.title.text = conversation.getTitle() - view.date.text = dateFormatter.getConversationTimestamp(conversation.date) + holder.avatars.recipients = conversation.recipients + holder.title.collapseEnabled = conversation.recipients.size > 1 + holder.title.text = conversation.getTitle() + holder.date.text = dateFormatter.getConversationTimestamp(conversation.date) - view.blocker.text = when (conversation.blockingClient) { + holder.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 } - view.reason.text = conversation.blockReason - view.blocker.isVisible = view.blocker.text.isNotEmpty() - view.reason.isVisible = view.blocker.text.isNotEmpty() + holder.reason.text = conversation.blockReason + holder.blocker.isVisible = holder.blocker.text.isNotEmpty() + holder.reason.isVisible = holder.blocker.text.isNotEmpty() } override fun getItemViewType(position: Int): Int { diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/AttachmentAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/AttachmentAdapter.kt index 82b910403..7c6ae2dae 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/AttachmentAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/AttachmentAdapter.kt @@ -34,7 +34,8 @@ import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.Subject -import kotlinx.android.synthetic.main.attachment_contact_list_item.view.* +import kotlinx.android.synthetic.main.attachment_contact_list_item.* +import kotlinx.android.synthetic.main.attachment_image_list_item.* import kotlinx.android.synthetic.main.attachment_image_list_item.view.* import javax.inject.Inject @@ -70,18 +71,17 @@ class AttachmentAdapter @Inject constructor( override fun onBindViewHolder(holder: QkViewHolder, position: Int) { val attachment = getItem(position) - val view = holder.containerView when (attachment) { is Attachment.Image -> Glide.with(context) .load(attachment.getUri()) - .into(view.thumbnail) + .into(holder.thumbnail) is Attachment.Contact -> Observable.just(attachment.vCard) .mapNotNull { vCard -> Ezvcard.parse(vCard).first() } .subscribeOn(Schedulers.computation()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe( { vcard -> view.name?.text = vcard.formattedName.value }, { throwable -> Log.i("AttachmentAdapter.kt", "Name field is null") } ) + .subscribe( { vcard -> holder.name?.text = vcard.formattedName.value }, { throwable -> Log.i("AttachmentAdapter.kt", "Name field is null") } ) } } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/MessagesAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/MessagesAdapter.kt index 88fbfb0ba..f11b33983 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/MessagesAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/MessagesAdapter.kt @@ -59,7 +59,15 @@ import com.moez.QKSMS.util.Preferences 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.attachments +import kotlinx.android.synthetic.main.message_list_item_in.body +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 java.util.* import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -175,11 +183,10 @@ class MessagesAdapter @Inject constructor( } } - override fun onBindViewHolder(viewHolder: QkViewHolder, position: Int) { + override fun onBindViewHolder(holder: QkViewHolder, position: Int) { val message = getItem(position) ?: return val previous = if (position == 0) null else getItem(position - 1) val next = if (position == itemCount - 1) null else getItem(position + 1) - val view = viewHolder.containerView val theme = when (message.isOutgoingMessage()) { true -> colors.theme() @@ -187,10 +194,10 @@ class MessagesAdapter @Inject constructor( } // Update the selected state - view.isActivated = isSelected(message.id) || highlight == message.id + holder.containerView.isActivated = isSelected(message.id) || highlight == message.id // Bind the cancel view - view.findViewById(R.id.cancel)?.let { cancel -> + holder.cancel?.let { cancel -> val isCancellable = message.isSending() && message.date > System.currentTimeMillis() cancel.setVisible(isCancellable) cancel.clicks().subscribe { cancelSending.onNext(message.id) } @@ -212,31 +219,31 @@ class MessagesAdapter @Inject constructor( } // Bind the message status - bindStatus(viewHolder, message, next) + bindStatus(holder, message, next) // Bind the timestamp val timeSincePrevious = TimeUnit.MILLISECONDS.toMinutes(message.date - (previous?.date ?: 0)) val simIndex = subs.takeIf { it.size > 1 }?.indexOfFirst { it.subscriptionId == message.subId } ?: -1 - view.timestamp.text = dateFormatter.getMessageTimestamp(message.date) - view.simIndex.text = "${simIndex + 1}" + holder.timestamp.text = dateFormatter.getMessageTimestamp(message.date) + holder.simIndex.text = "${simIndex + 1}" - view.timestamp.setVisible(timeSincePrevious >= BubbleUtils.TIMESTAMP_THRESHOLD + holder.timestamp.setVisible(timeSincePrevious >= BubbleUtils.TIMESTAMP_THRESHOLD || message.subId != previous?.subId && simIndex != -1) - view.sim.setVisible(message.subId != previous?.subId && simIndex != -1) - view.simIndex.setVisible(message.subId != previous?.subId && simIndex != -1) + holder.sim.setVisible(message.subId != previous?.subId && simIndex != -1) + holder.simIndex.setVisible(message.subId != previous?.subId && simIndex != -1) // Bind the grouping val media = message.parts.filter { !it.isSmil() && !it.isText() } - view.setPadding(bottom = if (canGroup(message, next)) 0 else 16.dpToPx(context)) + holder.containerView.setPadding(bottom = if (canGroup(message, next)) 0 else 16.dpToPx(context)) // Bind the avatar and bubble colour if (!message.isMe()) { - view.avatar.setRecipient(contactCache[message.address]) - view.avatar.setVisible(!canGroup(message, next), View.INVISIBLE) + holder.avatar.setRecipient(contactCache[message.address]) + holder.avatar.setVisible(!canGroup(message, next), View.INVISIBLE) - view.body.setTextColor(theme.textPrimary) - view.body.setBackgroundTint(theme.theme) + holder.body.setTextColor(theme.textPrimary) + holder.body.setBackgroundTint(theme.theme) } // Bind the body text @@ -262,31 +269,29 @@ class MessagesAdapter @Inject constructor( } } val emojiOnly = messageText.isNotBlank() && messageText.matches(EMOJI_REGEX) - textViewStyler.setTextSize(view.body, when (emojiOnly) { + textViewStyler.setTextSize(holder.body, when (emojiOnly) { true -> TextViewStyler.SIZE_EMOJI false -> TextViewStyler.SIZE_PRIMARY }) - view.body.text = messageText - view.body.setVisible(message.isSms() || messageText.isNotBlank()) - view.body.setBackgroundResource(getBubble( + holder.body.text = messageText + holder.body.setVisible(message.isSms() || messageText.isNotBlank()) + holder.body.setBackgroundResource(getBubble( emojiOnly = emojiOnly, canGroupWithPrevious = canGroup(message, previous) || media.isNotEmpty(), canGroupWithNext = canGroup(message, next), isMe = message.isMe())) // Bind the attachments - val partsAdapter = view.attachments.adapter as PartsAdapter + val partsAdapter = holder.attachments.adapter as PartsAdapter partsAdapter.theme = theme - partsAdapter.setData(message, previous, next, view) + partsAdapter.setData(message, previous, next, holder) } - private fun bindStatus(viewHolder: QkViewHolder, message: Message, next: Message?) { - val view = viewHolder.containerView - + private fun bindStatus(holder: QkViewHolder, message: Message, next: Message?) { val age = TimeUnit.MILLISECONDS.toMinutes(System.currentTimeMillis() - message.date) - view.status.text = when { + holder.status.text = when { message.isSending() -> context.getString(R.string.message_status_sending) message.isDelivered() -> context.getString(R.string.message_status_delivered, dateFormatter.getTimestamp(message.dateSent)) @@ -300,7 +305,7 @@ class MessagesAdapter @Inject constructor( else -> dateFormatter.getTimestamp(message.date) } - view.status.setVisible(when { + holder.status.setVisible(when { expanded[message.id] == true -> true message.isSending() -> true message.isFailedMessage() -> true diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ChipsAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ChipsAdapter.kt index 0bd7f0847..04f5e7c78 100755 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ChipsAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ChipsAdapter.kt @@ -33,7 +33,6 @@ import com.moez.QKSMS.common.util.extensions.setBackgroundTint import com.moez.QKSMS.model.Recipient import io.reactivex.subjects.PublishSubject import kotlinx.android.synthetic.main.contact_chip.* -import kotlinx.android.synthetic.main.contact_chip.view.* import javax.inject.Inject class ChipsAdapter @Inject constructor() : QkAdapter() { @@ -59,10 +58,9 @@ class ChipsAdapter @Inject constructor() : QkAdapter() { override fun onBindViewHolder(holder: QkViewHolder, position: Int) { val chip = getItem(position) - val view = holder.containerView - view.avatar.setRecipient(Recipient(id = -1, contact = chip.contact)) - view.name.text = chip.contact?.name?.takeIf { it.isNotBlank() } ?: chip.address + holder.avatar.setRecipient(Recipient(id = -1, contact = chip.contact)) + holder.name.text = chip.contact?.name?.takeIf { it.isNotBlank() } ?: chip.address } /** diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ComposeItemAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ComposeItemAdapter.kt index 718edcc13..3acb379eb 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ComposeItemAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ComposeItemAdapter.kt @@ -19,7 +19,6 @@ package com.moez.QKSMS.feature.compose.editing import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView @@ -39,6 +38,7 @@ import io.reactivex.disposables.CompositeDisposable import io.reactivex.rxkotlin.plusAssign 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 @@ -85,101 +85,100 @@ class ComposeItemAdapter @Inject constructor( 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) + 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) } } - private fun bindNew(view: View, contact: Contact) { - view.index.isVisible = false + private fun bindNew(holder: QkViewHolder, contact: Contact) { + holder.index.isVisible = false - view.icon.isVisible = false + holder.icon.isVisible = false - view.avatar.recipients = listOf(createRecipient(contact)) + holder.avatar.recipients = listOf(createRecipient(contact)) - view.title.text = contact.numbers.joinToString { it.address } + holder.title.text = contact.numbers.joinToString { it.address } - view.subtitle.isVisible = false + holder.subtitle.isVisible = false - view.numbers.isVisible = false + holder.numbers.isVisible = false } - private fun bindRecent(view: View, conversation: Conversation, prev: ComposeItem?) { - view.index.isVisible = false + private fun bindRecent(holder: QkViewHolder, conversation: Conversation, prev: ComposeItem?) { + holder.index.isVisible = false - view.icon.isVisible = prev !is ComposeItem.Recent - view.icon.setImageResource(R.drawable.ic_history_black_24dp) + holder.icon.isVisible = prev !is ComposeItem.Recent + holder.icon.setImageResource(R.drawable.ic_history_black_24dp) - view.avatar.recipients = conversation.recipients + holder.avatar.recipients = conversation.recipients - view.title.text = conversation.getTitle() + holder.title.text = conversation.getTitle() - view.subtitle.isVisible = conversation.recipients.size > 1 && conversation.name.isBlank() - view.subtitle.text = conversation.recipients.joinToString(", ") { recipient -> + holder.subtitle.isVisible = conversation.recipients.size > 1 && conversation.name.isBlank() + holder.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 + holder.numbers.isVisible = conversation.recipients.size == 1 + (holder.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 + private fun bindStarred(holder: QkViewHolder, contact: Contact, prev: ComposeItem?) { + holder.index.isVisible = false - view.icon.isVisible = prev !is ComposeItem.Starred - view.icon.setImageResource(R.drawable.ic_star_black_24dp) + holder.icon.isVisible = prev !is ComposeItem.Starred + holder.icon.setImageResource(R.drawable.ic_star_black_24dp) - view.avatar.recipients = listOf(createRecipient(contact)) + holder.avatar.recipients = listOf(createRecipient(contact)) - view.title.text = contact.name + holder.title.text = contact.name - view.subtitle.isVisible = false + holder.subtitle.isVisible = false - view.numbers.isVisible = true - (view.numbers.adapter as PhoneNumberAdapter).data = contact.numbers + holder.numbers.isVisible = true + (holder.numbers.adapter as PhoneNumberAdapter).data = contact.numbers } - private fun bindGroup(view: View, group: ContactGroup, prev: ComposeItem?) { - view.index.isVisible = false + private fun bindGroup(holder: QkViewHolder, group: ContactGroup, prev: ComposeItem?) { + holder.index.isVisible = false - view.icon.isVisible = prev !is ComposeItem.Group - view.icon.setImageResource(R.drawable.ic_people_black_24dp) + holder.icon.isVisible = prev !is ComposeItem.Group + holder.icon.setImageResource(R.drawable.ic_people_black_24dp) - view.avatar.recipients = group.contacts.map(::createRecipient) + holder.avatar.recipients = group.contacts.map(::createRecipient) - view.title.text = group.title + holder.title.text = group.title - view.subtitle.isVisible = true - view.subtitle.text = group.contacts.joinToString(", ") { it.name } + holder.subtitle.isVisible = true + holder.subtitle.text = group.contacts.joinToString(", ") { it.name } - view.numbers.isVisible = false + holder.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 || + 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 || (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 + holder.icon.isVisible = false - view.avatar.recipients = listOf(createRecipient(contact)) + holder.avatar.recipients = listOf(createRecipient(contact)) - view.title.text = contact.name + holder.title.text = contact.name - view.subtitle.isVisible = false + holder.subtitle.isVisible = false - view.numbers.isVisible = true - (view.numbers.adapter as PhoneNumberAdapter).data = contact.numbers + holder.numbers.isVisible = true + (holder.numbers.adapter as PhoneNumberAdapter).data = contact.numbers } private fun createRecipient(contact: Contact): Recipient { diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/PhoneNumberAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/PhoneNumberAdapter.kt index 565bd1b9f..534f013e9 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/PhoneNumberAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/PhoneNumberAdapter.kt @@ -24,7 +24,7 @@ import com.moez.QKSMS.R import com.moez.QKSMS.common.base.QkAdapter import com.moez.QKSMS.common.base.QkViewHolder import com.moez.QKSMS.model.PhoneNumber -import kotlinx.android.synthetic.main.contact_number_list_item.view.* +import kotlinx.android.synthetic.main.contact_number_list_item.* class PhoneNumberAdapter : QkAdapter() { @@ -36,10 +36,9 @@ class PhoneNumberAdapter : QkAdapter() { override fun onBindViewHolder(holder: QkViewHolder, position: Int) { val number = getItem(position) - val view = holder.containerView - view.address.text = number.address - view.type.text = number.type + holder.address.text = number.address + holder.type.text = number.type } override fun areItemsTheSame(old: PhoneNumber, new: PhoneNumber): Boolean { diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/FileBinder.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/FileBinder.kt index 73d9ab836..ff12a1bdd 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/FileBinder.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/FileBinder.kt @@ -21,9 +21,9 @@ package com.moez.QKSMS.feature.compose.part import android.annotation.SuppressLint import android.content.Context import android.view.Gravity -import android.view.View import android.widget.FrameLayout import com.moez.QKSMS.R +import com.moez.QKSMS.common.base.QkViewHolder import com.moez.QKSMS.common.util.Colors import com.moez.QKSMS.common.util.extensions.resolveThemeColor import com.moez.QKSMS.common.util.extensions.setBackgroundTint @@ -34,7 +34,7 @@ import com.moez.QKSMS.model.MmsPart import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers -import kotlinx.android.synthetic.main.mms_file_list_item.view.* +import kotlinx.android.synthetic.main.mms_file_list_item.* import javax.inject.Inject class FileBinder @Inject constructor(colors: Colors, private val context: Context) : PartBinder() { @@ -47,16 +47,16 @@ class FileBinder @Inject constructor(colors: Colors, private val context: Contex @SuppressLint("CheckResult") override fun bindPart( - view: View, + holder: QkViewHolder, part: MmsPart, message: Message, canGroupWithPrevious: Boolean, canGroupWithNext: Boolean ) { BubbleUtils.getBubble(false, canGroupWithPrevious, canGroupWithNext, message.isMe()) - .let(view.fileBackground::setBackgroundResource) + .let(holder.fileBackground::setBackgroundResource) - view.setOnClickListener { clicks.onNext(part.id) } + holder.containerView.setOnClickListener { clicks.onNext(part.id) } Observable.just(part.getUri()) .map(context.contentResolver::openInputStream) @@ -71,23 +71,23 @@ class FileBinder @Inject constructor(colors: Colors, private val context: Contex } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe { size -> view.size.text = size } + .subscribe { size -> holder.size.text = size } - view.filename.text = part.name + holder.filename.text = part.name - val params = view.fileBackground.layoutParams as FrameLayout.LayoutParams + val params = holder.fileBackground.layoutParams as FrameLayout.LayoutParams if (!message.isMe()) { - view.fileBackground.layoutParams = params.apply { gravity = Gravity.START } - view.fileBackground.setBackgroundTint(theme.theme) - view.icon.setTint(theme.textPrimary) - view.filename.setTextColor(theme.textPrimary) - view.size.setTextColor(theme.textTertiary) + holder.fileBackground.layoutParams = params.apply { gravity = Gravity.START } + holder.fileBackground.setBackgroundTint(theme.theme) + holder.icon.setTint(theme.textPrimary) + holder.filename.setTextColor(theme.textPrimary) + holder.size.setTextColor(theme.textTertiary) } else { - view.fileBackground.layoutParams = params.apply { gravity = Gravity.END } - view.fileBackground.setBackgroundTint(view.context.resolveThemeColor(R.attr.bubbleColor)) - view.icon.setTint(view.context.resolveThemeColor(android.R.attr.textColorSecondary)) - view.filename.setTextColor(view.context.resolveThemeColor(android.R.attr.textColorPrimary)) - view.size.setTextColor(view.context.resolveThemeColor(android.R.attr.textColorTertiary)) + holder.fileBackground.layoutParams = params.apply { gravity = Gravity.END } + 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)) } } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/MediaBinder.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/MediaBinder.kt index dafece1c6..1ac437a5c 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/MediaBinder.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/MediaBinder.kt @@ -19,8 +19,8 @@ package com.moez.QKSMS.feature.compose.part import android.content.Context -import android.view.View import com.moez.QKSMS.R +import com.moez.QKSMS.common.base.QkViewHolder import com.moez.QKSMS.common.util.Colors import com.moez.QKSMS.common.util.extensions.setVisible import com.moez.QKSMS.common.widget.BubbleImageView @@ -29,7 +29,7 @@ import com.moez.QKSMS.extensions.isVideo import com.moez.QKSMS.model.Message import com.moez.QKSMS.model.MmsPart import com.moez.QKSMS.util.GlideApp -import kotlinx.android.synthetic.main.mms_preview_list_item.view.* +import kotlinx.android.synthetic.main.mms_preview_list_item.* import javax.inject.Inject class MediaBinder @Inject constructor(colors: Colors, private val context: Context) : PartBinder() { @@ -40,23 +40,23 @@ class MediaBinder @Inject constructor(colors: Colors, private val context: Conte override fun canBindPart(part: MmsPart) = part.isImage() || part.isVideo() override fun bindPart( - view: View, + holder: QkViewHolder, part: MmsPart, message: Message, canGroupWithPrevious: Boolean, canGroupWithNext: Boolean ) { - view.video.setVisible(part.isVideo()) - view.setOnClickListener { clicks.onNext(part.id) } + holder.video.setVisible(part.isVideo()) + holder.containerView.setOnClickListener { clicks.onNext(part.id) } - view.thumbnail.bubbleStyle = when { + holder.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 else -> BubbleImageView.Style.ONLY } - GlideApp.with(context).load(part.getUri()).fitCenter().into(view.thumbnail) + GlideApp.with(context).load(part.getUri()).fitCenter().into(holder.thumbnail) } } \ No newline at end of file diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/PartBinder.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/PartBinder.kt index fc3fb7fc4..d56f8547d 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/PartBinder.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/PartBinder.kt @@ -18,7 +18,7 @@ */ package com.moez.QKSMS.feature.compose.part -import android.view.View +import com.moez.QKSMS.common.base.QkViewHolder import com.moez.QKSMS.common.util.Colors import com.moez.QKSMS.model.Message import com.moez.QKSMS.model.MmsPart @@ -36,7 +36,7 @@ abstract class PartBinder { abstract fun canBindPart(part: MmsPart): Boolean abstract fun bindPart( - view: View, + holder: QkViewHolder, part: MmsPart, message: Message, canGroupWithPrevious: Boolean, diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/PartsAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/PartsAdapter.kt index 31c67aee5..567f49268 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/PartsAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/PartsAdapter.kt @@ -31,7 +31,7 @@ import com.moez.QKSMS.feature.compose.BubbleUtils.canGroup import com.moez.QKSMS.model.Message import com.moez.QKSMS.model.MmsPart import io.reactivex.Observable -import kotlinx.android.synthetic.main.message_list_item_in.view.* +import kotlinx.android.synthetic.main.message_list_item_in.* import javax.inject.Inject class PartsAdapter @Inject constructor( @@ -54,35 +54,34 @@ class PartsAdapter @Inject constructor( private lateinit var message: Message private var previous: Message? = null private var next: Message? = null - private var messageView: View? = null + private var holder: QkViewHolder? = null private var bodyVisible: Boolean = true - fun setData(message: Message, previous: Message?, next: Message?, messageView: View) { + fun setData(message: Message, previous: Message?, next: Message?, holder: QkViewHolder) { this.message = message this.previous = previous this.next = next - this.messageView = messageView - this.bodyVisible = messageView.body.visibility == View.VISIBLE + this.holder = holder + this.bodyVisible = holder.body.visibility == View.VISIBLE this.data = message.parts.filter { !it.isSmil() && !it.isText() } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkViewHolder { val layout = partBinders.getOrNull(viewType)?.partLayout ?: 0 val view = LayoutInflater.from(parent.context).inflate(layout, parent, false) - messageView?.let(view::forwardTouches) + holder?.containerView?.let(view::forwardTouches) return QkViewHolder(view) } override fun onBindViewHolder(holder: QkViewHolder, position: Int) { val part = data[position] - val view = holder.containerView val canGroupWithPrevious = canGroup(message, previous) || position > 0 val canGroupWithNext = canGroup(message, next) || position < itemCount - 1 || bodyVisible partBinders .firstOrNull { it.canBindPart(part) } - ?.bindPart(view, part, message, canGroupWithPrevious, canGroupWithNext) + ?.bindPart(holder, part, message, canGroupWithPrevious, canGroupWithNext) } override fun getItemViewType(position: Int): Int { diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/VCardBinder.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/VCardBinder.kt index 294f23c79..eebcdaf61 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/VCardBinder.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/VCardBinder.kt @@ -21,9 +21,9 @@ package com.moez.QKSMS.feature.compose.part import android.content.Context import android.util.Log import android.view.Gravity -import android.view.View import android.widget.FrameLayout import com.moez.QKSMS.R +import com.moez.QKSMS.common.base.QkViewHolder import com.moez.QKSMS.common.util.Colors import com.moez.QKSMS.common.util.extensions.resolveThemeColor import com.moez.QKSMS.common.util.extensions.setBackgroundTint @@ -37,7 +37,7 @@ 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.view.* +import kotlinx.android.synthetic.main.mms_vcard_list_item.* import javax.inject.Inject class VCardBinder @Inject constructor(colors: Colors, private val context: Context) : PartBinder() { @@ -48,37 +48,37 @@ class VCardBinder @Inject constructor(colors: Colors, private val context: Conte override fun canBindPart(part: MmsPart) = part.isVCard() override fun bindPart( - view: View, + holder: QkViewHolder, part: MmsPart, message: Message, canGroupWithPrevious: Boolean, canGroupWithNext: Boolean ) { BubbleUtils.getBubble(false, canGroupWithPrevious, canGroupWithNext, message.isMe()) - .let(view.vCardBackground::setBackgroundResource) + .let(holder.vCardBackground::setBackgroundResource) - view.setOnClickListener { clicks.onNext(part.id) } + holder.containerView.setOnClickListener { clicks.onNext(part.id) } Observable.just(part.getUri()) .map(context.contentResolver::openInputStream) .mapNotNull { inputStream -> inputStream.use { Ezvcard.parse(it).first() } } .subscribeOn(Schedulers.computation()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe( { vcard -> view.name?.text = vcard.formattedName.value }, { throwable -> Log.i("VCardBinder.kt", "Name field is null") } ) + .subscribe( { vcard -> holder.name?.text = vcard.formattedName.value }, { throwable -> Log.i("VCardBinder.kt", "Name field is null") } ) - val params = view.vCardBackground.layoutParams as FrameLayout.LayoutParams + val params = holder.vCardBackground.layoutParams as FrameLayout.LayoutParams if (!message.isMe()) { - view.vCardBackground.layoutParams = params.apply { gravity = Gravity.START } - view.vCardBackground.setBackgroundTint(theme.theme) - view.vCardAvatar.setTint(theme.textPrimary) - view.name.setTextColor(theme.textPrimary) - view.label.setTextColor(theme.textTertiary) + holder.vCardBackground.layoutParams = params.apply { gravity = Gravity.START } + holder.vCardBackground.setBackgroundTint(theme.theme) + holder.vCardAvatar.setTint(theme.textPrimary) + holder.name.setTextColor(theme.textPrimary) + holder.label.setTextColor(theme.textTertiary) } else { - view.vCardBackground.layoutParams = params.apply { gravity = Gravity.END } - view.vCardBackground.setBackgroundTint(view.context.resolveThemeColor(R.attr.bubbleColor)) - view.vCardAvatar.setTint(view.context.resolveThemeColor(android.R.attr.textColorSecondary)) - view.name.setTextColor(view.context.resolveThemeColor(android.R.attr.textColorPrimary)) - view.label.setTextColor(view.context.resolveThemeColor(android.R.attr.textColorTertiary)) + holder.vCardBackground.layoutParams = params.apply { gravity = Gravity.END } + 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)) } } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationMediaAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationMediaAdapter.kt index e34063c0f..16565c6d1 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationMediaAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationMediaAdapter.kt @@ -29,6 +29,7 @@ import com.moez.QKSMS.common.util.extensions.setVisible import com.moez.QKSMS.extensions.isVideo import com.moez.QKSMS.model.MmsPart import com.moez.QKSMS.util.GlideApp +import kotlinx.android.synthetic.main.conversation_media_list_item.* import kotlinx.android.synthetic.main.conversation_media_list_item.view.* import javax.inject.Inject @@ -50,14 +51,13 @@ class ConversationMediaAdapter @Inject constructor( override fun onBindViewHolder(holder: QkViewHolder, position: Int) { val part = getItem(position) ?: return - val view = holder.containerView GlideApp.with(context) .load(part.getUri()) .fitCenter() - .into(view.thumbnail) + .into(holder.thumbnail) - view.video.setVisible(part.isVideo()) + holder.video.setVisible(part.isVideo()) } } \ No newline at end of file diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationRecipientAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationRecipientAdapter.kt index 41b6cffa4..f165e5429 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationRecipientAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationRecipientAdapter.kt @@ -29,6 +29,7 @@ import com.moez.QKSMS.common.util.extensions.setVisible import com.moez.QKSMS.model.Recipient import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.Subject +import kotlinx.android.synthetic.main.conversation_recipient_list_item.* import kotlinx.android.synthetic.main.conversation_recipient_list_item.view.* import javax.inject.Inject @@ -64,19 +65,18 @@ class ConversationRecipientAdapter @Inject constructor( override fun onBindViewHolder(holder: QkViewHolder, position: Int) { val recipient = getItem(position) ?: return - val view = holder.containerView - view.avatar.setRecipient(recipient) + holder.avatar.setRecipient(recipient) - view.name.text = recipient.contact?.name ?: recipient.address + holder.name.text = recipient.contact?.name ?: recipient.address - view.address.text = recipient.address - view.address.setVisible(recipient.contact != null) + holder.address.text = recipient.address + holder.address.setVisible(recipient.contact != null) - view.add.setVisible(recipient.contact == null) + holder.add.setVisible(recipient.contact == null) val theme = colors.theme(recipient) - view.theme.setTint(theme.theme) + holder.theme.setTint(theme.theme) } } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/conversations/ConversationsAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/conversations/ConversationsAdapter.kt index 6285346f5..56b5f51e6 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/conversations/ConversationsAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/conversations/ConversationsAdapter.kt @@ -33,6 +33,7 @@ import com.moez.QKSMS.common.util.extensions.resolveThemeColor import com.moez.QKSMS.common.util.extensions.setTint import com.moez.QKSMS.model.Conversation import com.moez.QKSMS.util.PhoneNumberUtils +import kotlinx.android.synthetic.main.conversation_list_item.* import kotlinx.android.synthetic.main.conversation_list_item.view.* import javax.inject.Inject @@ -84,22 +85,21 @@ class ConversationsAdapter @Inject constructor( } } - override fun onBindViewHolder(viewHolder: QkViewHolder, position: Int) { + override fun onBindViewHolder(holder: QkViewHolder, position: Int) { val conversation = getItem(position) ?: return - val view = viewHolder.containerView - view.isActivated = isSelected(conversation.id) + holder.containerView.isActivated = isSelected(conversation.id) - view.avatars.recipients = conversation.recipients - view.title.collapseEnabled = conversation.recipients.size > 1 - view.title.text = conversation.getTitle() - view.date.text = conversation.date.takeIf { it > 0 }?.let(dateFormatter::getConversationTimestamp) - view.snippet.text = when { + holder.avatars.recipients = conversation.recipients + holder.title.collapseEnabled = conversation.recipients.size > 1 + holder.title.text = conversation.getTitle() + holder.date.text = conversation.date.takeIf { it > 0 }?.let(dateFormatter::getConversationTimestamp) + holder.snippet.text = when { conversation.draft.isNotEmpty() -> context.getString(R.string.main_draft, conversation.draft) conversation.me -> context.getString(R.string.main_sender_you, conversation.snippet) else -> conversation.snippet } - view.pinned.isVisible = conversation.pinned + holder.pinned.isVisible = conversation.pinned // If the last message wasn't incoming, then the colour doesn't really matter anyway val lastMessage = conversation.lastMessage @@ -110,7 +110,7 @@ class ConversationsAdapter @Inject constructor( } } - view.unread.setTint(colors.theme(recipient).theme) + holder.unread.setTint(colors.theme(recipient).theme) } override fun getItemId(index: Int): Long { diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/gallery/GalleryPagerAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/gallery/GalleryPagerAdapter.kt index 2569a6c07..f3a4405bc 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/gallery/GalleryPagerAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/gallery/GalleryPagerAdapter.kt @@ -39,8 +39,9 @@ import com.moez.QKSMS.model.MmsPart import com.moez.QKSMS.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.view.* +import kotlinx.android.synthetic.main.gallery_video_page.* import java.util.* import javax.inject.Inject @@ -90,7 +91,6 @@ class GalleryPagerAdapter @Inject constructor(private val context: Context) : Qk override fun onBindViewHolder(holder: QkViewHolder, position: Int) { val part = getItem(position) ?: return - val view = holder.containerView when (getItemViewType(position)) { VIEW_TYPE_IMAGE -> { // We need to explicitly request a gif from glide for animations to work @@ -98,12 +98,12 @@ class GalleryPagerAdapter @Inject constructor(private val context: Context) : Qk ContentType.IMAGE_GIF -> GlideApp.with(context) .asGif() .load(part.getUri()) - .into(view.image) + .into(holder.image) else -> GlideApp.with(context) .asBitmap() .load(part.getUri()) - .into(view.image) + .into(holder.image) } } @@ -111,7 +111,7 @@ class GalleryPagerAdapter @Inject constructor(private val context: Context) : Qk val videoTrackSelectionFactory = AdaptiveTrackSelection.Factory(null) val trackSelector = DefaultTrackSelector(videoTrackSelectionFactory) val exoPlayer = ExoPlayerFactory.newSimpleInstance(context, trackSelector) - view.video.player = exoPlayer + holder.video.player = exoPlayer exoPlayers.add(exoPlayer) val dataSourceFactory = DefaultDataSourceFactory(context, Util.getUserAgent(context, "QKSMS")) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/main/SearchAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/main/SearchAdapter.kt index 6d03af13f..9a964a208 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/main/SearchAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/main/SearchAdapter.kt @@ -33,6 +33,7 @@ import com.moez.QKSMS.common.util.DateFormatter import com.moez.QKSMS.common.util.extensions.setVisible import com.moez.QKSMS.extensions.removeAccents import com.moez.QKSMS.model.SearchResult +import kotlinx.android.synthetic.main.search_list_item.* import kotlinx.android.synthetic.main.search_list_item.view.* import javax.inject.Inject @@ -56,12 +57,11 @@ class SearchAdapter @Inject constructor( } } - override fun onBindViewHolder(viewHolder: QkViewHolder, position: Int) { + override fun onBindViewHolder(holder: QkViewHolder, position: Int) { val previous = data.getOrNull(position - 1) val result = getItem(position) - val view = viewHolder.containerView - view.resultsHeader.setVisible(result.messages > 0 && previous?.messages == 0) + holder.resultsHeader.setVisible(result.messages > 0 && previous?.messages == 0) val query = result.query val title = SpannableString(result.conversation.getTitle()) @@ -71,23 +71,23 @@ class SearchAdapter @Inject constructor( title.setSpan(BackgroundColorSpan(highlightColor), index, index + query.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) index = title.indexOf(query, index + query.length, true) } - view.title.text = title + holder.title.text = title - view.avatars.recipients = result.conversation.recipients + holder.avatars.recipients = result.conversation.recipients when (result.messages == 0) { true -> { - view.date.setVisible(true) - view.date.text = dateFormatter.getConversationTimestamp(result.conversation.date) - view.snippet.text = when (result.conversation.me) { + holder.date.setVisible(true) + holder.date.text = dateFormatter.getConversationTimestamp(result.conversation.date) + holder.snippet.text = when (result.conversation.me) { true -> context.getString(R.string.main_sender_you, result.conversation.snippet) false -> result.conversation.snippet } } false -> { - view.date.setVisible(false) - view.snippet.text = context.getString(R.string.main_message_results, result.messages) + holder.date.setVisible(false) + holder.snippet.text = context.getString(R.string.main_message_results, result.messages) } } } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/scheduled/ScheduledMessageAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/scheduled/ScheduledMessageAdapter.kt index e23c2a687..23c09227c 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/scheduled/ScheduledMessageAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/scheduled/ScheduledMessageAdapter.kt @@ -18,6 +18,7 @@ */ package com.moez.QKSMS.feature.scheduled +import android.content.Context import android.net.Uri import android.view.LayoutInflater import android.view.ViewGroup @@ -34,10 +35,12 @@ import com.moez.QKSMS.repository.ContactRepository import com.moez.QKSMS.util.PhoneNumberUtils import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.Subject +import kotlinx.android.synthetic.main.scheduled_message_list_item.* import kotlinx.android.synthetic.main.scheduled_message_list_item.view.* import javax.inject.Inject class ScheduledMessageAdapter @Inject constructor( + private val context: Context, private val contactRepo: ContactRepository, private val dateFormatter: DateFormatter, private val phoneNumberUtils: PhoneNumberUtils @@ -52,7 +55,7 @@ class ScheduledMessageAdapter @Inject constructor( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkViewHolder { val view = LayoutInflater.from(parent.context).inflate(R.layout.scheduled_message_list_item, parent, false) - view.attachments.adapter = ScheduledMessageAttachmentAdapter() + view.attachments.adapter = ScheduledMessageAttachmentAdapter(context) view.attachments.setRecycledViewPool(imagesViewPool) return QkViewHolder(view).apply { @@ -65,21 +68,20 @@ class ScheduledMessageAdapter @Inject constructor( override fun onBindViewHolder(holder: QkViewHolder, position: Int) { val message = getItem(position) ?: return - val view = holder.containerView // GroupAvatarView only accepts recipients, so map the phone numbers to recipients - view.avatars.recipients = message.recipients.map { address -> Recipient(address = address) } + holder.avatars.recipients = message.recipients.map { address -> Recipient(address = address) } - view.recipients.text = message.recipients.joinToString(",") { address -> + holder.recipients.text = message.recipients.joinToString(",") { address -> contactCache[address]?.name?.takeIf { it.isNotBlank() } ?: address } - view.date.text = dateFormatter.getScheduledTimestamp(message.date) - view.body.text = message.body + holder.date.text = dateFormatter.getScheduledTimestamp(message.date) + holder.body.text = message.body - val adapter = view.attachments.adapter as ScheduledMessageAttachmentAdapter + val adapter = holder.attachments.adapter as ScheduledMessageAttachmentAdapter adapter.data = message.attachments.map(Uri::parse) - view.attachments.isVisible = message.attachments.isNotEmpty() + holder.attachments.isVisible = message.attachments.isNotEmpty() } /** @@ -102,4 +104,4 @@ class ScheduledMessageAdapter @Inject constructor( } -} \ No newline at end of file +} diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/scheduled/ScheduledMessageAttachmentAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/scheduled/ScheduledMessageAttachmentAdapter.kt index 8b4e8838d..558c8af7a 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/scheduled/ScheduledMessageAttachmentAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/scheduled/ScheduledMessageAttachmentAdapter.kt @@ -18,6 +18,7 @@ */ package com.moez.QKSMS.feature.scheduled +import android.content.Context import android.net.Uri import android.view.LayoutInflater import android.view.ViewGroup @@ -26,9 +27,12 @@ import com.moez.QKSMS.common.base.QkAdapter import com.moez.QKSMS.common.base.QkViewHolder import com.moez.QKSMS.util.GlideApp import kotlinx.android.synthetic.main.attachment_image_list_item.view.* +import kotlinx.android.synthetic.main.scheduled_message_image_list_item.* import javax.inject.Inject -class ScheduledMessageAttachmentAdapter @Inject constructor() : QkAdapter() { +class ScheduledMessageAttachmentAdapter @Inject constructor( + private val context: Context +) : QkAdapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkViewHolder { val view = LayoutInflater.from(parent.context).inflate(R.layout.scheduled_message_image_list_item, parent, false) @@ -39,9 +43,8 @@ class ScheduledMessageAttachmentAdapter @Inject constructor() : QkAdapter() override fun onBindViewHolder(holder: QkViewHolder, position: Int) { val attachment = getItem(position) - val view = holder.containerView - GlideApp.with(view).load(attachment).into(view.thumbnail) + GlideApp.with(context).load(attachment).into(holder.thumbnail) } -} \ No newline at end of file +} diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/themepicker/ThemeAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/themepicker/ThemeAdapter.kt index 7489a34ad..cf99614e7 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/themepicker/ThemeAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/themepicker/ThemeAdapter.kt @@ -36,6 +36,7 @@ import com.moez.QKSMS.common.util.extensions.setVisible 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 @@ -70,7 +71,6 @@ class ThemeAdapter @Inject constructor( override fun onBindViewHolder(holder: QkViewHolder, position: Int) { val palette = getItem(position) - val view = holder.containerView val screenWidth = Resources.getSystem().displayMetrics.widthPixels val minPadding = (16 * 6).dpToPx(context) @@ -81,12 +81,12 @@ class ThemeAdapter @Inject constructor( } val swatchPadding = (screenWidth - size * 5) / 12 - view.palette.removeAllViews() - view.palette.setPadding(swatchPadding, swatchPadding, swatchPadding, swatchPadding) + holder.palette.removeAllViews() + holder.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, view.palette, false).apply { + LayoutInflater.from(context).inflate(R.layout.theme_list_item, holder.palette, false).apply { // Send clicks to the selected subject setOnClickListener { colorSelected.onNext(color) } @@ -107,7 +107,7 @@ class ThemeAdapter @Inject constructor( } } } - .forEach { theme -> view.palette.addView(theme) } + .forEach { theme -> holder.palette.addView(theme) } } } \ No newline at end of file -- GitLab From a33b45d091a0f71018e117a080348d7b72b008dc Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Wed, 4 Dec 2019 00:31:38 -0500 Subject: [PATCH 061/109] Upgrade gradle, force jdk 8 --- build.gradle | 18 ++++++++++++++++-- gradle/wrapper/gradle-wrapper.properties | 4 ++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 53d635bdb..fb7aa4500 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ buildscript { ext.exoplayer_version = "2.8.1" ext.glide_version = "4.8.0" ext.junit_version = '4.12' - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.3.60' ext.lifecycle_version = '2.2.0' ext.material_version = '1.1.0' ext.mockito_version = '2.18.3' @@ -42,7 +42,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.4.2' + classpath 'com.android.tools.build:gradle:3.5.2' classpath 'com.google.gms:google-services:4.2.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'io.fabric.tools:gradle:1.29.0' @@ -76,3 +76,17 @@ subprojects { task clean(type: Delete) { delete rootProject.buildDir } + +subprojects { + afterEvaluate { + if (project.hasProperty('kapt')) { + kapt { + // we expect this closure to run over a org.jetbrains.kotlin.gradle.plugin.KaptExtension + javacOptions { + option("-source", "8") + option("-target", "8") + } + } + } + } +} diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6914a81f7..4b9cec46b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Jun 06 19:27:58 EDT 2019 +#Tue Dec 03 23:30:52 EST 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip -- GitLab From f3735b9871032d0443161eb24ab59df2929c5473 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Wed, 4 Dec 2019 00:32:44 -0500 Subject: [PATCH 062/109] Add config.yml --- .circleci/config.yml | 80 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 .circleci/config.yml diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 000000000..a5ed0ee26 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,80 @@ +version: 2 + +jobs: + build: + working_directory: ~/code + docker: + - image: circleci/android:api-29 + steps: + - checkout + - restore_cache: + key: jars-{{ checksum "build.gradle" }}-{{ checksum "presentation/build.gradle" }}-{{ checksum "data/build.gradle" }}-{{ checksum "domain/build.gradle" }} + - run: + name: Download dependencies + command: ./gradlew androidDependencies + - save_cache: + paths: + - ~/.gradle + key: jars-{{ checksum "build.gradle" }}-{{ checksum "presentation/build.gradle" }}-{{ checksum "data/build.gradle" }}-{{ checksum "domain/build.gradle" }} + - run: + name: Gradle build + command: ./gradlew :presentation:assembleWithAnalyticsRelease :presentation:bundleWithAnalyticsRelease assembleAndroidTest -PtestCoverageEnabled='true' + - store_artifacts: + path: presentation/build/outputs + destination: builds + - persist_to_workspace: + root: presentation/build/outputs + paths: . + + test: + working_directory: ~/code + docker: + - image: circleci/android:api-29 + steps: + - checkout + - restore_cache: + key: jars-{{ checksum "build.gradle" }}-{{ checksum "presentation/build.gradle" }}-{{ checksum "data/build.gradle" }}-{{ checksum "domain/build.gradle" }} + - run: + name: Download dependencies + command: ./gradlew androidDependencies + - save_cache: + paths: + - ~/.gradle + key: jars-{{ checksum "build.gradle" }}-{{ checksum "presentation/build.gradle" }}-{{ checksum "data/build.gradle" }}-{{ checksum "domain/build.gradle" }} + - store_test_results: + path: presentation/build/test-results + + publish-github-release: + docker: + - image: cibuilds/github:0.10 + steps: + - attach_workspace: + at: presentation/build/outputs + - run: + name: "Publish Release on GitHub" + command: | + VERSION=$(my-binary --version) + ghr -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} -delete ${VERSION} presentation/build/outputs/ + +workflows: + version: 2 + main: + jobs: + - build: + filters: + tags: + only: /^v.*/ + - test: + requires: + - build + filters: + tags: + only: /^v.*/ + - deploy: + requires: + - test + filters: + branches: + ignore: /.*/ + tags: + only: /^v.*/ -- GitLab From 2c7c3c62588e4e771cdf6ca6f48ed87ba2ce7ce2 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Wed, 4 Dec 2019 01:26:40 -0500 Subject: [PATCH 063/109] Add secret files --- .circleci/config.yml | 5 +++++ secrets.tar.enc | Bin 8208 -> 11296 bytes 2 files changed, 5 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index a5ed0ee26..1d6130b67 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -16,6 +16,11 @@ jobs: paths: - ~/.gradle key: jars-{{ checksum "build.gradle" }}-{{ checksum "presentation/build.gradle" }}-{{ checksum "data/build.gradle" }}-{{ checksum "domain/build.gradle" }} + - run: + name: Decrypt and unzip secrets + command: | + openssl aes-256-cbc -d -in secrets.tar.enc -k ${SECRETS_KEY} -iv ${SECRETS_IV} >> secrets.tar + tar xvf secrets.tar - run: name: Gradle build command: ./gradlew :presentation:assembleWithAnalyticsRelease :presentation:bundleWithAnalyticsRelease assembleAndroidTest -PtestCoverageEnabled='true' diff --git a/secrets.tar.enc b/secrets.tar.enc index 35ec7e2a41fd55d371889a7165714576c7fb4506..a96512c7e039c7a011ba5a1823cedb9a235a7a9a 100644 GIT binary patch literal 11296 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 a^HVNYDhZWbf3Qz)gJp!Li^IJ-{Z|0`>?nBv literal 8208 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 -- GitLab From 6231f9251f0377857b763ee2ea8a20653a4ebe12 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Wed, 4 Dec 2019 01:40:44 -0500 Subject: [PATCH 064/109] Explicitly use sha1 --- .circleci/config.yml | 2 +- secrets.tar.enc | Bin 11296 -> 11296 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 1d6130b67..0709bbc92 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -19,7 +19,7 @@ jobs: - run: name: Decrypt and unzip secrets command: | - openssl aes-256-cbc -d -in secrets.tar.enc -k ${SECRETS_KEY} -iv ${SECRETS_IV} >> secrets.tar + openssl aes-256-cbc -d -in secrets.tar.enc -md sha1 -k ${SECRETS_KEY} -iv ${SECRETS_IV} >> secrets.tar tar xvf secrets.tar - run: name: Gradle build diff --git a/secrets.tar.enc b/secrets.tar.enc index a96512c7e039c7a011ba5a1823cedb9a235a7a9a..4291a913328f7cbd5af4f09fa13a338e917eb168 100644 GIT binary patch literal 11296 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 z zO+J=p24MsL)2o9ObZq}_a{BNsJ2Kzf?a-@tEG5Sz)lbwsyJv0mLTYZ^d8OQPgX1zH zV#~Bsm&w^pby|Lg&sL_XbTad|%HRLhbI%-CDb;3)QkfALIrY179mi+;`+WBNmS@wZ zJzb^ObNGYO^xuD*i@xsS-TdcA@vXxm53BhyIF`*5|EAd_dCSO_^R#$=)9gLbTm9bk z9C>s)!9c2ev)^U+Uxtg-CNBE+y)CiK`-$Y_4V$y1azmNxg}%*rZZ9v}%sWfW+%({@ z`@H$BqRT4co$uyd4KUs(rFZZ0zuz|B=B4X=eOd6XtpD@D1x2=1vI&=iFJ0JWBR)B% zmupgz#G#6bEB{;+<~p(H{)QLH7tdzue$`D+5e_y#+FrxWH2wdqOV~kM+&y=9 z^`i3Qa+WL)j%8OiIu{6qsQOtIE}pgKu>JRc45j9gYU|d%o6qszXV3JvN1s;in*H~c z_lh~|_ovLW7x`?k>BFnYy!56g1$I%2-OLWTlTTaz{4bWc$LsDtgX(nicmM3aTvL5y z>ypyl$fEfC=KC)mY#uAhLsumqxhA+sd`guPW9CDHne0tLM?MN)=RbI5<`TP8E^~L5 zO`Ba)@T+gv^oFQE9%im~MN?tC^y6XuW8ZdL~> zob)`mqyE|{rL%vH{Bq}8tokmp{oum*>gMVB{gtxwHE(K%&tt#y=xNLBj+qNnjw?K< zuc--q-uy_=Jo|p2qUrCg5098Un3Q=l=H<2^1G`01dH-gq&VRR|xjSe1Ih$sUm!~sa zEc_d44@4~d_WjK4U;A5((!3Z>hw-noJ2tVhP_p*%1g=M&fpR`>(H^tf%@;p<`9|sq zgIKoa;lQ+4AF8kH+*thYUwUULKl_B+Jce`jiG0vlcbqBkPXBd*k7rDBvcIraZ@zmV zAwydKz%OTsUsYuEgQudVWF-rYMLm-E7_Gq=vS z`l!&K-eT3^ImhwgyHE2{O=R;*oew>FWz+ntcv*Iw6z}XRaqhUI^%bFM4rSZoZ|r`U zI(u{E)N?^bbwU%lq$hm1d6n~%+@z|Vv-2LMY8o6_!l}2=WY$Nu8>T82S2Ol*`_27Q z@5-cUo=1cu57i&g*~y-DXK6ul(u&YMpQ8=;$^T-hnCWHZJt=X*k+{e1DM_ajqXJ-i<|GfCX-xqK5r>_3Bsefqio_OXQxVX|-K7krWS4A+eh znsuNgb1K7xEt{sdtWgzO@Y29Y$0CsX$NQ@x@hAU3YY;E0TlVlc(^hkpeh8}1XN_^1_{rc{ zl85&qwKVMfpO! zDvJuYJm$hv2xN%c?hTNNmxM}6B zX$SA-vczuPwB|$5#QO`>mj0BU-*oYpaND2tbK)b4uW;OQI*}u=;-OKunulW7t(^JO zSHIxzuCDJZac7Wez4a*X-lO0!iP=w@Sqy1Y`aMlm6?kV|)8E6)KN=}8C7K2NRwydy?!%7y8F z!cD#e|9{oMF@dxHkLjtz`s*Hk7fvu_mK|d-4d<pbPpas5-bRQrw8GljUGS!Owloe#Ob!O}jrGbBIy_F84W6E6-W z_NXuXb-MJ{B7@q?_J3?XUez_q6))VAy=5swIoEgZV^z=Oy?Z2An=K1wV)CEJ!r7Vh z=xlhzqhFFYH-}$zebybs%ek_1P3vi{cUu?^O@A9TNo}`)o!~mBa-VpyMKeyd{aEKT zGxY?|j#5?5e=K=kEGMmwDZB4pGUxxMzH+?%YC5K{ zST5I?-kquwTJ71P!nk&_#yTs9yPu=~Gqf}?{!{%_u;p9UvH6pQPJU7C>Z;uK_@$g^ z9shjmPHTmpozH%!-JCeXCV0=&gVrW}#TWcqIsY(ySt_&sS?L0woMku4-$(^%uh>(1 z_G7%crk%ETznr18YDu>8ME~9c+v2~!7B5W?eX=>~;`Tk}k2w-&>@oMq&#$%LrSU)S z@w63Hdwh?66`H*xuKR@cywIIbS8jf_Xs=P;%k8$BM+}asb*-M(n=Es#G@`Vi;mG!z zU--2Db#JMecZTr?gS}|V-w)|O7?XDt?s?cQA+ScOT!ZCLY|*E*1APsLzdTcw3JlkK zm%Kk$q};0J(n~gdr*$$*=Pvv3{PCk-W}UhDY#eK6JyN-#cYI5vrL7^arYpy#l-O9U zJF6qu*R^fxjX$}bOJq;npY9_%lO*|S9mN#(Z(RDNrS-f56QfFrhs@dhHlIIQ!l}#B z_Mdjq{xhB7*xyN?#e9G4IXdy|)i0lY3hwc{{4?J&KYhMTec$RWJZZISFR+C>@YV24 z)GV2>L8e``^7i?q(vznja#v)3{z~rVef=P9-P^nBtq-{c?0RYP5^}+xE literal 11296 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 a^HVNYDhZWbf3Qz)gJp!Li^IJ-{Z|0`>?nBv -- GitLab From 5a1d611ccf5c56955428a26c8670d6c4ef016ba8 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Wed, 4 Dec 2019 01:49:43 -0500 Subject: [PATCH 065/109] Remove tests from build stage --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 0709bbc92..84a4fd99d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -23,7 +23,7 @@ jobs: tar xvf secrets.tar - run: name: Gradle build - command: ./gradlew :presentation:assembleWithAnalyticsRelease :presentation:bundleWithAnalyticsRelease assembleAndroidTest -PtestCoverageEnabled='true' + command: ./gradlew :presentation:assembleWithAnalyticsRelease :presentation:bundleWithAnalyticsRelease - store_artifacts: path: presentation/build/outputs destination: builds -- GitLab From 90a728a86b0b2170624cc1767e957393445358be Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Wed, 4 Dec 2019 01:55:25 -0500 Subject: [PATCH 066/109] Deploy test --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 84a4fd99d..e24bb4fc1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -49,7 +49,7 @@ jobs: - store_test_results: path: presentation/build/test-results - publish-github-release: + deploy: docker: - image: cibuilds/github:0.10 steps: -- GitLab From c723918db9f3944126483c46d53af4375b3d150e Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Wed, 4 Dec 2019 02:00:09 -0500 Subject: [PATCH 067/109] Store apk and bundle --- .circleci/config.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e24bb4fc1..e9de94a8c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -25,8 +25,11 @@ jobs: name: Gradle build command: ./gradlew :presentation:assembleWithAnalyticsRelease :presentation:bundleWithAnalyticsRelease - store_artifacts: - path: presentation/build/outputs + path: presentation/build/outputs/apk destination: builds + - store_artifacts: + path: presentation/build/outputs/bundle + destination: builds - persist_to_workspace: root: presentation/build/outputs paths: . -- GitLab From 40e5e4e60d57447c192c1924bfc12a40634d3fcd Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Wed, 4 Dec 2019 02:01:55 -0500 Subject: [PATCH 068/109] Spaces --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e9de94a8c..5a7fc68b7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -28,8 +28,8 @@ jobs: path: presentation/build/outputs/apk destination: builds - store_artifacts: - path: presentation/build/outputs/bundle - destination: builds + path: presentation/build/outputs/bundle + destination: builds - persist_to_workspace: root: presentation/build/outputs paths: . -- GitLab From 20a9d08661b35aaf60da3cfdcfc4b552fb072943 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Wed, 4 Dec 2019 02:11:31 -0500 Subject: [PATCH 069/109] Include version in file name --- presentation/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/presentation/build.gradle b/presentation/build.gradle index f9f19e07e..bcd8ef6bd 100644 --- a/presentation/build.gradle +++ b/presentation/build.gradle @@ -33,6 +33,7 @@ android { targetSdkVersion 29 versionCode 2209 versionName "3.7.10" + setProperty("archivesBaseName", "QKSMS-v${versionName}") testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" setProperty("archivesBaseName", "QKSMS-v${versionName}") -- GitLab From 635ca72a1510bb63c29d078cbdca647963c33a81 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Wed, 4 Dec 2019 02:21:30 -0500 Subject: [PATCH 070/109] =?UTF-8?q?Don=E2=80=99t=20worry=20about=20deletio?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .circleci/config.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 5a7fc68b7..c17427667 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -60,9 +60,7 @@ jobs: at: presentation/build/outputs - run: name: "Publish Release on GitHub" - command: | - VERSION=$(my-binary --version) - ghr -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} -delete ${VERSION} presentation/build/outputs/ + command: ghr -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} presentation/build/outputs/ workflows: version: 2 -- GitLab From 2697dbbe5e32ce82755ea95aacc2d70d7ee0529a Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Wed, 4 Dec 2019 02:28:58 -0500 Subject: [PATCH 071/109] Correct deletion --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index c17427667..c2e5ca073 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -60,7 +60,7 @@ jobs: at: presentation/build/outputs - run: name: "Publish Release on GitHub" - command: ghr -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} presentation/build/outputs/ + command: ghr -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} -delete ${CIRCLE_TAG} presentation/build/outputs/ workflows: version: 2 -- GitLab From 9a8ea48a021205d363a7039f400508e9e0540fde Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Sun, 29 Dec 2019 22:47:52 -0500 Subject: [PATCH 072/109] Another try --- .circleci/config.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index c2e5ca073..d524fe69b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -24,12 +24,11 @@ jobs: - run: name: Gradle build command: ./gradlew :presentation:assembleWithAnalyticsRelease :presentation:bundleWithAnalyticsRelease + - run: + name: Flatten outputs + command: find presentation/build/outputs -mindepth 2 -type f -exec mv -i '{}' presentation/build/outputs/ ';' - store_artifacts: - path: presentation/build/outputs/apk - destination: builds - - store_artifacts: - path: presentation/build/outputs/bundle - destination: builds + path: presentation/build/outputs - persist_to_workspace: root: presentation/build/outputs paths: . @@ -60,7 +59,7 @@ jobs: at: presentation/build/outputs - run: name: "Publish Release on GitHub" - command: ghr -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} -delete ${CIRCLE_TAG} presentation/build/outputs/ + command: ghr -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} ${CIRCLE_TAG} presentation/build/outputs/ workflows: version: 2 -- GitLab From 70dac5c994f699da7b21ab70fe0351caa8c6f5ef Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Wed, 1 Jan 2020 02:02:22 -0500 Subject: [PATCH 073/109] Use broader contacts intent --- .../src/main/java/com/moez/QKSMS/common/Navigator.kt | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/presentation/src/main/java/com/moez/QKSMS/common/Navigator.kt b/presentation/src/main/java/com/moez/QKSMS/common/Navigator.kt index 3855b5fd7..0140fa58a 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/Navigator.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/Navigator.kt @@ -222,14 +222,9 @@ class Navigator @Inject constructor( } fun addContact(address: String) { - val uri = Uri.parse("tel: $address") - var intent = Intent(ContactsContract.Intents.SHOW_OR_CREATE_CONTACT, uri) - - if (intent.resolveActivity(context.packageManager) == null) { - intent = Intent(Intent.ACTION_INSERT) - .setType(ContactsContract.Contacts.CONTENT_TYPE) - .putExtra(ContactsContract.Intents.Insert.PHONE, address) - } + val intent = Intent(Intent.ACTION_INSERT) + .setType(ContactsContract.Contacts.CONTENT_TYPE) + .putExtra(ContactsContract.Intents.Insert.PHONE, address) startActivityExternal(intent) } -- GitLab From 6189453b5d1fe843ec96b3c4a523503504b0e2ec Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Wed, 1 Jan 2020 02:11:15 -0500 Subject: [PATCH 074/109] Migrate badge --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index a795fb5f7..e8aa5ec4a 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,6 @@ Message is an open source replacement to the stock messaging app on Android. Message is forked from [QKSMS](https://github.com/moezbhatti/qksms) -## Authors - [Authors](https://gitlab.e.foundation/e/apps/Message/-/blob/master/AUTHORS) ## Release Notes -- GitLab From 816118fcc45d7bb3ddf42b18a069f165793a13b6 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Wed, 1 Jan 2020 15:49:47 -0500 Subject: [PATCH 075/109] Make sure we have contacts permission before trying to sync --- .../java/com/moez/QKSMS/feature/main/MainViewModel.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainViewModel.kt b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainViewModel.kt index e00a71592..031f3fdcd 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainViewModel.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainViewModel.kt @@ -107,10 +107,12 @@ class MainViewModel @Inject constructor( } // Sync contacts when we detect a change - disposables += contactAddedListener.listen() - .debounce(1, TimeUnit.SECONDS) - .subscribeOn(Schedulers.io()) - .subscribe { syncContacts.execute(Unit) } + if (permissionManager.hasContacts()) { + disposables += contactAddedListener.listen() + .debounce(1, TimeUnit.SECONDS) + .subscribeOn(Schedulers.io()) + .subscribe { syncContacts.execute(Unit) } + } markAllSeen.execute(Unit) } -- GitLab From 6b88d30ba0eb453a72374f6042ae3d680e6c9456 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Wed, 1 Jan 2020 19:30:48 -0500 Subject: [PATCH 076/109] =?UTF-8?q?Fix=20default=20sms=20=E2=80=9CCHANGE?= =?UTF-8?q?=E2=80=9D=20button=20not=20working?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #1548 --- .../main/java/com/moez/QKSMS/feature/main/MainActivity.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainActivity.kt b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainActivity.kt index c8339c21b..d2ca475a1 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainActivity.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainActivity.kt @@ -34,6 +34,7 @@ import androidx.appcompat.app.ActionBarDrawerToggle import androidx.core.app.ActivityCompat import androidx.core.view.GravityCompat import androidx.core.view.isVisible +import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProviders import androidx.recyclerview.widget.ItemTouchHelper @@ -129,7 +130,9 @@ class MainActivity : QkThemedActivity(), MainView { onNewIntentIntent.onNext(intent) (snackbar as? ViewStub)?.setOnInflateListener { _, _ -> - snackbarButton.clicks().autoDisposable(scope()).subscribe(snackbarButtonIntent) + snackbarButton.clicks() + .autoDisposable(scope(Lifecycle.Event.ON_DESTROY)) + .subscribe(snackbarButtonIntent) } (syncing as? ViewStub)?.setOnInflateListener { _, _ -> -- GitLab From 37b4c39b32f4084adcb2ccad77515cacbb4497bc Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Wed, 1 Jan 2020 20:03:46 -0500 Subject: [PATCH 077/109] Increase max signature length Closes #1539 --- presentation/src/main/res/layout/field_dialog.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/presentation/src/main/res/layout/field_dialog.xml b/presentation/src/main/res/layout/field_dialog.xml index 5170aeb3f..b0fec2b93 100644 --- a/presentation/src/main/res/layout/field_dialog.xml +++ b/presentation/src/main/res/layout/field_dialog.xml @@ -25,7 +25,7 @@ android:layout_height="wrap_content" android:background="@null" android:lines="1" - android:maxLength="30" + android:maxLength="64" android:padding="24dp" android:singleLine="true" android:textStyle="bold" -- GitLab From e6d7b3e65eada0c281df0dd8900dc97fe6e49563 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Wed, 1 Jan 2020 20:48:29 -0500 Subject: [PATCH 078/109] Fix 24h timestamps when language is Japanese Closes #1547 --- .../main/java/com/moez/QKSMS/common/util/DateFormatter.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/presentation/src/main/java/com/moez/QKSMS/common/util/DateFormatter.kt b/presentation/src/main/java/com/moez/QKSMS/common/util/DateFormatter.kt index 00974bbc3..d4628c58f 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/util/DateFormatter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/util/DateFormatter.kt @@ -39,7 +39,10 @@ class DateFormatter @Inject constructor(val context: Context) { var formattedPattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), pattern) if (DateFormat.is24HourFormat(context)) { - formattedPattern = formattedPattern.replace("h", "HH").replace(" a".toRegex(), "") + formattedPattern = formattedPattern + .replace("h", "HH") + .replace("K", "HH") + .replace(" a".toRegex(), "") } return SimpleDateFormat(formattedPattern, Locale.getDefault()) -- GitLab From 47cd0d13f9afb70964232099cbc1d84dcb568aeb Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Tue, 7 Jan 2020 22:53:44 -0500 Subject: [PATCH 079/109] =?UTF-8?q?Don=E2=80=99t=20use=20config=20override?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #1526 --- .../main/java/com/android/mms/transaction/DownloadManager.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/android-smsmms/src/main/java/com/android/mms/transaction/DownloadManager.java b/android-smsmms/src/main/java/com/android/mms/transaction/DownloadManager.java index 5ac48647f..698817ca6 100755 --- a/android-smsmms/src/main/java/com/android/mms/transaction/DownloadManager.java +++ b/android-smsmms/src/main/java/com/android/mms/transaction/DownloadManager.java @@ -80,8 +80,6 @@ public class DownloadManager { String httpParams = MmsConfig.getHttpParams(); if (!TextUtils.isEmpty(httpParams)) { configOverrides.putString(SmsManager.MMS_CONFIG_HTTP_PARAMS, httpParams); - } else { - configOverrides = smsManager.getCarrierConfigValues(); } grantUriPermission(context, contentUri); -- GitLab From eef633f259fbaf8d99bf877846ce1f5f2d8910a7 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Wed, 8 Jan 2020 18:59:46 -0500 Subject: [PATCH 080/109] Improve MMS attachment quality Fixes #1551 --- .../util/extensions/GlobalExtensions.kt | 5 + data/build.gradle | 1 + .../QKSMS/repository/ImageRepositoryImpl.kt | 86 ------------- .../QKSMS/repository/MessageRepositoryImpl.kt | 115 +++++++++++++----- .../com/moez/QKSMS/util/GlideAppModule.kt | 1 + .../java/com/moez/QKSMS/util/ImageUtils.kt | 80 +++--------- .../moez/QKSMS/repository/ImageRepository.kt | 28 ----- .../java/com/moez/QKSMS/util/Preferences.kt | 2 +- .../com/moez/QKSMS/injection/AppModule.kt | 5 - presentation/src/main/res/values/strings.xml | 4 +- 10 files changed, 113 insertions(+), 214 deletions(-) create mode 100644 common/src/main/java/com/moez/QKSMS/common/util/extensions/GlobalExtensions.kt delete mode 100644 data/src/main/java/com/moez/QKSMS/repository/ImageRepositoryImpl.kt delete mode 100644 domain/src/main/java/com/moez/QKSMS/repository/ImageRepository.kt diff --git a/common/src/main/java/com/moez/QKSMS/common/util/extensions/GlobalExtensions.kt b/common/src/main/java/com/moez/QKSMS/common/util/extensions/GlobalExtensions.kt new file mode 100644 index 000000000..69b16be1d --- /dev/null +++ b/common/src/main/java/com/moez/QKSMS/common/util/extensions/GlobalExtensions.kt @@ -0,0 +1,5 @@ +package com.moez.QKSMS.common.util.extensions + +fun now(): Long { + return System.currentTimeMillis() +} diff --git a/data/build.gradle b/data/build.gradle index a78666df9..c66279856 100644 --- a/data/build.gradle +++ b/data/build.gradle @@ -57,6 +57,7 @@ dependencies { implementation "androidx.exifinterface:exifinterface:$androidx_exifinterface_version" // glide + implementation "com.github.bumptech.glide:gifencoder-integration:$glide_version" implementation "com.github.bumptech.glide:glide:$glide_version" kapt "com.github.bumptech.glide:compiler:$glide_version" diff --git a/data/src/main/java/com/moez/QKSMS/repository/ImageRepositoryImpl.kt b/data/src/main/java/com/moez/QKSMS/repository/ImageRepositoryImpl.kt deleted file mode 100644 index a4aee3642..000000000 --- a/data/src/main/java/com/moez/QKSMS/repository/ImageRepositoryImpl.kt +++ /dev/null @@ -1,86 +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.repository - -import android.content.Context -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.graphics.Matrix -import android.net.Uri -import androidx.exifinterface.media.ExifInterface -import javax.inject.Inject - -class ImageRepositoryImpl @Inject constructor(private val context: Context) : ImageRepository { - - override fun loadImage(uri: Uri, width: Int, height: Int): Bitmap? { - val orientation = context.contentResolver.openInputStream(uri)?.use(::ExifInterface) - ?.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL) - val rotated = orientation == ExifInterface.ORIENTATION_ROTATE_90 - || orientation == ExifInterface.ORIENTATION_ROTATE_270 - - // Determine the dimensions - val dimensionsOptions = BitmapFactory.Options().apply { inJustDecodeBounds = true } - BitmapFactory.decodeStream(context.contentResolver.openInputStream(uri), null, dimensionsOptions) - val srcWidth = if (rotated) dimensionsOptions.outHeight else dimensionsOptions.outWidth - val srcHeight = if (rotated) dimensionsOptions.outWidth else dimensionsOptions.outHeight - - // If we get the dimensions and they don't exceed the max size, we don't need to scale - val inputStream = context.contentResolver.openInputStream(uri) - val bitmap = if ((width == 0 || srcWidth < width) && (height == 0 || srcHeight < height)) { - BitmapFactory.decodeStream(inputStream) - } else { - val widthScaleFactor = width.toDouble() / srcWidth - val heightScaleFactor = height.toDouble() / srcHeight - val options = when { - widthScaleFactor > heightScaleFactor -> BitmapFactory.Options().apply { - inScaled = true - inSampleSize = 4 - inDensity = srcHeight - inTargetDensity = height * inSampleSize - } - - else -> BitmapFactory.Options().apply { - inScaled = true - inSampleSize = 4 - inDensity = srcWidth - inTargetDensity = width * inSampleSize - } - } - BitmapFactory.decodeStream(inputStream, null, options) ?: return null - } - - return when (orientation) { - ExifInterface.ORIENTATION_ROTATE_90 -> rotateBitmap(bitmap, 90f) - ExifInterface.ORIENTATION_ROTATE_180 -> rotateBitmap(bitmap, 180f) - ExifInterface.ORIENTATION_ROTATE_270 -> rotateBitmap(bitmap, 270f) - else -> bitmap - } - } - - private fun rotateBitmap(bitmap: Bitmap, degree: Float): Bitmap { - val w = bitmap.width - val h = bitmap.height - - val mtx = Matrix() - mtx.postRotate(degree) - - return Bitmap.createBitmap(bitmap, 0, 0, w, h, mtx, true) - } - -} diff --git a/data/src/main/java/com/moez/QKSMS/repository/MessageRepositoryImpl.kt b/data/src/main/java/com/moez/QKSMS/repository/MessageRepositoryImpl.kt index af81e68af..ca45deae9 100644 --- a/data/src/main/java/com/moez/QKSMS/repository/MessageRepositoryImpl.kt +++ b/data/src/main/java/com/moez/QKSMS/repository/MessageRepositoryImpl.kt @@ -24,6 +24,7 @@ import android.content.ContentUris import android.content.ContentValues import android.content.Context import android.content.Intent +import android.graphics.BitmapFactory import android.media.MediaScannerConnection import android.os.Build import android.os.Environment @@ -40,6 +41,7 @@ import com.google.android.mms.pdu_alt.PduPersister import com.klinker.android.send_message.SmsManagerFactory import com.klinker.android.send_message.StripAccents import com.klinker.android.send_message.Transaction +import com.moez.QKSMS.common.util.extensions.now import com.moez.QKSMS.compat.TelephonyCompat import com.moez.QKSMS.extensions.anyOf import com.moez.QKSMS.manager.ActiveConversationManager @@ -66,12 +68,12 @@ import java.io.FileOutputStream import java.io.IOException import javax.inject.Inject import javax.inject.Singleton +import kotlin.math.sqrt @Singleton class MessageRepositoryImpl @Inject constructor( private val activeConversationManager: ActiveConversationManager, private val context: Context, - private val imageRepository: ImageRepository, private val messageIds: KeyManager, private val phoneNumberUtils: PhoneNumberUtils, private val prefs: Preferences, @@ -329,49 +331,98 @@ class MessageRepositoryImpl @Inject constructor( alarmManager.setExact(AlarmManager.RTC_WAKEUP, sendTime, intent) } } else { // No delay - val message = insertSentSms(subId, threadId, addresses.first(), strippedBody, System.currentTimeMillis()) + val message = insertSentSms(subId, threadId, addresses.first(), strippedBody, now()) sendSms(message) } } else { // MMS val parts = arrayListOf() - if (signedBody.isNotBlank()) { - parts += MMSPart("text", ContentType.TEXT_PLAIN, signedBody.toByteArray()) - } + val maxWidth = smsManager.carrierConfigValues.getInt(SmsManager.MMS_CONFIG_MAX_IMAGE_WIDTH) + .takeIf { prefs.mmsSize.get() == -1 } ?: Int.MAX_VALUE - val smsManager = subId.takeIf { it != -1 } - ?.let(SmsManagerFactory::createSmsManager) - ?: SmsManager.getDefault() - val width = smsManager.carrierConfigValues.getInt(SmsManager.MMS_CONFIG_MAX_IMAGE_WIDTH) - val height = smsManager.carrierConfigValues.getInt(SmsManager.MMS_CONFIG_MAX_IMAGE_HEIGHT) + val maxHeight = smsManager.carrierConfigValues.getInt(SmsManager.MMS_CONFIG_MAX_IMAGE_HEIGHT) + .takeIf { prefs.mmsSize.get() == -1 } ?: Int.MAX_VALUE - // Add the GIFs as attachments - parts += attachments - .mapNotNull { attachment -> attachment as? Attachment.Image } - .filter { attachment -> attachment.isGif(context) } - .mapNotNull { attachment -> attachment.getUri() } - .map { uri -> ImageUtils.compressGif(context, uri, prefs.mmsSize.get() * 1024) } - .map { bitmap -> MMSPart("image", ContentType.IMAGE_GIF, bitmap) } + var remainingBytes = when (prefs.mmsSize.get()) { + -1 -> smsManager.carrierConfigValues.getInt(SmsManager.MMS_CONFIG_MAX_MESSAGE_SIZE) + 0 -> Int.MAX_VALUE + else -> prefs.mmsSize.get() * 1024 + } * 0.9 // Ugly, but buys us a bit of wiggle room - // Compress the images and add them as attachments - var totalImageBytes = 0 - parts += attachments - .mapNotNull { attachment -> attachment as? Attachment.Image } - .filter { attachment -> !attachment.isGif(context) } - .mapNotNull { attachment -> attachment.getUri() } - .mapNotNull { uri -> tryOrNull { imageRepository.loadImage(uri, width, height) } } - .also { totalImageBytes = it.sumBy { it.allocationByteCount } } - .map { bitmap -> - val byteRatio = bitmap.allocationByteCount / totalImageBytes.toFloat() - ImageUtils.compressBitmap(bitmap, (prefs.mmsSize.get() * 1024 * byteRatio).toInt()) - } - .map { bitmap -> MMSPart("image", ContentType.IMAGE_JPEG, bitmap) } + signedBody.takeIf { it.isNotEmpty() }?.toByteArray()?.let { bytes -> + remainingBytes -= bytes.size + parts += MMSPart("text", ContentType.TEXT_PLAIN, bytes) + } - // Send contacts + // Attach contacts parts += attachments .mapNotNull { attachment -> attachment as? Attachment.Contact } .map { attachment -> attachment.vCard.toByteArray() } - .map { vCard -> MMSPart("contact", ContentType.TEXT_VCARD, vCard) } + .map { vCard -> + remainingBytes -= vCard.size + MMSPart("contact", ContentType.TEXT_VCARD, vCard) + } + + val imageBytesByAttachment = attachments + .mapNotNull { attachment -> attachment as? Attachment.Image } + .associateWith { attachment -> + val uri = attachment.getUri() ?: return@associateWith byteArrayOf() + when (attachment.isGif(context)) { + true -> ImageUtils.getScaledGif(context, uri, maxWidth, maxHeight) + false -> ImageUtils.getScaledImage(context, uri, maxWidth, maxHeight) + } + } + .toMutableMap() + + val imageByteCount = imageBytesByAttachment.values.sumBy { byteArray -> byteArray.size } + if (imageByteCount > remainingBytes) { + imageBytesByAttachment.forEach { (attachment, originalBytes) -> + val uri = attachment.getUri() ?: return@forEach + val maxBytes = originalBytes.size / imageByteCount.toFloat() * remainingBytes + + // Get the image dimensions + val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } + BitmapFactory.decodeStream(context.contentResolver.openInputStream(uri), null, options) + val width = options.outWidth + val height = options.outHeight + val aspectRatio = width.toFloat() / height.toFloat() + + var attempts = 0 + var scaledBytes = originalBytes + + while (scaledBytes.size > maxBytes) { + // Estimate how much we need to scale the image down by. If it's still too big, we'll need to + // try smaller and smaller values + val scale = maxBytes / originalBytes.size * (0.9 - attempts * 0.2) + if (scale <= 0) { + Timber.w("Failed to compress ${originalBytes.size / 1024}Kb to ${maxBytes.toInt() / 1024}Kb") + return@forEach + } + + val newArea = scale * width * height + val newWidth = sqrt(newArea * aspectRatio).toInt() + val newHeight = (newWidth / aspectRatio).toInt() + + attempts++ + scaledBytes = when (attachment.isGif(context)) { + true -> ImageUtils.getScaledGif(context, uri, newWidth, newHeight, 80) + false -> ImageUtils.getScaledImage(context, uri, newWidth, newHeight, 80) + } + + Timber.d("Compression attempt $attempts: ${scaledBytes.size / 1024}/${maxBytes.toInt() / 1024}Kb ($width*$height -> $newWidth*$newHeight)") + } + + Timber.v("Compressed ${originalBytes.size / 1024}Kb to ${scaledBytes.size / 1024}Kb with a target size of ${maxBytes.toInt() / 1024}Kb in $attempts attempts") + imageBytesByAttachment[attachment] = scaledBytes + } + } + + imageBytesByAttachment.forEach { (attachment, bytes) -> + parts += when (attachment.isGif(context)) { + true -> MMSPart("image", ContentType.IMAGE_GIF, bytes) + false -> MMSPart("image", ContentType.IMAGE_JPEG, bytes) + } + } // We need to strip the separators from outgoing MMS, or else they'll appear to have sent and not go through val transaction = Transaction(context) diff --git a/data/src/main/java/com/moez/QKSMS/util/GlideAppModule.kt b/data/src/main/java/com/moez/QKSMS/util/GlideAppModule.kt index cddc39f2a..294a2c647 100644 --- a/data/src/main/java/com/moez/QKSMS/util/GlideAppModule.kt +++ b/data/src/main/java/com/moez/QKSMS/util/GlideAppModule.kt @@ -34,6 +34,7 @@ class GlideAppModule : AppGlideModule() { } override fun registerComponents(context: Context, glide: Glide, registry: Registry) { + // registry.prepend(GifDrawable::class.java, ReEncodingGifResourceEncoder(context, glide.bitmapPool)) } } diff --git a/data/src/main/java/com/moez/QKSMS/util/ImageUtils.kt b/data/src/main/java/com/moez/QKSMS/util/ImageUtils.kt index c9d613d83..ff74b7fd5 100644 --- a/data/src/main/java/com/moez/QKSMS/util/ImageUtils.kt +++ b/data/src/main/java/com/moez/QKSMS/util/ImageUtils.kt @@ -19,77 +19,35 @@ package com.moez.QKSMS.util import android.content.Context -import android.graphics.Bitmap import android.net.Uri -import com.bumptech.glide.load.resource.bitmap.CenterCrop import java.io.ByteArrayOutputStream object ImageUtils { - fun compressGif(context: Context, uri: Uri, maxBytes: Int): ByteArray { - val request = GlideApp + fun getScaledGif(context: Context, uri: Uri, maxWidth: Int, maxHeight: Int, quality: Int = 90): ByteArray { + val gif = GlideApp .with(context) .asGif() .load(uri) - .transform(CenterCrop()) - - val gif = request.submit().get() - - val width = gif.firstFrame.width - val height = gif.firstFrame.height - - val stream = ByteArrayOutputStream() - GifEncoder(context, GlideApp.get(context).bitmapPool) - .encodeTransformedToStream(gif, stream) - - val unscaledBytes = stream.size().toDouble() - - var attempts = 0 - var bytes = unscaledBytes - while (maxBytes > 0 && bytes > maxBytes) { - val scale = Math.sqrt(maxBytes / unscaledBytes) * (1 - attempts * 0.1) - - val scaledGif = request.submit((width * scale).toInt(), (height * scale).toInt()).get() - - stream.reset() - GifEncoder(context, GlideApp.get(context).bitmapPool) - .encodeTransformedToStream(scaledGif, stream) - - attempts++ - bytes = stream.size().toDouble() - } - - return stream.toByteArray() + .centerInside() + .encodeQuality(quality) + .submit(maxWidth, maxHeight) + .get() + + val outputStream = ByteArrayOutputStream() + GifEncoder(context, GlideApp.get(context).bitmapPool).encodeTransformedToStream(gif, outputStream) + return outputStream.toByteArray() } - fun compressBitmap(src: Bitmap, maxBytes: Int): ByteArray { - val quality = 90 - - val height = src.height - val width = src.width - - val stream = ByteArrayOutputStream() - src.compress(Bitmap.CompressFormat.JPEG, quality, stream) - - val unscaledBytes = stream.size().toDouble() - - // Based on the byte size of the bitmap, we'll try to reduce the image's dimensions such - // that it will fit within the max byte size set. If we don't get it right the first time, - // use a slightly heavier compression until we fit within the max size - var attempts = 0 - var bytes = unscaledBytes - while (maxBytes > 0 && bytes > maxBytes) { - val scale = Math.sqrt(maxBytes / unscaledBytes) * (1 - attempts * 0.1) - - stream.reset() - Bitmap.createScaledBitmap(src, (width * scale).toInt(), (height * scale).toInt(), true) - .compress(Bitmap.CompressFormat.JPEG, quality, stream) - - attempts++ - bytes = stream.size().toDouble() - } - - return stream.toByteArray().also { src.recycle() } + fun getScaledImage(context: Context, uri: Uri, maxWidth: Int, maxHeight: Int, quality: Int = 90): ByteArray { + return GlideApp + .with(context) + .`as`(ByteArray::class.java) + .load(uri) + .centerInside() + .encodeQuality(quality) + .submit(maxWidth, maxHeight) + .get() } } \ No newline at end of file diff --git a/domain/src/main/java/com/moez/QKSMS/repository/ImageRepository.kt b/domain/src/main/java/com/moez/QKSMS/repository/ImageRepository.kt deleted file mode 100644 index da02a1f8b..000000000 --- a/domain/src/main/java/com/moez/QKSMS/repository/ImageRepository.kt +++ /dev/null @@ -1,28 +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.repository - -import android.graphics.Bitmap -import android.net.Uri - -interface ImageRepository { - - fun loadImage(uri: Uri, width: Int, height: Int): Bitmap? - -} diff --git a/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt b/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt index d783e0249..e994247df 100644 --- a/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt +++ b/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt @@ -110,7 +110,7 @@ class Preferences @Inject constructor( val unicode = rxPrefs.getBoolean("unicode", false) val mobileOnly = rxPrefs.getBoolean("mobileOnly", false) val longAsMms = rxPrefs.getBoolean("longAsMms", false) - val mmsSize = rxPrefs.getInteger("mmsSize", 300) + val mmsSize = rxPrefs.getInteger("mmsSize", -1) val logging = rxPrefs.getBoolean("logging", false) init { diff --git a/presentation/src/main/java/com/moez/QKSMS/injection/AppModule.kt b/presentation/src/main/java/com/moez/QKSMS/injection/AppModule.kt index edc51c508..f0ffcca7e 100644 --- a/presentation/src/main/java/com/moez/QKSMS/injection/AppModule.kt +++ b/presentation/src/main/java/com/moez/QKSMS/injection/AppModule.kt @@ -74,8 +74,6 @@ import com.moez.QKSMS.repository.ContactRepository import com.moez.QKSMS.repository.ContactRepositoryImpl import com.moez.QKSMS.repository.ConversationRepository import com.moez.QKSMS.repository.ConversationRepositoryImpl -import com.moez.QKSMS.repository.ImageRepository -import com.moez.QKSMS.repository.ImageRepositoryImpl import com.moez.QKSMS.repository.MessageRepository import com.moez.QKSMS.repository.MessageRepositoryImpl import com.moez.QKSMS.repository.ScheduledMessageRepository @@ -200,9 +198,6 @@ class AppModule(private var application: Application) { @Provides fun provideConversationRepository(repository: ConversationRepositoryImpl): ConversationRepository = repository - @Provides - fun provideImageRepository(repository: ImageRepositoryImpl): ImageRepository = repository - @Provides fun provideMessageRepository(repository: MessageRepositoryImpl): MessageRepository = repository diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index c119e0ad2..4a87cdccb 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -428,9 +428,10 @@ + Automatic 100KB 200KB - 300KB (Recommended) + 300KB 600KB 1000KB 2000KB @@ -438,6 +439,7 @@ + -1 100 200 300 -- GitLab From 7d78e3b42cc1f5e5c55bb174a21369a256d82d4b Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Wed, 8 Jan 2020 23:33:36 -0500 Subject: [PATCH 081/109] Fix build --- data/build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/data/build.gradle b/data/build.gradle index c66279856..a78666df9 100644 --- a/data/build.gradle +++ b/data/build.gradle @@ -57,7 +57,6 @@ dependencies { implementation "androidx.exifinterface:exifinterface:$androidx_exifinterface_version" // glide - implementation "com.github.bumptech.glide:gifencoder-integration:$glide_version" implementation "com.github.bumptech.glide:glide:$glide_version" kapt "com.github.bumptech.glide:compiler:$glide_version" -- GitLab From e45ce20e288207c91a18c00807bdbf22acf40857 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Sat, 11 Jan 2020 14:03:17 -0500 Subject: [PATCH 082/109] Rewrite ConversationInfo screen Fixes #1413 --- .../ConversationInfoAdapter.kt | 153 ++++++++++++++++++ .../ConversationInfoController.kt | 75 +++------ .../conversationinfo/ConversationInfoItem.kt | 20 +++ .../ConversationInfoPresenter.kt | 50 +++--- .../conversationinfo/ConversationInfoState.kt | 13 +- .../conversationinfo/ConversationInfoView.kt | 1 + .../ConversationMediaAdapter.kt | 63 -------- .../ConversationRecipientAdapter.kt | 82 ---------- .../GridSpacingItemDecoration.kt | 22 ++- .../layout/conversation_info_controller.xml | 92 +---------- .../res/layout/conversation_info_settings.xml | 74 +++++++++ .../layout/conversation_media_list_item.xml | 5 +- 12 files changed, 324 insertions(+), 326 deletions(-) create mode 100644 presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoAdapter.kt create mode 100644 presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoItem.kt delete mode 100644 presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationMediaAdapter.kt delete mode 100644 presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationRecipientAdapter.kt create mode 100644 presentation/src/main/res/layout/conversation_info_settings.xml diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoAdapter.kt new file mode 100644 index 000000000..f75034171 --- /dev/null +++ b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoAdapter.kt @@ -0,0 +1,153 @@ +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() { + + val recipientClicks: Subject = PublishSubject.create() + val recipientLongClicks: Subject = PublishSubject.create() + val themeClicks: Subject = PublishSubject.create() + val nameClicks: Subject = PublishSubject.create() + val notificationClicks: Subject = PublishSubject.create() + val archiveClicks: Subject = PublishSubject.create() + val blockClicks: Subject = PublishSubject.create() + val deleteClicks: Subject = PublishSubject.create() + val mediaClicks: Subject = 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 + } + } + +} diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoController.kt b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoController.kt index d618b344d..95a1f241b 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoController.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoController.kt @@ -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 = recipientAdapter.recipientClicks - - override fun recipientLongClicks(): Observable = recipientAdapter.recipientLongClicks - - override fun themeClicks(): Observable = recipientAdapter.themeClicks - - override fun nameClicks(): Observable<*> = name.clicks() - - override fun nameChanges(): Observable = 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 = adapter.recipientClicks + override fun recipientLongClicks(): Observable = adapter.recipientLongClicks + override fun themeClicks(): Observable = adapter.themeClicks + override fun nameClicks(): Observable<*> = adapter.nameClicks + override fun nameChanges(): Observable = 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 = adapter.mediaClicks + override fun showNameDialog(name: String) = nameDialog.setText(name).show() override fun showThemePicker(recipientId: Long) { diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoItem.kt b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoItem.kt new file mode 100644 index 000000000..810aa21c8 --- /dev/null +++ b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoItem.kt @@ -0,0 +1,20 @@ +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, + val archived: Boolean, + val blocked: Boolean + ) : ConversationInfoItem() + + data class ConversationInfoMedia(val value: MmsPart) : ConversationInfoItem() + +} diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoPresenter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoPresenter.kt index f595b0623..2ad141bd7 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoPresenter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoPresenter.kt @@ -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( - ConversationInfoState(threadId = threadId, media = messageRepo.getPartsForConversation(threadId)) + ConversationInfoState(threadId = threadId) ) { private val conversation: Subject = 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() + + 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 diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoState.kt b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoState.kt index cbda9f47e..693a862ee 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoState.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoState.kt @@ -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? = null, val threadId: Long = 0, - val archived: Boolean = false, - val blocked: Boolean = false, - val media: RealmResults? = null, + val data: List = listOf(), val hasError: Boolean = false -) \ No newline at end of file +) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoView.kt b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoView.kt index 343dd8d03..5a86ffffe 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoView.kt @@ -33,6 +33,7 @@ interface ConversationInfoView : QkViewContract { fun blockClicks(): Observable<*> fun deleteClicks(): Observable<*> fun confirmDelete(): Observable<*> + fun mediaClicks(): Observable fun showNameDialog(name: String) fun showThemePicker(recipientId: Long) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationMediaAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationMediaAdapter.kt deleted file mode 100644 index 16565c6d1..000000000 --- a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationMediaAdapter.kt +++ /dev/null @@ -1,63 +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.conversationinfo - -import android.content.Context -import android.view.LayoutInflater -import android.view.ViewGroup -import com.moez.QKSMS.R -import com.moez.QKSMS.common.Navigator -import com.moez.QKSMS.common.base.QkRealmAdapter -import com.moez.QKSMS.common.base.QkViewHolder -import com.moez.QKSMS.common.util.extensions.setVisible -import com.moez.QKSMS.extensions.isVideo -import com.moez.QKSMS.model.MmsPart -import com.moez.QKSMS.util.GlideApp -import kotlinx.android.synthetic.main.conversation_media_list_item.* -import kotlinx.android.synthetic.main.conversation_media_list_item.view.* -import javax.inject.Inject - -class ConversationMediaAdapter @Inject constructor( - private val context: Context, - private val navigator: Navigator -) : QkRealmAdapter() { - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkViewHolder { - val inflater = LayoutInflater.from(parent.context) - val view = inflater.inflate(R.layout.conversation_media_list_item, parent, false) - return QkViewHolder(view).apply { - view.thumbnail.setOnClickListener { - val part = getItem(adapterPosition) ?: return@setOnClickListener - navigator.showMedia(part.id) - } - } - } - - override fun onBindViewHolder(holder: QkViewHolder, position: Int) { - val part = getItem(position) ?: return - - GlideApp.with(context) - .load(part.getUri()) - .fitCenter() - .into(holder.thumbnail) - - holder.video.setVisible(part.isVideo()) - } - -} \ No newline at end of file diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationRecipientAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationRecipientAdapter.kt deleted file mode 100644 index f165e5429..000000000 --- a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationRecipientAdapter.kt +++ /dev/null @@ -1,82 +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.conversationinfo - -import android.view.LayoutInflater -import android.view.ViewGroup -import com.moez.QKSMS.R -import com.moez.QKSMS.common.base.QkRealmAdapter -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.model.Recipient -import io.reactivex.subjects.PublishSubject -import io.reactivex.subjects.Subject -import kotlinx.android.synthetic.main.conversation_recipient_list_item.* -import kotlinx.android.synthetic.main.conversation_recipient_list_item.view.* -import javax.inject.Inject - -class ConversationRecipientAdapter @Inject constructor( - private val colors: Colors -) : QkRealmAdapter() { - - val recipientClicks: Subject = PublishSubject.create() - val recipientLongClicks: Subject = PublishSubject.create() - val themeClicks: Subject = PublishSubject.create() - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkViewHolder { - val layoutInflater = LayoutInflater.from(parent.context) - val view = layoutInflater.inflate(R.layout.conversation_recipient_list_item, parent, false) - return QkViewHolder(view).apply { - view.setOnClickListener { - val recipient = getItem(adapterPosition) ?: return@setOnClickListener - recipientClicks.onNext(recipient.id) - } - - view.setOnLongClickListener { - val recipient = getItem(adapterPosition) ?: return@setOnLongClickListener false - recipientLongClicks.onNext(recipient.id) - return@setOnLongClickListener true - } - - view.theme.setOnClickListener { - val recipient = getItem(adapterPosition) ?: return@setOnClickListener - themeClicks.onNext(recipient.id) - } - } - } - - override fun onBindViewHolder(holder: QkViewHolder, position: Int) { - val recipient = getItem(position) ?: return - - 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) - } - -} diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/GridSpacingItemDecoration.kt b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/GridSpacingItemDecoration.kt index 6c4b4e71c..41bfdf906 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/GridSpacingItemDecoration.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/GridSpacingItemDecoration.kt @@ -23,9 +23,13 @@ import android.graphics.Rect import android.view.View import androidx.recyclerview.widget.RecyclerView import com.moez.QKSMS.common.util.extensions.dpToPx -import javax.inject.Inject +import com.moez.QKSMS.feature.conversationinfo.ConversationInfoItem.ConversationInfoMedia +import com.moez.QKSMS.feature.conversationinfo.ConversationInfoItem.ConversationInfoRecipient -class GridSpacingItemDecoration @Inject constructor(context: Context) : RecyclerView.ItemDecoration() { +class GridSpacingItemDecoration( + private val adapter: ConversationInfoAdapter, + private val context: Context +) : RecyclerView.ItemDecoration() { private val spanCount = 3 private val spacing = 2.dpToPx(context) @@ -34,13 +38,19 @@ class GridSpacingItemDecoration @Inject constructor(context: Context) : Recycler super.getItemOffsets(outRect, view, parent, state) val position = parent.getChildAdapterPosition(view) - val column = position % spanCount + val item = adapter.getItem(position) - outRect.left = column * spacing / spanCount - outRect.right = spacing - (column + 1) * spacing / spanCount + if (item is ConversationInfoRecipient && adapter.getItem(position + 1) !is ConversationInfoRecipient) { + outRect.bottom = 8.dpToPx(context) + } else if (item is ConversationInfoMedia) { + val firstPartIndex = adapter.data.indexOfFirst { it is ConversationInfoMedia } + val localPartIndex = position - firstPartIndex + + val column = localPartIndex % spanCount - if (position >= spanCount) { outRect.top = spacing + outRect.left = column * spacing / spanCount + outRect.right = spacing - (column + 1) * spacing / spanCount } } diff --git a/presentation/src/main/res/layout/conversation_info_controller.xml b/presentation/src/main/res/layout/conversation_info_controller.xml index 0ccb95d7f..65558d5d7 100644 --- a/presentation/src/main/res/layout/conversation_info_controller.xml +++ b/presentation/src/main/res/layout/conversation_info_controller.xml @@ -17,91 +17,11 @@ ~ You should have received a copy of the GNU General Public License ~ along with QKSMS. If not, see . --> - - - - - - - - - - - - - - - - - - - - - - - - - + android:clipChildren="false" + android:clipToPadding="false" + android:paddingTop="8dp" + android:paddingBottom="8dp" /> diff --git a/presentation/src/main/res/layout/conversation_info_settings.xml b/presentation/src/main/res/layout/conversation_info_settings.xml new file mode 100644 index 000000000..50d5903af --- /dev/null +++ b/presentation/src/main/res/layout/conversation_info_settings.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + diff --git a/presentation/src/main/res/layout/conversation_media_list_item.xml b/presentation/src/main/res/layout/conversation_media_list_item.xml index 9838824b4..aeee85976 100644 --- a/presentation/src/main/res/layout/conversation_media_list_item.xml +++ b/presentation/src/main/res/layout/conversation_media_list_item.xml @@ -22,11 +22,10 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> - -- GitLab From 8720a6664906bafa61a2253057cc789b6ecfabad Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Sat, 11 Jan 2020 14:38:54 -0500 Subject: [PATCH 083/109] Improve randomness of contact colours --- .../src/main/java/com/moez/QKSMS/common/util/Colors.kt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/presentation/src/main/java/com/moez/QKSMS/common/util/Colors.kt b/presentation/src/main/java/com/moez/QKSMS/common/util/Colors.kt index 7eed92601..ed74dace7 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/util/Colors.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/util/Colors.kt @@ -134,11 +134,7 @@ class Colors @Inject constructor( } private fun generateColor(recipient: Recipient): Int { - val first = recipient.contact?.name?.firstOrNull() - ?: phoneNumberUtils.normalizeNumber(recipient.address).firstOrNull() - ?: '#' - - val index = first.hashCode().absoluteValue % randomColors.size + val index = recipient.address.hashCode().absoluteValue % randomColors.size return randomColors[index] } } -- GitLab From 3a3c7451c75980bf06284ea729de7c942e827501 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Sat, 11 Jan 2020 21:02:48 -0500 Subject: [PATCH 084/109] Improve randomized colours --- presentation/src/main/res/values/colors.xml | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/presentation/src/main/res/values/colors.xml b/presentation/src/main/res/values/colors.xml index d9533b5e2..503891293 100644 --- a/presentation/src/main/res/values/colors.xml +++ b/presentation/src/main/res/values/colors.xml @@ -66,15 +66,13 @@ #00838F - #e57373 - #7986CB - #00BFA5 - #81C784 - #FFAB40 - #FF8A65 - #8d6e63 - #448AFF - #78909c + #06C9AF + #6DC966 + #F1AF28 + #FF8963 + #FF6969 + #5D99FF + #8899EC -- GitLab From 624920fc831570be44954f0a2f5321a89aac6e86 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Sat, 11 Jan 2020 21:45:35 -0500 Subject: [PATCH 085/109] Fix clipToOutline issues --- presentation/src/main/res/drawable/circle.xml | 5 +++-- presentation/src/main/res/layout/avatar_view.xml | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/presentation/src/main/res/drawable/circle.xml b/presentation/src/main/res/drawable/circle.xml index eeac2e096..a073500be 100644 --- a/presentation/src/main/res/drawable/circle.xml +++ b/presentation/src/main/res/drawable/circle.xml @@ -18,8 +18,9 @@ ~ along with QKSMS. If not, see . --> + android:shape="rectangle"> + - \ No newline at end of file + diff --git a/presentation/src/main/res/layout/avatar_view.xml b/presentation/src/main/res/layout/avatar_view.xml index 8f8e2aa9f..420fb7960 100644 --- a/presentation/src/main/res/layout/avatar_view.xml +++ b/presentation/src/main/res/layout/avatar_view.xml @@ -23,7 +23,6 @@ android:id="@+id/avatar" android:layout_width="match_parent" android:layout_height="match_parent" - tools:background="@color/tools_theme" tools:parentTag="com.moez.QKSMS.common.widget.AvatarView"> - \ No newline at end of file + -- GitLab From fa51f6fb3fdb23e310d6fad197578b87f4cf50f6 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Sun, 12 Jan 2020 01:21:10 -0500 Subject: [PATCH 086/109] Use correct default val for theme observable --- .../src/main/java/com/moez/QKSMS/common/util/Colors.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/presentation/src/main/java/com/moez/QKSMS/common/util/Colors.kt b/presentation/src/main/java/com/moez/QKSMS/common/util/Colors.kt index ed74dace7..aee7f9be2 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/util/Colors.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/util/Colors.kt @@ -89,7 +89,8 @@ class Colors @Inject constructor( fun themeObservable(recipient: Recipient? = null): Observable { val pref = when { - recipient == null || !prefs.autoColor.get() -> prefs.theme() + recipient == null -> prefs.theme() + prefs.autoColor.get() -> prefs.theme(recipient.id, prefs.theme().get()) else -> prefs.theme(recipient.id, generateColor(recipient)) } return pref.asObservable() -- GitLab From b7270499002a600cca0f3192e456e9abbe5d16c9 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Sun, 12 Jan 2020 02:02:39 -0500 Subject: [PATCH 087/109] Use Recipient instead of Chip --- .../QKSMS/mapper/CursorToRecipientImpl.kt | 9 +++---- .../repository/ConversationRepositoryImpl.kt | 6 +++++ .../repository/ConversationRepository.kt | 2 ++ .../QKSMS/feature/compose/ComposeActivity.kt | 6 ++--- .../QKSMS/feature/compose/ComposeState.kt | 4 +-- .../moez/QKSMS/feature/compose/ComposeView.kt | 6 ++--- .../QKSMS/feature/compose/ComposeViewModel.kt | 27 +++++++++---------- .../QKSMS/feature/compose/editing/Chip.kt | 26 ------------------ .../feature/compose/editing/ChipsAdapter.kt | 16 +++++------ .../compose/editing/DetailedChipView.kt | 14 +++++----- .../feature/contacts/ContactsViewModel.kt | 17 +++++------- 11 files changed, 55 insertions(+), 78 deletions(-) delete mode 100644 presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/Chip.kt diff --git a/data/src/main/java/com/moez/QKSMS/mapper/CursorToRecipientImpl.kt b/data/src/main/java/com/moez/QKSMS/mapper/CursorToRecipientImpl.kt index 074828909..08bf3974c 100644 --- a/data/src/main/java/com/moez/QKSMS/mapper/CursorToRecipientImpl.kt +++ b/data/src/main/java/com/moez/QKSMS/mapper/CursorToRecipientImpl.kt @@ -37,11 +37,10 @@ class CursorToRecipientImpl @Inject constructor( const val COLUMN_ADDRESS = 1 } - override fun map(from: Cursor) = Recipient().apply { - id = from.getLong(COLUMN_ID) - address = from.getString(COLUMN_ADDRESS) - lastUpdate = System.currentTimeMillis() - } + override fun map(from: Cursor) = Recipient( + id = from.getLong(COLUMN_ID), + address = from.getString(COLUMN_ADDRESS), + lastUpdate = System.currentTimeMillis()) override fun getRecipientCursor(): Cursor? { return when (permissionManager.hasReadSms()) { 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 9cb9aad1a..7706c904a 100644 --- a/data/src/main/java/com/moez/QKSMS/repository/ConversationRepositoryImpl.kt +++ b/data/src/main/java/com/moez/QKSMS/repository/ConversationRepositoryImpl.kt @@ -220,6 +220,12 @@ class ConversationRepositoryImpl @Inject constructor( .observeOn(Schedulers.io()) } + override fun getRecipients(): RealmResults { + val realm = Realm.getDefaultInstance() + return realm.where(Recipient::class.java) + .findAll() + } + override fun getUnmanagedRecipients(): Observable> { val realm = Realm.getDefaultInstance() return realm.where(Recipient::class.java) 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 0377c6770..7ccf33ff5 100644 --- a/domain/src/main/java/com/moez/QKSMS/repository/ConversationRepository.kt +++ b/domain/src/main/java/com/moez/QKSMS/repository/ConversationRepository.kt @@ -54,6 +54,8 @@ interface ConversationRepository { fun getUnmanagedConversations(): Observable> + fun getRecipients(): RealmResults + fun getUnmanagedRecipients(): Observable> fun getRecipient(recipientId: Long): Recipient? 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 fbf41f536..536e0e944 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 @@ -56,10 +56,10 @@ import com.moez.QKSMS.common.util.extensions.setBackgroundTint import com.moez.QKSMS.common.util.extensions.setTint import com.moez.QKSMS.common.util.extensions.setVisible import com.moez.QKSMS.common.util.extensions.showKeyboard -import com.moez.QKSMS.feature.compose.editing.Chip import com.moez.QKSMS.feature.compose.editing.ChipsAdapter import com.moez.QKSMS.feature.contacts.ContactsActivity import com.moez.QKSMS.model.Attachment +import com.moez.QKSMS.model.Recipient import com.uber.autodispose.android.lifecycle.scope import com.uber.autodispose.autoDisposable import dagger.android.AndroidInjection @@ -92,7 +92,7 @@ class ComposeActivity : QkThemedActivity(), ComposeView { override val activityVisibleIntent: Subject = PublishSubject.create() override val chipsSelectedIntent: Subject> = PublishSubject.create() - override val chipDeletedIntent: Subject by lazy { chipsAdapter.chipDeleted } + override val chipDeletedIntent: Subject by lazy { chipsAdapter.chipDeleted } override val menuReadyIntent: Observable = menu.map { Unit } override val optionsItemIntent: Subject = PublishSubject.create() override val sendAsGroupIntent by lazy { sendAsGroupBackground.clicks() } @@ -307,7 +307,7 @@ class ComposeActivity : QkThemedActivity(), ComposeView { startActivityForResult(Intent.createChooser(intent, null), AttachContactRequestCode) } - override fun showContacts(sharing: Boolean, chips: List) { + override fun showContacts(sharing: Boolean, chips: List) { message.hideKeyboard() val serialized = HashMap(chips.associate { chip -> chip.address to chip.contact?.lookupKey }) val intent = Intent(this, ContactsActivity::class.java) 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 0de28b089..1524b2018 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 @@ -19,17 +19,17 @@ package com.moez.QKSMS.feature.compose import com.moez.QKSMS.compat.SubscriptionInfoCompat -import com.moez.QKSMS.feature.compose.editing.Chip import com.moez.QKSMS.model.Attachment import com.moez.QKSMS.model.Conversation import com.moez.QKSMS.model.Message +import com.moez.QKSMS.model.Recipient import io.realm.RealmResults data class ComposeState( val hasError: Boolean = false, val editingMode: Boolean = false, val threadId: Long = 0, - val selectedChips: List = ArrayList(), + val selectedChips: List = ArrayList(), val sendAsGroup: Boolean = true, val conversationtitle: String = "", val loading: Boolean = false, 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 687235369..65b9fd865 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 @@ -22,8 +22,8 @@ import android.net.Uri import androidx.annotation.StringRes import androidx.core.view.inputmethod.InputContentInfoCompat import com.moez.QKSMS.common.base.QkView -import com.moez.QKSMS.feature.compose.editing.Chip import com.moez.QKSMS.model.Attachment +import com.moez.QKSMS.model.Recipient import io.reactivex.Observable import io.reactivex.subjects.Subject @@ -31,7 +31,7 @@ interface ComposeView : QkView { val activityVisibleIntent: Observable val chipsSelectedIntent: Subject> - val chipDeletedIntent: Subject + val chipDeletedIntent: Subject val menuReadyIntent: Observable val optionsItemIntent: Observable val sendAsGroupIntent: Observable<*> @@ -62,7 +62,7 @@ interface ComposeView : QkView { fun requestDefaultSms() fun requestStoragePermission() fun requestSmsPermission() - fun showContacts(sharing: Boolean, chips: List) + fun showContacts(sharing: Boolean, chips: List) fun themeChanged() fun showKeyboard() fun requestCamera() 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 5470729e7..605695367 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 @@ -41,14 +41,6 @@ import com.moez.QKSMS.extensions.asObservable import com.moez.QKSMS.extensions.isImage import com.moez.QKSMS.extensions.isVideo import com.moez.QKSMS.extensions.mapNotNull -import com.moez.QKSMS.feature.compose.editing.Chip -import com.moez.QKSMS.feature.compose.editing.ComposeItem -import com.moez.QKSMS.feature.compose.editing.ComposeItem.* -import com.moez.QKSMS.feature.compose.editing.PhoneNumberAction -import com.moez.QKSMS.filter.ContactFilter -import com.moez.QKSMS.interactor.* -import com.moez.QKSMS.model.* -import com.moez.QKSMS.filter.ContactGroupFilter import com.moez.QKSMS.interactor.AddScheduledMessage import com.moez.QKSMS.interactor.CancelDelayedMessage import com.moez.QKSMS.interactor.DeleteMessages @@ -61,6 +53,7 @@ import com.moez.QKSMS.model.Attachment import com.moez.QKSMS.model.Attachments import com.moez.QKSMS.model.Conversation import com.moez.QKSMS.model.Message +import com.moez.QKSMS.model.Recipient import com.moez.QKSMS.repository.ContactRepository import com.moez.QKSMS.repository.ConversationRepository import com.moez.QKSMS.repository.MessageRepository @@ -116,10 +109,10 @@ class ComposeViewModel @Inject constructor( ) { private val attachments: Subject> = BehaviorSubject.createDefault(sharedAttachments) - private val chipsReducer: Subject<(List) -> List> = PublishSubject.create() + private val chipsReducer: Subject<(List) -> List> = PublishSubject.create() private val conversation: Subject = BehaviorSubject.create() private val messages: Subject> = BehaviorSubject.create() - private val selectedChips: Subject> = BehaviorSubject.createDefault(listOf()) + private val selectedChips: Subject> = BehaviorSubject.createDefault(listOf()) private val searchResults: Subject> = BehaviorSubject.create() private val searchSelection: Subject = BehaviorSubject.createDefault(-1) @@ -175,11 +168,11 @@ class ComposeViewModel @Inject constructor( .subscribe(conversation::onNext) if (addresses.isNotEmpty()) { - selectedChips.onNext(addresses.map { address -> Chip(address) }) + selectedChips.onNext(addresses.map { address -> Recipient(address = address) }) } disposables += chipsReducer - .scan(listOf()) { previousState, reducer -> reducer(previousState) } + .scan(listOf()) { previousState, reducer -> reducer(previousState) } .doOnNext { chips -> newState { copy(selectedChips = chips) } } .skipUntil(state.filter { state -> state.editingMode }) .takeUntil(state.filter { state -> !state.editingMode }) @@ -254,13 +247,19 @@ class ComposeViewModel @Inject constructor( } // Filter out any numbers that are already selected hashmap.filter { (address) -> - chips.none { chip -> phoneNumberUtils.compare(address, chip.address) } + chips.none { recipient -> phoneNumberUtils.compare(address, recipient.address) } } } .filter { hashmap -> hashmap.isNotEmpty() } .map { hashmap -> hashmap.map { (address, lookupKey) -> - Chip(address, lookupKey?.let(contactRepo::getUnmanagedContact)) + conversationRepo.getRecipients() + .asSequence() + .filter { recipient -> recipient.contact?.lookupKey == lookupKey } + .first { recipient -> phoneNumberUtils.compare(recipient.address, address) } + ?: Recipient( + address = address, + contact = lookupKey?.let(contactRepo::getUnmanagedContact)) } } .autoDisposable(view.scope()) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/Chip.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/Chip.kt deleted file mode 100644 index 31846fbdb..000000000 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/Chip.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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.editing - -import com.moez.QKSMS.model.Contact - -data class Chip( - val address: String, - val contact: Contact? = null -) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ChipsAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ChipsAdapter.kt index 04f5e7c78..b415d32f3 100755 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ChipsAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ChipsAdapter.kt @@ -35,10 +35,10 @@ import io.reactivex.subjects.PublishSubject import kotlinx.android.synthetic.main.contact_chip.* import javax.inject.Inject -class ChipsAdapter @Inject constructor() : QkAdapter() { +class ChipsAdapter @Inject constructor() : QkAdapter() { var view: RecyclerView? = null - val chipDeleted: PublishSubject = PublishSubject.create() + val chipDeleted: PublishSubject = PublishSubject.create() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkViewHolder { val inflater = LayoutInflater.from(parent.context) @@ -57,18 +57,18 @@ class ChipsAdapter @Inject constructor() : QkAdapter() { } override fun onBindViewHolder(holder: QkViewHolder, position: Int) { - val chip = getItem(position) + val recipient = getItem(position) - holder.avatar.setRecipient(Recipient(id = -1, contact = chip.contact)) - holder.name.text = chip.contact?.name?.takeIf { it.isNotBlank() } ?: chip.address + holder.avatar.setRecipient(recipient) + holder.name.text = recipient.contact?.name?.takeIf { it.isNotBlank() } ?: recipient.address } /** * The [context] has to come from a view, because we're inflating a view that used themed attrs */ - private fun showDetailedChip(context: Context, chip: Chip) { + private fun showDetailedChip(context: Context, recipient: Recipient) { val detailedChipView = DetailedChipView(context) - detailedChipView.setChip(chip) + detailedChipView.setRecipient(recipient) val rootView = view?.rootView as ViewGroup @@ -83,7 +83,7 @@ class ChipsAdapter @Inject constructor() : QkAdapter() { detailedChipView.show() detailedChipView.setOnDeleteListener { - chipDeleted.onNext(chip) + chipDeleted.onNext(recipient) detailedChipView.hide() } } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/DetailedChipView.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/DetailedChipView.kt index 370d1a59c..aa90cd994 100755 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/DetailedChipView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/DetailedChipView.kt @@ -45,8 +45,14 @@ class DetailedChipView(context: Context) : RelativeLayout(context) { isFocusable = true isFocusableInTouchMode = true + } + + fun setRecipient(recipient: Recipient) { + avatar.setRecipient(recipient) + name.text = recipient.contact?.name?.takeIf { it.isNotBlank() } ?: recipient.address + info.text = recipient.address - colors.theme().let { theme -> + colors.theme(recipient).let { theme -> card.setBackgroundTint(theme.theme) name.setTextColor(theme.textPrimary) info.setTextColor(theme.textTertiary) @@ -54,12 +60,6 @@ class DetailedChipView(context: Context) : RelativeLayout(context) { } } - fun setChip(chip: Chip) { - avatar.setRecipient(Recipient(id = -1, contact = chip.contact)) - name.text = chip.contact?.name?.takeIf { it.isNotBlank() } ?: chip.address - info.text = chip.address - } - fun show() { startAnimation(AlphaAnimation(0f, 1f).apply { duration = 200 }) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsViewModel.kt b/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsViewModel.kt index cb9386f29..c101d218e 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsViewModel.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsViewModel.kt @@ -22,7 +22,6 @@ import android.view.inputmethod.EditorInfo import com.moez.QKSMS.common.base.QkViewModel import com.moez.QKSMS.extensions.mapNotNull import com.moez.QKSMS.extensions.removeAccents -import com.moez.QKSMS.feature.compose.editing.Chip import com.moez.QKSMS.feature.compose.editing.ComposeItem import com.moez.QKSMS.feature.compose.editing.PhoneNumberAction import com.moez.QKSMS.filter.ContactFilter @@ -32,6 +31,7 @@ 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 com.moez.QKSMS.model.Recipient import com.moez.QKSMS.repository.ContactRepository import com.moez.QKSMS.repository.ConversationRepository import com.moez.QKSMS.util.PhoneNumberUtils @@ -68,7 +68,7 @@ class ContactsViewModel @Inject constructor( .observeOn(Schedulers.io()) .map { hashmap -> hashmap.map { (address, lookupKey) -> - Chip(address, lookupKey?.let(contactsRepo::getUnmanagedContact)) + Recipient(address = address, contact = lookupKey?.let(contactsRepo::getUnmanagedContact)) } } @@ -181,11 +181,10 @@ class ContactsViewModel @Inject constructor( .observeOn(Schedulers.io()) .autoDisposable(view.scope()) .subscribe { (composeItem, force) -> - val contacts = composeItem.getContacts() - val newChips = contacts.map { contact -> + view.finish(HashMap(composeItem.getContacts().associate { contact -> if (contact.numbers.size == 1 || contact.getDefaultNumber() != null && !force) { - val number = contact.getDefaultNumber() ?: contact.numbers[0]!! - Chip(number.address, contact) + val address = contact.getDefaultNumber()?.address ?: contact.numbers[0]!!.address + address to contact.lookupKey } else { runBlocking { newState { copy(selectedContact = contact) } @@ -203,12 +202,10 @@ class ContactsViewModel @Inject constructor( setDefaultPhoneNumber.execute(params) } - Chip(number.address, contact) + number.address to contact.lookupKey } ?: return@subscribe } - } - - view.finish(HashMap(newChips.associate { chip -> chip.address to chip.contact?.lookupKey })) + })) } } -- GitLab From fe362d395aebd0f0586cdded053ea0bf6631b4b2 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Sun, 12 Jan 2020 02:05:04 -0500 Subject: [PATCH 088/109] Fix 7d9cd35c7c1ca1c6168d0afaa66164e9e2c23556 --- .../src/main/java/com/moez/QKSMS/common/util/Colors.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/presentation/src/main/java/com/moez/QKSMS/common/util/Colors.kt b/presentation/src/main/java/com/moez/QKSMS/common/util/Colors.kt index aee7f9be2..35dfe09c0 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/util/Colors.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/util/Colors.kt @@ -90,8 +90,8 @@ class Colors @Inject constructor( fun themeObservable(recipient: Recipient? = null): Observable { val pref = when { recipient == null -> prefs.theme() - prefs.autoColor.get() -> prefs.theme(recipient.id, prefs.theme().get()) - else -> prefs.theme(recipient.id, generateColor(recipient)) + prefs.autoColor.get() -> prefs.theme(recipient.id, generateColor(recipient)) + else -> prefs.theme(recipient.id, prefs.theme().get()) } return pref.asObservable() .map { color -> Theme(color, this) } -- GitLab From 896df9f9015e17a7cff8c945d9b1b081bb874cdf Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Sun, 12 Jan 2020 18:32:45 -0500 Subject: [PATCH 089/109] Formatting --- .../manager/ActiveConversationManagerImpl.kt | 2 +- .../manager/ActiveConversationManager.kt | 2 +- .../QKSMS/feature/compose/ComposeViewModel.kt | 31 +++++++++---------- .../conversations/ConversationsAdapter.kt | 8 ----- .../moez/QKSMS/feature/main/MainActivity.kt | 21 ++++++------- .../src/main/res/layout/main_activity.xml | 1 + 6 files changed, 28 insertions(+), 37 deletions(-) diff --git a/data/src/main/java/com/moez/QKSMS/manager/ActiveConversationManagerImpl.kt b/data/src/main/java/com/moez/QKSMS/manager/ActiveConversationManagerImpl.kt index 65892a0de..4ef827650 100644 --- a/data/src/main/java/com/moez/QKSMS/manager/ActiveConversationManagerImpl.kt +++ b/data/src/main/java/com/moez/QKSMS/manager/ActiveConversationManagerImpl.kt @@ -34,4 +34,4 @@ class ActiveConversationManagerImpl @Inject constructor() : ActiveConversationMa return threadId } -} \ No newline at end of file +} diff --git a/domain/src/main/java/com/moez/QKSMS/manager/ActiveConversationManager.kt b/domain/src/main/java/com/moez/QKSMS/manager/ActiveConversationManager.kt index 35d125346..4ed432d4c 100644 --- a/domain/src/main/java/com/moez/QKSMS/manager/ActiveConversationManager.kt +++ b/domain/src/main/java/com/moez/QKSMS/manager/ActiveConversationManager.kt @@ -29,4 +29,4 @@ interface ActiveConversationManager { fun getActiveConversation(): Long? -} \ No newline at end of file +} 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 605695367..618b6ec67 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 @@ -32,7 +32,6 @@ import com.moez.QKSMS.common.Navigator import com.moez.QKSMS.common.base.QkViewModel import com.moez.QKSMS.common.util.BillingManager import com.moez.QKSMS.common.util.ClipboardUtils -import com.moez.QKSMS.common.util.Colors import com.moez.QKSMS.common.util.MessageDetailsFormatter import com.moez.QKSMS.common.util.extensions.makeToast import com.moez.QKSMS.compat.SubscriptionManagerCompat @@ -83,7 +82,6 @@ class ComposeViewModel @Inject constructor( @Named("addresses") private val addresses: List, @Named("text") private val sharedText: String, @Named("attachments") private val sharedAttachments: Attachments, - private val colors: Colors, private val contactRepo: ContactRepository, private val context: Context, private val activeConversationManager: ActiveConversationManager, @@ -455,21 +453,22 @@ class ComposeViewModel @Inject constructor( .subscribe { message -> cancelMessage.execute(message.id) } // Set the current conversation - Observables.combineLatest( - view.activityVisibleIntent.distinctUntilChanged(), - conversation.mapNotNull { conversation -> - conversation.takeIf { it.isValid }?.id - }.distinctUntilChanged()) - { visible, threadId -> - when (visible) { - true -> { - activeConversationManager.setActiveConversation(threadId) - markRead.execute(listOf(threadId)) - } + Observables + .combineLatest( + view.activityVisibleIntent.distinctUntilChanged(), + conversation.mapNotNull { conversation -> + conversation.takeIf { it.isValid }?.id + }.distinctUntilChanged()) + { visible, threadId -> + when (visible) { + true -> { + activeConversationManager.setActiveConversation(threadId) + markRead.execute(listOf(threadId)) + } - false -> activeConversationManager.setActiveConversation(null) - } - } + false -> activeConversationManager.setActiveConversation(null) + } + } .autoDisposable(view.scope()) .subscribe() diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/conversations/ConversationsAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/conversations/ConversationsAdapter.kt index 56b5f51e6..cf741f840 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/conversations/ConversationsAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/conversations/ConversationsAdapter.kt @@ -45,10 +45,6 @@ class ConversationsAdapter @Inject constructor( private val phoneNumberUtils: PhoneNumberUtils ) : QkRealmAdapter() { - init { - setHasStableIds(true) - } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkViewHolder { val layoutInflater = LayoutInflater.from(parent.context) val view = layoutInflater.inflate(R.layout.conversation_list_item, parent, false) @@ -113,10 +109,6 @@ class ConversationsAdapter @Inject constructor( holder.unread.setTint(colors.theme(recipient).theme) } - override fun getItemId(index: Int): Long { - return getItem(index)!!.id - } - override fun getItemViewType(position: Int): Int { return if (getItem(position)?.unread == false) 0 else 1 } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainActivity.kt b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainActivity.kt index d2ca475a1..3fdc07ec0 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainActivity.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainActivity.kt @@ -38,7 +38,6 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProviders import androidx.recyclerview.widget.ItemTouchHelper -import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.snackbar.Snackbar import com.jakewharton.rxbinding2.view.clicks import com.jakewharton.rxbinding2.widget.textChanges @@ -146,8 +145,8 @@ class MainActivity : QkThemedActivity(), MainView { homeIntent.onNext(Unit) } - recyclerView.setHasFixedSize(true) - recyclerView.layoutManager = LinearLayoutManager(this) + itemTouchCallback.adapter = conversationsAdapter + conversationsAdapter.autoScrollToStart(recyclerView) // Don't allow clicks to pass through the drawer layout drawer.clicks().autoDisposable(scope()).subscribe() @@ -157,8 +156,10 @@ class MainActivity : QkThemedActivity(), MainView { .autoDisposable(scope()) .subscribe { theme -> // Set the color for the drawer icons - val states = arrayOf(intArrayOf(android.R.attr.state_activated), + val states = arrayOf( + intArrayOf(android.R.attr.state_activated), intArrayOf(-android.R.attr.state_activated)) + resolveThemeColor(android.R.attr.textColorSecondary) .let { textSecondary -> ColorStateList(states, intArrayOf(theme.theme, textSecondary)) } .let { tintList -> @@ -175,9 +176,6 @@ class MainActivity : QkThemedActivity(), MainView { compose.setTint(theme.textPrimary) } - itemTouchCallback.adapter = conversationsAdapter - conversationsAdapter.autoScrollToStart(recyclerView) - // These theme attributes don't apply themselves on API 21 if (Build.VERSION.SDK_INT <= 22) { toolbarSearch.setBackgroundTint(resolveThemeColor(R.attr.bubbleColor)) @@ -269,10 +267,11 @@ class MainActivity : QkThemedActivity(), MainView { inbox.isActivated = state.page is Inbox 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 (drawerLayout.isDrawerOpen(GravityCompat.START) && !state.drawerOpen) { + drawerLayout.closeDrawer(GravityCompat.START) + } else if (!drawerLayout.isDrawerVisible(GravityCompat.START) && state.drawerOpen) { + drawerLayout.openDrawer(GravityCompat.START) + } when (state.syncing) { is SyncRepository.SyncProgress.Idle -> { diff --git a/presentation/src/main/res/layout/main_activity.xml b/presentation/src/main/res/layout/main_activity.xml index ab58cee80..f19d171f7 100644 --- a/presentation/src/main/res/layout/main_activity.xml +++ b/presentation/src/main/res/layout/main_activity.xml @@ -84,6 +84,7 @@ android:clipToPadding="false" android:paddingTop="8dp" android:paddingBottom="8dp" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layout_constraintBottom_toTopOf="@id/bottom" app:layout_constraintTop_toBottomOf="@id/toolbar" tools:listitem="@layout/conversation_list_item" /> -- GitLab From 6adfdfe8bd21c8b7688b366a7377b222e1a78dca Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Sun, 12 Jan 2020 18:39:12 -0500 Subject: [PATCH 090/109] Fix weird auto scrolling issue --- .../QKSMS/common/util/extensions/ViewExtensions.kt | 10 +--------- .../java/com/moez/QKSMS/feature/main/MainViewModel.kt | 2 +- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/ViewExtensions.kt b/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/ViewExtensions.kt index ab33108fd..f2bf92d17 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/ViewExtensions.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/ViewExtensions.kt @@ -120,14 +120,6 @@ fun ViewPager.addOnPageChangeListener(listener: (Int) -> Unit) { } fun RecyclerView.scrapViews() { - val adapter = adapter - val layoutManager = layoutManager - - this.adapter = null - this.layoutManager = null - - this.adapter = adapter - this.layoutManager = layoutManager - + recycledViewPool.clear() adapter?.notifyDataSetChanged() } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainViewModel.kt b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainViewModel.kt index 031f3fdcd..c041ce086 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainViewModel.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainViewModel.kt @@ -204,7 +204,7 @@ class MainViewModel @Inject constructor( prefs.keyChanges .filter { key -> key.contains("theme") } .map { true } - .mergeWith(prefs.autoColor.asObservable()) + .mergeWith(prefs.autoColor.asObservable().skip(1)) .doOnNext { view.themeChanged() } .takeUntil(view.activityResumedIntent.filter { resumed -> resumed }) } -- GitLab From f3001c66860fbad418f5f4bb6c8b6691a6f7bd47 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Sun, 12 Jan 2020 20:50:55 -0500 Subject: [PATCH 091/109] Fix QKSMS+ theme preview --- .../java/com/moez/QKSMS/feature/themepicker/HSVPickerView.kt | 4 +++- presentation/src/main/res/drawable/color_picker_gradient.xml | 3 ++- presentation/src/main/res/layout/hsv_picker_view.xml | 1 - 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/themepicker/HSVPickerView.kt b/presentation/src/main/java/com/moez/QKSMS/feature/themepicker/HSVPickerView.kt index aec322ca9..155224433 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/themepicker/HSVPickerView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/themepicker/HSVPickerView.kt @@ -33,7 +33,9 @@ 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) : ConstraintLayout(context, attrs) { +class HSVPickerView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null +) : ConstraintLayout(context, attrs) { val selectedColor: Subject = BehaviorSubject.create() diff --git a/presentation/src/main/res/drawable/color_picker_gradient.xml b/presentation/src/main/res/drawable/color_picker_gradient.xml index 74e5855c8..50f0d5cef 100644 --- a/presentation/src/main/res/drawable/color_picker_gradient.xml +++ b/presentation/src/main/res/drawable/color_picker_gradient.xml @@ -23,7 +23,8 @@ - \ No newline at end of file + diff --git a/presentation/src/main/res/layout/hsv_picker_view.xml b/presentation/src/main/res/layout/hsv_picker_view.xml index 1ae8c543d..bdc1685a7 100644 --- a/presentation/src/main/res/layout/hsv_picker_view.xml +++ b/presentation/src/main/res/layout/hsv_picker_view.xml @@ -29,7 +29,6 @@ android:layout_height="0dp" android:background="@drawable/rounded_rectangle_4dp" android:backgroundTint="@color/white" - android:rotation="90" app:layout_constraintBottom_toBottomOf="@id/saturation" app:layout_constraintEnd_toEndOf="@id/saturation" app:layout_constraintStart_toStartOf="@id/saturation" -- GitLab From 744b3d6ffbdb57e4bbb7b36ea451b7fb6e87e42f Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Sun, 12 Jan 2020 22:17:57 -0500 Subject: [PATCH 092/109] =?UTF-8?q?Don=E2=80=99t=20recreate=20existing=20n?= =?UTF-8?q?otification=20channels?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../moez/QKSMS/manager/NotificationManager.kt | 2 +- .../java/com/moez/QKSMS/common/Navigator.kt | 4 +- .../common/util/NotificationManagerImpl.kt | 101 +++++++++--------- 3 files changed, 56 insertions(+), 51 deletions(-) diff --git a/domain/src/main/java/com/moez/QKSMS/manager/NotificationManager.kt b/domain/src/main/java/com/moez/QKSMS/manager/NotificationManager.kt index 7499d82ec..2af74aa71 100644 --- a/domain/src/main/java/com/moez/QKSMS/manager/NotificationManager.kt +++ b/domain/src/main/java/com/moez/QKSMS/manager/NotificationManager.kt @@ -26,7 +26,7 @@ interface NotificationManager { fun notifyFailed(threadId: Long) - fun createNotificationChannel(threadId: Long) + fun createNotificationChannel(threadId: Long = 0L) fun buildNotificationChannelId(threadId: Long): String diff --git a/presentation/src/main/java/com/moez/QKSMS/common/Navigator.kt b/presentation/src/main/java/com/moez/QKSMS/common/Navigator.kt index 0140fa58a..d548c92db 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/Navigator.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/Navigator.kt @@ -270,8 +270,8 @@ class Navigator @Inject constructor( } val channelId = notificationManager.buildNotificationChannelId(threadId) val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS) - intent.putExtra(Settings.EXTRA_CHANNEL_ID, channelId) - intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) + .putExtra(Settings.EXTRA_CHANNEL_ID, channelId) + .putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) startActivity(intent) } } diff --git a/presentation/src/main/java/com/moez/QKSMS/common/util/NotificationManagerImpl.kt b/presentation/src/main/java/com/moez/QKSMS/common/util/NotificationManagerImpl.kt index 22b47ff54..1ba84dac3 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/util/NotificationManagerImpl.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/util/NotificationManagerImpl.kt @@ -18,7 +18,6 @@ */ package com.moez.QKSMS.common.util -import android.annotation.SuppressLint import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager @@ -80,19 +79,8 @@ class NotificationManagerImpl @Inject constructor( private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager init { - @SuppressLint("NewApi") - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val name = "Default" - val importance = NotificationManager.IMPORTANCE_HIGH - val channel = NotificationChannel(DEFAULT_CHANNEL_ID, name, importance).apply { - enableLights(true) - lightColor = Color.WHITE - enableVibration(true) - vibrationPattern = VIBRATE_PATTERN - } - - notificationManager.createNotificationChannel(channel) - } + // Make sure the default channel has been initialized + createNotificationChannel() } /** @@ -126,7 +114,8 @@ class NotificationManagerImpl @Inject constructor( val contentPI = taskStackBuilder.getPendingIntent(threadId.toInt() + 10000, PendingIntent.FLAG_UPDATE_CURRENT) val seenIntent = Intent(context, MarkSeenReceiver::class.java).putExtra("threadId", threadId) - val seenPI = PendingIntent.getBroadcast(context, threadId.toInt() + 20000, seenIntent, PendingIntent.FLAG_UPDATE_CURRENT) + val seenPI = PendingIntent.getBroadcast(context, threadId.toInt() + 20000, seenIntent, + PendingIntent.FLAG_UPDATE_CURRENT) // We can't store a null preference, so map it to a null Uri if the pref string is empty val ringtone = prefs.ringtone(threadId).get() @@ -213,13 +202,15 @@ class NotificationManagerImpl @Inject constructor( notification .setLargeIcon(avatar) .setContentTitle(conversation.getTitle()) - .setContentText(context.resources.getQuantityString(R.plurals.notification_new_messages, messages.size, messages.size)) + .setContentText(context.resources.getQuantityString( + R.plurals.notification_new_messages, messages.size, messages.size)) } Preferences.NOTIFICATION_PREVIEWS_NONE -> { notification .setContentTitle(context.getString(R.string.app_name)) - .setContentText(context.resources.getQuantityString(R.plurals.notification_new_messages, messages.size, messages.size)) + .setContentText(context.resources.getQuantityString( + R.plurals.notification_new_messages, messages.size, messages.size)) } } @@ -238,7 +229,8 @@ class NotificationManagerImpl @Inject constructor( when (action) { Preferences.NOTIFICATION_ACTION_READ -> { val intent = Intent(context, MarkReadReceiver::class.java).putExtra("threadId", threadId) - val pi = PendingIntent.getBroadcast(context, threadId.toInt() + 30000, intent, PendingIntent.FLAG_UPDATE_CURRENT) + val pi = PendingIntent.getBroadcast(context, threadId.toInt() + 30000, intent, + PendingIntent.FLAG_UPDATE_CURRENT) NotificationCompat.Action.Builder(R.drawable.ic_check_white_24dp, actionLabels[action], pi) .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ).build() } @@ -248,8 +240,10 @@ class NotificationManagerImpl @Inject constructor( getReplyAction(threadId) } else { val intent = Intent(context, QkReplyActivity::class.java).putExtra("threadId", threadId) - val pi = PendingIntent.getActivity(context, threadId.toInt() + 40000, intent, PendingIntent.FLAG_UPDATE_CURRENT) - NotificationCompat.Action.Builder(R.drawable.ic_reply_white_24dp, actionLabels[action], pi) + val pi = PendingIntent.getActivity(context, threadId.toInt() + 40000, intent, + PendingIntent.FLAG_UPDATE_CURRENT) + NotificationCompat.Action + .Builder(R.drawable.ic_reply_white_24dp, actionLabels[action], pi) .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY).build() } } @@ -258,15 +252,19 @@ class NotificationManagerImpl @Inject constructor( val address = conversation.recipients[0]?.address val intentAction = if (permissions.hasCalling()) Intent.ACTION_CALL else Intent.ACTION_DIAL val intent = Intent(intentAction, Uri.parse("tel:$address")) - val pi = PendingIntent.getActivity(context, threadId.toInt() + 50000, intent, PendingIntent.FLAG_UPDATE_CURRENT) + val pi = PendingIntent.getActivity(context, threadId.toInt() + 50000, intent, + PendingIntent.FLAG_UPDATE_CURRENT) NotificationCompat.Action.Builder(R.drawable.ic_call_white_24dp, actionLabels[action], pi) .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_CALL).build() } Preferences.NOTIFICATION_ACTION_DELETE -> { val messageIds = messages.map { it.id }.toLongArray() - val intent = Intent(context, DeleteMessagesReceiver::class.java).putExtra("threadId", threadId).putExtra("messageIds", messageIds) - val pi = PendingIntent.getBroadcast(context, threadId.toInt() + 60000, intent, PendingIntent.FLAG_UPDATE_CURRENT) + val intent = Intent(context, DeleteMessagesReceiver::class.java) + .putExtra("threadId", threadId) + .putExtra("messageIds", messageIds) + val pi = PendingIntent.getBroadcast(context, threadId.toInt() + 60000, intent, + PendingIntent.FLAG_UPDATE_CURRENT) NotificationCompat.Action.Builder(R.drawable.ic_delete_white_24dp, actionLabels[action], pi) .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_DELETE).build() } @@ -339,9 +337,11 @@ class NotificationManagerImpl @Inject constructor( private fun getReplyAction(threadId: Long): NotificationCompat.Action { val replyIntent = Intent(context, RemoteMessagingReceiver::class.java).putExtra("threadId", threadId) - val replyPI = PendingIntent.getBroadcast(context, threadId.toInt() + 40000, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT) + val replyPI = PendingIntent.getBroadcast(context, threadId.toInt() + 40000, replyIntent, + PendingIntent.FLAG_UPDATE_CURRENT) - val title = context.resources.getStringArray(R.array.notification_actions)[Preferences.NOTIFICATION_ACTION_REPLY] + val title = context.resources.getStringArray(R.array.notification_actions)[ + Preferences.NOTIFICATION_ACTION_REPLY] val responseSet = context.resources.getStringArray(R.array.qk_responses) val remoteInput = RemoteInput.Builder("body") .setLabel(title) @@ -361,27 +361,39 @@ class NotificationManagerImpl @Inject constructor( */ override fun createNotificationChannel(threadId: Long) { - // Only proceed if the android version supports notification channels - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return + // Only proceed if the android version supports notification channels, and the channel hasn't + // already been created + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || getNotificationChannel(threadId) != null) { + return + } - conversationRepo.getConversation(threadId)?.let { conversation -> - val channelId = buildNotificationChannelId(threadId) - val name = conversation.getTitle() - val importance = NotificationManager.IMPORTANCE_HIGH - val channel = NotificationChannel(channelId, name, importance).apply { + val channel = when (threadId) { + 0L -> NotificationChannel(DEFAULT_CHANNEL_ID, "Default", NotificationManager.IMPORTANCE_HIGH).apply { enableLights(true) lightColor = Color.WHITE enableVibration(true) vibrationPattern = VIBRATE_PATTERN - lockscreenVisibility = Notification.VISIBILITY_PUBLIC - setSound(prefs.ringtone().get().let(Uri::parse), AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_NOTIFICATION) - .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) - .build()) } - notificationManager.createNotificationChannel(channel) + else -> { + val conversation = conversationRepo.getConversation(threadId) ?: return + val channelId = buildNotificationChannelId(threadId) + val title = conversation.getTitle() + NotificationChannel(channelId, title, NotificationManager.IMPORTANCE_HIGH).apply { + enableLights(true) + lightColor = Color.WHITE + enableVibration(true) + vibrationPattern = VIBRATE_PATTERN + lockscreenVisibility = Notification.VISIBILITY_PUBLIC + setSound(prefs.ringtone().get().let(Uri::parse), AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_NOTIFICATION) + .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) + .build()) + } + } } + + notificationManager.createNotificationChannel(channel) } /** @@ -391,9 +403,8 @@ class NotificationManagerImpl @Inject constructor( val channelId = buildNotificationChannelId(threadId) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - return notificationManager - .notificationChannels - .firstOrNull { channel -> channel.id == channelId } + return notificationManager.notificationChannels + .find { channel -> channel.id == channelId } } return null @@ -407,13 +418,7 @@ class NotificationManagerImpl @Inject constructor( */ private fun getChannelIdForNotification(threadId: Long): String { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val channelId = buildNotificationChannelId(threadId) - - return notificationManager - .notificationChannels - .map { channel -> channel.id } - .firstOrNull { id -> id == channelId } - ?: DEFAULT_CHANNEL_ID + return getNotificationChannel(threadId)?.id ?: DEFAULT_CHANNEL_ID } return DEFAULT_CHANNEL_ID -- GitLab From d81bd039c1491ca1f09d0bfb5ed846e5af8070b3 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Sun, 12 Jan 2020 23:12:27 -0500 Subject: [PATCH 093/109] Leave autoColor on for everyone --- .../src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt b/data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt index 6e19486fe..f9f99e8b7 100644 --- a/data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt +++ b/data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt @@ -190,9 +190,6 @@ class QkRealmMigration @Inject constructor( prefs.theme(recipientId).set(theme) } - // This is enabled for new users, but the behaviour shouldn't change automatically for old users - prefs.autoColor.set(false) - version++ } -- GitLab From b68f820f5dd5e12c2d06e2e8f1755af4802839e6 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Sun, 12 Jan 2020 23:30:59 -0500 Subject: [PATCH 094/109] Fix 3.8 migration --- data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt b/data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt index f9f99e8b7..74f3ad66d 100644 --- a/data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt +++ b/data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt @@ -178,7 +178,7 @@ class QkRealmMigration @Inject constructor( realm.where("Conversation").findAll().forEach { conversation -> val pref = prefs.theme(conversation.getLong("id")) if (pref.isSet) { - conversation.getList("Recipient").forEach { recipient -> + conversation.getList("recipients").forEach { recipient -> recipients[recipient.getLong("id")] = pref.get() } -- GitLab From b6e1c3a181f615b068e89aadf5b40adabceaa927 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Sun, 12 Jan 2020 23:37:17 -0500 Subject: [PATCH 095/109] Set user property for autoColor pref --- .../feature/settings/SettingsPresenter.kt | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsPresenter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsPresenter.kt index e42634e4c..fc653e29c 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsPresenter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsPresenter.kt @@ -27,6 +27,7 @@ import com.moez.QKSMS.common.util.Colors import com.moez.QKSMS.common.util.DateFormatter import com.moez.QKSMS.common.util.extensions.makeToast import com.moez.QKSMS.interactor.SyncMessages +import com.moez.QKSMS.manager.AnalyticsManager import com.moez.QKSMS.repository.SyncRepository import com.moez.QKSMS.util.NightModeManager import com.moez.QKSMS.util.Preferences @@ -39,15 +40,16 @@ import java.util.concurrent.TimeUnit import javax.inject.Inject class SettingsPresenter @Inject constructor( - colors: Colors, - syncRepo: SyncRepository, - private val context: Context, - private val billingManager: BillingManager, - private val dateFormatter: DateFormatter, - private val navigator: Navigator, - private val nightModeManager: NightModeManager, - private val prefs: Preferences, - private val syncMessages: SyncMessages + colors: Colors, + syncRepo: SyncRepository, + private val analytics: AnalyticsManager, + private val context: Context, + private val billingManager: BillingManager, + private val dateFormatter: DateFormatter, + private val navigator: Navigator, + private val nightModeManager: NightModeManager, + private val prefs: Preferences, + private val syncMessages: SyncMessages ) : QkPresenter(SettingsState( nightModeId = prefs.nightMode.get() )) { @@ -184,7 +186,10 @@ class SettingsPresenter @Inject constructor( R.id.textSize -> view.showTextSizePicker() - R.id.autoColor -> prefs.autoColor.set(!prefs.autoColor.get()) + R.id.autoColor -> { + analytics.setUserProperty("Preference: Auto Color", !prefs.autoColor.get()) + prefs.autoColor.set(!prefs.autoColor.get()) + } R.id.systemFont -> prefs.systemFont.set(!prefs.systemFont.get()) -- GitLab From 148eb7f513b34107ff0db28f796d4df58a9c19ba Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Sun, 12 Jan 2020 23:39:20 -0500 Subject: [PATCH 096/109] Update translations --- .../src/main/res/values-ar/strings.xml | 26 +- .../src/main/res/values-bn/strings.xml | 26 +- .../src/main/res/values-cs/strings.xml | 26 +- .../src/main/res/values-da/strings.xml | 26 +- .../src/main/res/values-de/strings.xml | 14 +- .../src/main/res/values-el/strings.xml | 14 +- .../src/main/res/values-es/strings.xml | 30 +- .../src/main/res/values-fa/strings.xml | 26 +- .../src/main/res/values-fi/strings.xml | 21 +- .../src/main/res/values-fr/strings.xml | 26 +- .../src/main/res/values-hi/strings.xml | 34 +- .../src/main/res/values-hr/strings.xml | 14 +- .../src/main/res/values-hu/strings.xml | 36 +- .../src/main/res/values-in/strings.xml | 18 +- .../src/main/res/values-it/strings.xml | 22 +- .../src/main/res/values-iw/strings.xml | 14 +- .../src/main/res/values-ja/strings.xml | 16 +- .../src/main/res/values-ko/strings.xml | 112 ++--- .../src/main/res/values-lt/strings.xml | 19 +- .../src/main/res/values-nb/strings.xml | 26 +- .../src/main/res/values-ne/strings.xml | 14 +- .../src/main/res/values-nl/strings.xml | 16 +- .../src/main/res/values-pl/strings.xml | 20 +- .../src/main/res/values-pt-rBR/strings.xml | 21 +- .../src/main/res/values-pt/strings.xml | 16 +- .../src/main/res/values-ro/strings.xml | 14 +- .../src/main/res/values-ru/strings.xml | 24 +- .../src/main/res/values-sk/strings.xml | 16 +- .../src/main/res/values-sl/strings.xml | 407 ++++++++++++++++++ .../src/main/res/values-sr/strings.xml | 14 +- .../src/main/res/values-sv/strings.xml | 62 +-- .../src/main/res/values-th/strings.xml | 20 +- .../src/main/res/values-tl/strings.xml | 14 +- .../src/main/res/values-tr/strings.xml | 24 +- .../src/main/res/values-uk/strings.xml | 21 +- .../src/main/res/values-ur/strings.xml | 14 +- .../src/main/res/values-vi/strings.xml | 18 +- .../src/main/res/values-zh-rCN/strings.xml | 80 ++-- .../src/main/res/values-zh/strings.xml | 30 +- 39 files changed, 1117 insertions(+), 274 deletions(-) create mode 100644 presentation/src/main/res/values-sl/strings.xml diff --git a/presentation/src/main/res/values-ar/strings.xml b/presentation/src/main/res/values-ar/strings.xml index 8b54784d0..df30ec316 100644 --- a/presentation/src/main/res/values-ar/strings.xml +++ b/presentation/src/main/res/values-ar/strings.xml @@ -30,9 +30,11 @@ أكتب اسماً أو رقماً تخطي متابعة + Add person اتصال تفاصيل حفظ إلى المعرض + Share فتح درج التنقل %d محددة مسح @@ -83,6 +85,10 @@ إعادة توجيه حذف + Choose a phone number + %s ∙ Default + Just once + Always %d مختارة %1$d من %2$d النتائج إرسال رسالة جماعية @@ -122,6 +128,7 @@ تم إرسال %s فشل الإرسال. إلمس لإعادة المحاولة التفاصيل + Address copied عنوان المحادثة الإشعارات السمة @@ -186,6 +193,7 @@ وضع ليلي بسوادٍ حالك وقت البدء وقت الانتهاء + Automatic contact colors حجم الخط استخدم خط النظام إيموجي تلقائية @@ -196,6 +204,7 @@ الزر 2 الزر 3 معاينات الإشعارات + Wake screen الاهتزاز الصوت بدون نغمة @@ -225,6 +234,8 @@ حذف حركات التشكيل من المحارف في الرسائل النصية الصادرة أرقام الهواتف النقالة فقط عند إنشاء رسالة، أظهر فقط أرقام الهواتف النقالة + Send long messages as MMS + If your longer text messages are failing to send, or sending in the wrong order, you can send them as MMS messages instead. Additional charges may apply الضغط التلقائي لمرفقات رسائل الوسائط مزامنة الرسائل إعادة مزامنة رسائلك مع قاعدة البيانات الأصلية للرسائل في النظام @@ -373,13 +384,14 @@ طويل - ١٠٠ ك.ب. - ٢٠٠ ك.ب. - ٣٠٠ ك.ب. (مستحسن) - ٦٠٠ ك.ب. - ١٠٠٠ ك.ب. - ٢٠٠٠ ك.ب. - دون ضغط + Automatic + 100KB + 200KB + 300KB + 600KB + 1000KB + 2000KB + No compression حسنًا diff --git a/presentation/src/main/res/values-bn/strings.xml b/presentation/src/main/res/values-bn/strings.xml index 0eb38018d..5a15bf69b 100644 --- a/presentation/src/main/res/values-bn/strings.xml +++ b/presentation/src/main/res/values-bn/strings.xml @@ -30,9 +30,11 @@ নাম বা নম্বর টাইপ করুন এড়িয়ে যান চালিয়ে যান + Add person কল করুন বিস্তারিত গ্যালারীতে সেভ করুন + Share নেভিগেশন ড্রয়ার খুলুন নির্বাচিত %d ক্লিয়ার @@ -83,6 +85,10 @@ সামনে পাঠান মুছে ফেলুন + Choose a phone number + %s ∙ Default + Just once + Always নির্বাচিত %d %2$d এর %1$d ফলাফল গ্রুপ বার্তা হিসেবে প্রেরণ করুন @@ -122,6 +128,7 @@ প্রেরিত হয়েছে: %s পাঠাতে ব্যর্থ. আবার চেষ্টা করতে ট্যাপ করুন বিস্তারিত + Address copied কথোপকথনের শিরোনাম নোটিফিকেশন থিম @@ -183,6 +190,7 @@ সম্পূর্ণ কালো রাত্রি মোড শুরুর সময় শেষের সময় + Automatic contact colors ফন্টের আকার সিস্টেম ফন্ট ব্যবহার করুন স্বয়ংক্রিয় ইমোজি @@ -193,6 +201,7 @@ বাটন ২ বাটন ৩ নোটিফিকেশন প্রিভিউ + Wake screen কম্পন শব্দ কোনোটা না @@ -222,6 +231,8 @@ বহির্গামী এসএমএস বার্তার অক্ষর থেকে কথার টান সরিয়ে দিন শুধু মোবাইল নম্বর বার্তা লিখার করার সময়, শুধুমাত্র মোবাইল নম্বর দেখান + Send long messages as MMS + If your longer text messages are failing to send, or sending in the wrong order, you can send them as MMS messages instead. Additional charges may apply এমএমএসের সংযুক্তি সক্রিয়ভাবে সংকোচন করুন বার্তাগুলি সিঙ্ক করুন আপনার বার্তাগুলি অ্যান্ড্রয়েডের নিজস্ব এসএমএস এর ডাটাবেস সাতে পুনরায় সিঙ্ক করুন @@ -357,13 +368,14 @@ দীর্ঘ - ১০০KB - ২০০KB - ৩০০KB (সুপারিশ করা) - ৬০০KB - ১০০০KB - ২০০০KB - সংকোচনহীন + Automatic + 100KB + 200KB + 300KB + 600KB + 1000KB + 2000KB + No compression ঠিক আছে diff --git a/presentation/src/main/res/values-cs/strings.xml b/presentation/src/main/res/values-cs/strings.xml index 674b07e0e..3f2e4016b 100644 --- a/presentation/src/main/res/values-cs/strings.xml +++ b/presentation/src/main/res/values-cs/strings.xml @@ -30,9 +30,11 @@ Nová zpráva Přeskočit Pokračovat + Přidat osobu Volat Podrobnosti Uložit do galerie + Sdílet Otevřít menu Vybráno: %d Vymazat @@ -85,6 +87,10 @@ Přeposlat Smazat + Vybrat telefonní číslo + %s ∙ Výchozí + Pouze jednou + Vždy Vybráno: %d %1$d z %2$d výsledků Odeslat jako skupinovou zprávu @@ -124,6 +130,7 @@ Doručeno %s Odeslání se nezdařilo. Klepnutím zkusíte znovu odeslat Podrobnosti + Adresa zkopírována Název konverzace Oznámení Motiv vzhledu @@ -186,6 +193,7 @@ Čistá černá, noční režim Čas zahájení Čas ukončení + Automatické barvy kontaktu Velikost písma Použít systémové písmo Automatické emoji @@ -196,6 +204,7 @@ Tlačítko 2 Tlačítko 3 Nastavení oznámení + Rozsvítit displej Vibrace Vyzvánění Žádný @@ -225,6 +234,8 @@ Odstranit diakritiku v odesílaných SMS zprávách Pouze mobilní čísla Při psaní zprávy zobrazit pouze mobilní telefonní čísla + Odesílat dlouhé zprávy jako MMS + Nedaří-li se odeslat vaše dlouhé zprávy, nebo jsou-li poslány v nesprávném pořadí, lze je odeslat jako MMS. Mohou být účtovány další poplatky. Automaticky komprimovat MMS přílohy Synchronizovat zprávy Opětovná synchronizace zpráv s nativní Android SMS databází @@ -366,13 +377,14 @@ Dlouhá - 100 kB - 200 kB - 300 kB (doporučeno) - 600 kB - 1 000 kB - 2 000 kB - Bez komprese + Automatic + 100KB + 200KB + 300KB + 600KB + 1000KB + 2000KB + No compression OK diff --git a/presentation/src/main/res/values-da/strings.xml b/presentation/src/main/res/values-da/strings.xml index d4bb129b1..880643da6 100644 --- a/presentation/src/main/res/values-da/strings.xml +++ b/presentation/src/main/res/values-da/strings.xml @@ -30,9 +30,11 @@ Angiv et navn eller nummer Overspring Fortsæt + Tilføj person Opkald Oplysninger Gem i Galleri + Del Åbn navigeringsskuffe %d valgt Ryd @@ -79,6 +81,10 @@ Videresend Slet + Vælg et telefonnr. + %s ∙ Standard + Kun én gang + Altid %d valgt %1$d af %2$d resultater Send som gruppe besked @@ -118,6 +124,7 @@ Leveret %s Mislykkedes at afsende. Tryk for at forsøge igen Oplysninger + Adresse kopieret Samtaletitel Notifikationer Tema @@ -178,6 +185,7 @@ Ren sort nattilstand Starttidspunkt Sluttidspunkt + Automatiske kontaktfarver Skriftstørrelse Brug systemets skrifttype Automatisk emoji @@ -188,6 +196,7 @@ Knap 2 Knap 3 Notifikationseksempler + Væk skærm Vibration Lyd Ingen @@ -217,6 +226,8 @@ Fjern betoninger fra tegn i udgående SMS\'er Kun mobilnumre Vis kun mobilnumre ved SMS-skrivning, + Send lang SMS som MMS + Hvis dine længere SMS\'er ikke afsendes eller afsendes i forkert rækkefølge, kan du i stedet sende dem som MMS\'er. Ekstragebyrer kan påløbe Auto-komprimér MMS-vedhæftninger Beskedsynkronisering Gen-synkroniéer beskeder med den indbyggede Android SMS-database @@ -352,13 +363,14 @@ Lang - 100 KB - 200 KB - 300 KB (anbefales) - 600 KB - 1.000 KB - 2.000 KB - Ingen komprimering + Automatisk + 100KB + 200KB + 300KB + 600KB + 1MB + 2MB + Ingen kompression OK diff --git a/presentation/src/main/res/values-de/strings.xml b/presentation/src/main/res/values-de/strings.xml index b296922a2..9c07f6930 100644 --- a/presentation/src/main/res/values-de/strings.xml +++ b/presentation/src/main/res/values-de/strings.xml @@ -30,9 +30,11 @@ Verfassen Überspringen Weiter + Person hinzufügen Anrufen Details In Galerie speichern + Teilen Öffne den Navigation Drawer %d ausgewählt Zurücksetzen @@ -83,6 +85,10 @@ Weiterleiten Löschen + Wählen Sie eine Telefonnummer + %s - Standard + Nur einmal + Immer %d ausgewählt %1$d von %2$d Ergebnissen Als Gruppennachricht senden @@ -122,6 +128,7 @@ Zugestellt %s Fehler beim Senden. Tippen, um es erneut zu versuchen Details + Adresse kopiert Unterhaltungstitel Benachrichtigungen Erscheinungsbild @@ -182,6 +189,7 @@ Rein schwarzer Nachtmodus Anfang Ende + Automatische Kontaktfarben Schriftgröße Systemschriftart verwenden Automatische Emoji @@ -192,6 +200,7 @@ Taste 2 Taste 3 Benachrichtigungsvorschau + Bildschirm aufwecken Vibration Ton Keine @@ -221,6 +230,8 @@ Akzente aus Zeichen in ausgehenden SMS-Nachrichten entfernen Nur Mobilfunknummern Beim Verfassen einer Nachricht nur Mobilfunknummern anzeigen + Lange Nachrichten als MMS senden + Wenn Ihre längeren Textnachrichten nicht gesendet oder in der falsche Reihenfolge gesendet werden, können Sie sie stattdessen als MMS-Nachrichten senden. Dafür können zusätzliche Gebühren anfallen. MMS-Anhänge automatisch komprimieren Nachrichten synchronisieren Nachrichten mit der Android SMS-Datenbank abgleichen @@ -356,9 +367,10 @@ Lang + Automatisch 100KB 200KB - 300KB (empfohlen) + 300KB 600KB 1000KB 2000KB diff --git a/presentation/src/main/res/values-el/strings.xml b/presentation/src/main/res/values-el/strings.xml index 8b9ddea47..8db4d043f 100644 --- a/presentation/src/main/res/values-el/strings.xml +++ b/presentation/src/main/res/values-el/strings.xml @@ -30,9 +30,11 @@ Πληκτρολογήστε ένα όνομα ή αριθμό Παράλειψη Συνέχεια + Add person Κλήση Λεπτομέρειες Αποθήκευση στη συλλογή + Share Άνοιγμα πλαισίου πλοήγησης Επιλεγμένα: %d Εκκαθάριση @@ -79,6 +81,10 @@ Προώθηση Διαγραφή + Choose a phone number + %s ∙ Default + Just once + Always Επιλέχθηκαν: %d %1$d από %2$d αποτελέσματα Αποστολή ως ομαδικό μήνυμα @@ -119,6 +125,7 @@ Παραδόθηκε: %s Αποτυχία αποστολής. Πατήστε για νέα προσπάθεια Λεπτομέρειες + Address copied Τίτλος συζήτησης Ειδοποιήσεις Θέμα εμφάνισης @@ -180,6 +187,7 @@ Νυχτερινή λειτουργία καθαρού μαύρου Ώρα έναρξης Ώρα λήξης + Automatic contact colors Μέγεθος γραμματοσειράς Χρήση γραμματοσειράς συστήματος Αυτόματα emoji @@ -190,6 +198,7 @@ Κουμπί 2 Κουμπί 3 Προεπισκοπήσεις ειδοποιήσεων + Wake screen Δόνηση Ήχος Κανένα @@ -219,6 +228,8 @@ Remove accents from characters in outgoing SMS messages Mobile numbers only When composing a message, only show mobile numbers + Send long messages as MMS + If your longer text messages are failing to send, or sending in the wrong order, you can send them as MMS messages instead. Additional charges may apply Auto-compress MMS attachments Sync messages Re-sync your messages with the native Android SMS database @@ -354,9 +365,10 @@ Long + Automatic 100KB 200KB - 300KB (Recommended) + 300KB 600KB 1000KB 2000KB diff --git a/presentation/src/main/res/values-es/strings.xml b/presentation/src/main/res/values-es/strings.xml index 0e7fc7c3c..41fb51933 100644 --- a/presentation/src/main/res/values-es/strings.xml +++ b/presentation/src/main/res/values-es/strings.xml @@ -30,9 +30,11 @@ Escriba un nombre o número Omitir Continuar + Add person Llamar Detalles Guardar en la galería + Share Abrir cajón de navegación %d Seleccionados Limpiar @@ -79,6 +81,10 @@ Reenviar Borrar + Choose a phone number + %s ∙ Default + Just once + Siempre %d seleccionados %1$d de %2$d resultados Enviar como mensaje de grupo @@ -118,6 +124,7 @@ Entregado %s No se envió. Pulse para volver a intentarlo Detalles + Address copied Título de la conversación Notificaciones Tema @@ -168,7 +175,7 @@ Enviar ahora Copy text - Delete + Borrar Apariencia General @@ -178,6 +185,7 @@ Modo noche negro puro Hora de inicio Hora de finalización + Automatic contact colors Tamaño de la fuente Usar fuente del sistema Emojis automáticos @@ -188,6 +196,7 @@ Botón 2 Botón 3 Previsualización de notificaciones + Wake screen Vibración Sonido Ninguno @@ -217,6 +226,8 @@ Quitar acentos de caracteres en los mensajes SMS salientes Sólo números móviles Al componer un mensaje, sólo mostrar números móviles + Send long messages as MMS + If your longer text messages are failing to send, or sending in the wrong order, you can send them as MMS messages instead. Additional charges may apply Autocomprimir archivos adjuntos MMS Sincronizar mensajes Vuelve a sincronizar tus mensajes con la base de datos nativa de Android SMS @@ -311,8 +322,8 @@ Llamar Eliminar - Yes - Continue + + Continuar Cancelar Eliminar Guardar @@ -329,10 +340,10 @@ Mensaje no enviado El mensaje a %s no se pudo enviar - System - Disabled - Always on - Automatic + Sistema + Desactivado + Siempre activado + Automático Mostrar nombre y el mensaje @@ -352,13 +363,14 @@ Largo + Automatic 100KB 200KB - 300KB (recomendado) + 300KB 600KB 1000KB 2000KB - Sin compresión + No compression Ok diff --git a/presentation/src/main/res/values-fa/strings.xml b/presentation/src/main/res/values-fa/strings.xml index 4b638aa9e..239362ec8 100644 --- a/presentation/src/main/res/values-fa/strings.xml +++ b/presentation/src/main/res/values-fa/strings.xml @@ -30,9 +30,11 @@ یک نام یا شماره را وارد کنید بیخیال ادامه + Add person تماس جزئیات ذخیره در گالری + Share بازکردن کشو ناوبری %d انتخاب شده پاکسازی @@ -81,6 +83,10 @@ فوروارد حذف + Choose a phone number + %s ∙ Default + Just once + Always %d انتخاب شده %1$d of %2$d results ارسال پیام به گروه @@ -120,6 +126,7 @@ رسیده %s در ارسال خطایی رخ داد. برای ارسال دوباره، ضربه بزنید جزئیات + Address copied عنوان گفتگو اعلانها پوسته @@ -180,6 +187,7 @@ حالت شب کامل زمان شروع زمان پایان + Automatic contact colors اندازه قلم از فونت سیستم استفاده کن ایموجی خودکار @@ -190,6 +198,7 @@ Button 2 Button 3 بازبینی اعلان + Wake screen لرزش صدای هیچی @@ -219,6 +228,8 @@ حذف کاراکترهای اضافه پیام ارسالی فقط شماره های تلفن وقتی در حال ارسال پیام هستید فقط می توانید شماره تلفن را ببنید + Send long messages as MMS + If your longer text messages are failing to send, or sending in the wrong order, you can send them as MMS messages instead. Additional charges may apply فشرده سازی خودکار پیوست ها همگام سازی پیام ها همگام سازی پیام ها با پایگاه داده @@ -354,13 +365,14 @@ طولانی - ۱۰۰KB - ۲۰۰KB - 300 کیلوبایت(توصیه شده) - ۶۰۰KB - ۱۰۰۰KB - ۲۰۰۰KB - بدون فشردگی + Automatic + 100KB + 200KB + 300KB + 600KB + 1000KB + 2000KB + No compression باشه diff --git a/presentation/src/main/res/values-fi/strings.xml b/presentation/src/main/res/values-fi/strings.xml index 2d6d13a08..39f8c3da3 100644 --- a/presentation/src/main/res/values-fi/strings.xml +++ b/presentation/src/main/res/values-fi/strings.xml @@ -30,9 +30,11 @@ Kirjoita nimi tai numero Ohita Jatka + Add person Soita Lisätiedot Tallenna galleriaan + Share Avaa navigointilaatikko %d valittu Tyhjennä @@ -83,6 +85,10 @@ Edelleenlähetä Poista + Choose a phone number + %s ∙ Default + Just once + Always %d valittu %1$d / %2$d tuloksesta Lähetä ryhmäviestinä @@ -91,7 +97,6 @@ Käyntikortti Ajoitettu Valitun ajan täytyy olla tulevaisuudessa! - Sinun täytyy avata QKSMS+ ajoittaaksesi viestejä Viesti ajoitettu Kirjoita viesti… Kopioi teksti @@ -123,6 +128,7 @@ Toimitettu: %s Lähettäminen epäonnistui. Yritä uudelleen napauttamalla Lisätiedot + Address copied Keskustelun otsikko Ilmoitukset Teema @@ -141,7 +147,6 @@ Ei koskaan Palauta Valitse varmuuskopio - Ole hyvä ja avaa QKSMS+ käyttääksesi varmuuskopiointia ja palautusta Varmuuskopiointi käynnissä… Palautus käynnissä… Palauta varmuuskopio @@ -184,6 +189,7 @@ Musta yötilä Aloitusaika Lopetusaika + Automatic contact colors Fonttikoko Käytä järjestelmäfonttia Automaattinen emoji @@ -194,6 +200,7 @@ Nappi 2 Nappi 3 Esikatselut ilmoituksessa + Wake screen Värinä Ääni Ei mitään @@ -223,6 +230,8 @@ Käytä yksinkertaisia merkkejä lähtevissä SMS-viesteissä Vain matkapuhelinnumerot When composing a message, only show mobile numbers + Send long messages as MMS + If your longer text messages are failing to send, or sending in the wrong order, you can send them as MMS messages instead. Additional charges may apply Pakkaa MMS-liitteet automaattisesti Synkronoi viestit Re-sync your messages with the native Android SMS database @@ -262,10 +271,7 @@ Tietoja Versio - Kehittäjä Lähdekoodi - Muutosloki - Yhteystiedot Lisenssi Tekijänoikeus Tue kehitystä, avaa kaikki @@ -358,13 +364,14 @@ Pitkä + Automatic 100KB 200KB - 300KB (Suositeltu) + 300KB 600KB 1000KB 2000KB - Ei pakkausta + No compression Kyllä diff --git a/presentation/src/main/res/values-fr/strings.xml b/presentation/src/main/res/values-fr/strings.xml index 9db9cf359..880402a29 100644 --- a/presentation/src/main/res/values-fr/strings.xml +++ b/presentation/src/main/res/values-fr/strings.xml @@ -28,9 +28,11 @@ Entrer un nom ou un numéro Ignorer Continuer + Ajouter une personne Appeler Détails Enregistrer dans la galerie + Partager Ouvrir le tiroir de navigation %d sélectionnée Effacer @@ -81,6 +83,10 @@ Transférer Supprimer + Choisir un numéro de téléphone + %s • Défaut + Une seule fois + Toujours %d sélectionnée %1$d sur %2$d résultats Envoyer en tant que message groupé @@ -120,6 +126,7 @@ Délivré %s Échec de l\'envoi. Appuyer pour réessayer Détails + Adresse copiée Titre de la conversation Notifications Thème @@ -180,6 +187,7 @@ Mode noir pur Heure de début Heure de fin + Couleurs de contact automatiques Taille de la police Utiliser la police du système Émoticône automatique @@ -190,6 +198,7 @@ Bouton 2 Bouton 3 Aperçus de notification + Réveiller l\'écran Vibration Son Aucun @@ -219,6 +228,8 @@ Supprimer les accents des caractères lors de l\'envoi de messages SMS Numéros de mobile Lorsque vous composez un message, afficher uniquement les numéros de mobile + Envoyer les messages longs en MMS + Si vos messages texte plus longs ne parviennent pas à être envoyés, ou s\'ils sont envoyés dans le désordre, vous pouvez les envoyer sous forme de messages MMS à la place. Des frais supplémentaires peuvent s\'appliquer Compresser automatiquement les pièces jointes des MMS Synchroniser les messages Resynchroniser les messages avec la base de données Android native @@ -323,13 +334,14 @@ Long - 100 Ko - 200 Ko - 300 Ko (recommandé) - 600 Ko - 1 000 Ko - 2 000 Ko - Aucune compression + Automatique + 100KB + 200KB + 300KB + 1000KB + 1000KB + 2000KB + Pas de compression OK diff --git a/presentation/src/main/res/values-hi/strings.xml b/presentation/src/main/res/values-hi/strings.xml index ca414ee59..667ca706a 100644 --- a/presentation/src/main/res/values-hi/strings.xml +++ b/presentation/src/main/res/values-hi/strings.xml @@ -30,9 +30,11 @@ रचना छोड़ जारी रखें + Add person कॉल विवरण गैलरी में सहेजें + Share खुला नेविगेशन दराज %d चयनित साफ करें @@ -79,8 +81,15 @@ अग्रेषित करें मिटाएँ - > + + + + Choose a phone number + %s ∙ Default + Just once + Always + %d चयनित %1$d के %2$d परिणाम समूह संदेश के रूप में भेजें @@ -89,7 +98,6 @@ संपर्क कार्ड के लिए निर्धारित चयनित समय भविष्य में होना चाहिए! - आप QKSMS + अनुसूचित संदेश का उपयोग करने के लिए अनलॉक करना होगा अनुसूचित संदेशों में जोड़ा गया संदेश लिखें पाठ की प्रतिलिपि बनाएं @@ -121,6 +129,7 @@ %s वितरित भेजने में असफल। पुनः प्रयास करें विवरण + Address copied Conversation title अधिसूचनाएँ थीम @@ -139,7 +148,6 @@ कभी नहीं Restore किसी बैकअप का चयन करें - Please unlock QKSMS+ to use backup and restore Backup in progress… Restore in progress… Restore from backup @@ -182,6 +190,7 @@ शुद्ध काली रात अंदाज़ शुरुआत समय अंत समय + Automatic contact colors लिखाई का आकर यंत्र ध्वनियां उपयोग करें Automatic emoji @@ -192,6 +201,7 @@ बटन दो बटन तीन अधिसूचना सेटिंग्स + Wake screen कंपन ध्वनियाँ कुछ नहीं @@ -221,6 +231,8 @@ Remove accents from characters in outgoing SMS messages Mobile numbers only When composing a message, only show mobile numbers + Send long messages as MMS + If your longer text messages are failing to send, or sending in the wrong order, you can send them as MMS messages instead. Additional charges may apply Auto-compress MMS attachments Sync messages Re-sync your messages with the native Android SMS database @@ -260,10 +272,7 @@ About संस्करण - Developer श्रोत कोड - बदलाव - संपर्क लाइसेंस Support development, unlock everything You can save a starving developer for just %s @@ -355,12 +364,13 @@ बड़ा - 100 किलो बाइट - 200 किलो बाइट - 300 किलो बाइट(सुझाव) - 600 किलो बाइट - 1000 किलो बाइट - 2000 किलो बाइट + Automatic + 100KB + 200KB + 300KB + 600KB + 1000KB + 2000KB No compression diff --git a/presentation/src/main/res/values-hr/strings.xml b/presentation/src/main/res/values-hr/strings.xml index 40f62f5cd..cefdfab59 100644 --- a/presentation/src/main/res/values-hr/strings.xml +++ b/presentation/src/main/res/values-hr/strings.xml @@ -30,9 +30,11 @@ Unesite ime ili broj Preskoči Nastavi + Add person Nazovi Detalji Save to gallery + Share Open navigation drawer %d odabrano Očisti @@ -80,6 +82,10 @@ Proslijedi Izbriši + Choose a phone number + %s ∙ Default + Just once + Always %d odabrano %1$d of %2$d results Send as group message @@ -119,6 +125,7 @@ Isporučeno %s Slanje nije uspjelo. Dodirnite da biste pokušali ponovno Detalji + Address copied Conversation title Obavijesti Tema @@ -180,6 +187,7 @@ Potpuno crni noćni način Vrijeme početka Vrijeme završetka + Automatic contact colors Veličina fonta Koristi sustavski font Automatsk emoji @@ -190,6 +198,7 @@ Button 2 Button 3 Pregledi obavijesti + Wake screen Vibracija Zvuk None @@ -219,6 +228,8 @@ Uklonite naglaske sa znakova u odlaznim SMS porukama Mobile numbers only When composing a message, only show mobile numbers + Send long messages as MMS + If your longer text messages are failing to send, or sending in the wrong order, you can send them as MMS messages instead. Additional charges may apply Auto-compress MMS attachments Uskladi poruke Ponovno uskladite svoje poruke sa nativnom Android SMS bazom podataka @@ -357,9 +368,10 @@ Long + Automatic 100KB 200KB - 300KB (Recommended) + 300KB 600KB 1000KB 2000KB diff --git a/presentation/src/main/res/values-hu/strings.xml b/presentation/src/main/res/values-hu/strings.xml index 0fb315d44..d59221e5b 100644 --- a/presentation/src/main/res/values-hu/strings.xml +++ b/presentation/src/main/res/values-hu/strings.xml @@ -30,16 +30,18 @@ Adj meg egy nevet vagy telefonszámot Kihagy Tovább + Személy hozzáadása Hívás Részletek Mentés a galériába + Megosztás Navigációs kiválasztása %d kijelölve Törlés Archiválás Archiválás visszavonása Törlés - Add to contacts + Hozzáadás az ismerősökhöz Rögzítés a tetejére Rögzítés törlése Megjelölés olvasottként @@ -47,7 +49,7 @@ Tiltás Üzenetek szinkronizálása… Te: %s - Draft: %s + Vázlat: %s Eredmények az üzenetek szövegében %d üzenet A beszélgetéseid itt jelennek meg @@ -81,6 +83,10 @@ Továbbítás Törlés + Válassz egy telefonszámot + %s · Alapértelmezett + Csak egyszer + Mindig %d kijelölve %1$d. a %2$d találatból Küldés csoportos üzenetként @@ -120,6 +126,7 @@ Kézbesítve %s Sikertelen küldés. Érintsd meg az újraküldéshez Részletek + Cím átmásolva A beszélgetés témája Értesítések Téma @@ -170,8 +177,8 @@ Időzített üzenet Küldés most - Copy text - Delete + Szöveg másolása + Törlés Megjelenés Általános @@ -181,6 +188,7 @@ Éjfekete mód Kezdés ideje Befejezés ideje + Ismerősök automatikus színezése Betűméret Rendszer betűtípus visszaállítása Automatikus emoji @@ -191,6 +199,7 @@ Gomb 2 Gomb 3 Értesítés előnézet + Képernyő felébresztése Rezgés Hang Nincs @@ -215,11 +224,13 @@ Elküldés megerősítése Megerősíti, hogy az üzenetek sikeresen el lettek-e küldve Aláírás - Add a signature to the end of your messages + Aláírás az üzenet végére Ékezetek törlése A kimenő SMS-üzenetek ékezetes karaktereinek átalakítása Csak mobilszámok Üzenet írásánál csak a mobilszámokat mutassa + A hosszú üzenetek MMS-ként küldése + Ha a hosszabb üzenetek küldése sikertelen, vagy rossz sorrendben érkeznek akkor lehet őket MMS-ként küldeni, de ez díjköteles lehet MMS-mellékletek automatikus tömörítése Üzenetek szinkronizálása Üzenetek újra-szinkronizálása a natív Android SMS-adatbázissal @@ -228,7 +239,7 @@ Hibanaplózás engedélyezve Hibanaplózás tiltva Nyomva tartás ideje (másodpercben) - Blocking + Blokkolás Üzenetek eldobása Üzenetek eldobása a blokkolt küldőktől az elrejtésük helyett Tiltott társalgások @@ -244,11 +255,11 @@ Új szám tiltása Üzenetek tiltása: Telefonszám - Block + Blokkolás Tiltott üzenetek A tiltott üzenetek itt fognak megjelenni - Block - Unblock + Blokkolás + Visszaengedélyezés %s és ennek a számnak a blokkolása %s és ezeknek a számoknak a tiltása @@ -314,7 +325,7 @@ Törlés Igen - Continue + Folytatás Mégsem Törlés Mentés @@ -355,13 +366,14 @@ Hosszú + Automatic 100KB 200KB - 300KB (Javsolt) + 300KB 600KB 1000KB 2000KB - Nincs tömörítés + No compression Oké diff --git a/presentation/src/main/res/values-in/strings.xml b/presentation/src/main/res/values-in/strings.xml index a07b8f2cf..a3e8b1f69 100644 --- a/presentation/src/main/res/values-in/strings.xml +++ b/presentation/src/main/res/values-in/strings.xml @@ -30,9 +30,11 @@ Ketik nama atau nomor Lewati Lanjut + Add person Panggil Detail Simpan ke galeri + Berbagi Buka laci navigasi %d dipilih Bersihkan @@ -80,6 +82,10 @@ Teruskan Hapus + Pilih nomor telepon + %s ∙ Default + Hanya sekali + Selalu %d dipilih %1$d dari %2$d hasil Kirim sebagai pesan grup @@ -119,6 +125,7 @@ %s terkirim Gagal mengirim. Ketuk untuk mencoba lagi Detail + Alamat disalin Judul percakapan Notifikasi Tema @@ -179,6 +186,7 @@ Mode malam hitam pekat Waktu mulai Waktu berakhir + Warna kontak otomatis Ukuran fon Gunakan fon sistem Emoji otomatis @@ -189,6 +197,7 @@ Tombol 2 Tombol 3 Pratinjau notifikasi + Layar ambien Getar Suara Tidak ada @@ -213,11 +222,13 @@ Konfirmasi pengiriman Mengonfirmasi bahwa pesan telah berhasil dikirim Tanda tangan - Add a signature to the end of your messages + Tambahkan tanda tangan di akhir pesan anda Hapus aksen Membuang aksen dari karakter di dalam pesan SMS keluar Hanya nomor seluler Saat menulis pesan, hanya tampilkan nomor seluler + Kirim pesan panjang sebagai MMS + Jika pesan teks panjang anda gagal dikirim, atau dikirim dalam urutan yang salah, anda bisa mengirimkannya sebagai pesan MMS. Mungkin akan dikenai tagihan tambahan Otomatis kompres lampiran MMS Sinkronisasi pesan Sinkronisasi ulang pesan anda dengan basis data aplikasi SMS Android bawaan @@ -350,13 +361,14 @@ Lama + Automatic 100KB 200KB - 300KB (Direkomendasikan) + 300KB 600KB 1000KB 2000KB - Tanpa kompresi + No compression Oke diff --git a/presentation/src/main/res/values-it/strings.xml b/presentation/src/main/res/values-it/strings.xml index d2b2ad9aa..f1c9144f8 100644 --- a/presentation/src/main/res/values-it/strings.xml +++ b/presentation/src/main/res/values-it/strings.xml @@ -28,10 +28,12 @@ Digita un nome o un numero Salta Continua + Aggiungi persona Chiama Dettagli Salva nella galleria Apri barra di navigazione + Condividi %d selezionato Pulisci Archivia @@ -79,7 +81,11 @@ Inoltra Elimina - %d selezionati + Scegli un numero di telefono + %s ∙ Predefinito + Solo una volta + Sempre + %d selezionato %1$d di %2$d risultati Invia come messaggio di gruppo I destinatari e le risposte saranno visibili a tutti @@ -118,6 +124,7 @@ %s consegnati Invio non riuscito. Tocca per riprovare Dettagli + Indirizzo copiato Titolo della conversazione Notifiche Tema @@ -178,6 +185,7 @@ Modalità nero notte profonda Ora inizio Ora fine + Colori dei contatti automatici Dimensione carattere Usa il carattere di sistema Emoji automatico @@ -188,6 +196,7 @@ Pulsante 2 Pulsante 3 Anteprima delle notifiche + Schermata iniziale Vibrazione Suono Nessuno @@ -217,6 +226,9 @@ Rimuovi gli accenti dai caratteri negli SMS in uscita Solo numeri di telefoni cellulari Durante la composizione di un messaggio, visualizza solo i numeri di telefoni cellulari + Invia messaggi lunghi come MMS + Se i tuoi messaggi di testo più lunghi non vengono inviati o vengono inviati nell\'ordine sbagliato, puoi invece inviarli come messaggi MMS. Potrebbero essere applicati costi aggiuntivi +>>>>>>> 3f4693ee... Update translations Compressione automatica degli allegati MMS Sincronizza i messaggi Risincronizza i tuoi messaggi con il database SMS Android nativo @@ -256,10 +268,7 @@ Informazioni Versione - Sviluppatore Codice sorgente - Elenco delle modifiche - Contatto Licenza Copyright Supporta lo sviluppo, sblocca tutte le funzioni @@ -352,13 +361,14 @@ Lungo + Automatic 100KB 200KB - 300KB (consigliato) + 300KB 600KB 1000KB 2000KB - Nessuna compressione + No compression Okay diff --git a/presentation/src/main/res/values-iw/strings.xml b/presentation/src/main/res/values-iw/strings.xml index 2bf6ae133..e25d95173 100644 --- a/presentation/src/main/res/values-iw/strings.xml +++ b/presentation/src/main/res/values-iw/strings.xml @@ -30,9 +30,11 @@ כתוב הודעה דילוג המשך + להוסיף מישהו התקשר פרטים שמירה לגלריה + שיתוף פתיחת מגירת ניווט %d נבחרו פינוי @@ -81,6 +83,10 @@ העברה מחיקה + נא לבחור מספר טלפון + %s ∙ בררת מחדל + פעם אחד בלבד + תמיד %d נבחרו %1$d מתוך %2$d תוצאות שליחה כהודעה קבוצתית @@ -120,6 +126,7 @@ %s נמסרה השליחה נכשלה. נא לגעת כדי לנסות שוב פרטים + הכתובת הועתקה כותרת התכתבות התראות ערכת עיצוב @@ -182,6 +189,7 @@ מצב לילה שחור טהור זמן התחלה זמן סיום + צבעים אוטומטיים לאנשי קשר גודל גופן שימוש בגופן המערכת אימוג׳י אוטומטי @@ -192,6 +200,7 @@ כפתור 2 כפתור 3 תצוגות מקדימות של התראות + הפעלת המסך רטט צליל ללא @@ -221,6 +230,8 @@ הסרת סימנים דיאקריטיים מתווים במסרונים יוצאים מספרי ניידים בלבד בעת כתיבת הודעה, להציג רק מספרי טלפון של ניידים + לשלוח הודעות ארוכות כ־MMS + אם יש לך בעיה בשליחת הודעות טקסט ארוכות יותר או בעיה ברצף שליחת ההודעה, ניתן לשלוח כהודעת MMS במקום. עלול לגרור עלויות נוספות דחיסה אוטומטית של קבצים מצורפים להודעות MMS סנכרון הודעות סנכרון ההודעות שלך מחדש עם מסד הנתונים המובנה של Android @@ -362,9 +373,10 @@ ארוכה + אוטומטי 100 ק״ב 200 ק״ב - 300 ק״ב (מומלץ) + 300 ק״ב 600 ק״ב 1000 ק״ב 2000 ק״ב diff --git a/presentation/src/main/res/values-ja/strings.xml b/presentation/src/main/res/values-ja/strings.xml index dc47ca85a..3e2eec64b 100644 --- a/presentation/src/main/res/values-ja/strings.xml +++ b/presentation/src/main/res/values-ja/strings.xml @@ -30,9 +30,11 @@ 作成 スキップ 続行 + 人を追加 通話 詳細 ギャラリーに保存 + 共有 ナビゲーション引き出しを開く %d 選択された 消去しました @@ -78,6 +80,10 @@ 転送 削除 + 電話番号を選択 + %s ∙ デフォルト + 一回だけ + 常に %d件を選択中 %2$d 結果の %1$d グループ メッセージとして送信します。 @@ -117,6 +123,7 @@ %s を配信しました 送信に失敗しました。タップすると、もう一度やり直します 詳細 + Address copied 会話のタイトル 通知 テーマ @@ -176,6 +183,7 @@ ピュアブラック夜間モード 開始時刻 終了時刻 + Automatic contact colors フォントサイズ システムフォントを使用する 自動絵文字 @@ -186,6 +194,7 @@ ボタン 2 ボタン 3 通知プレビュー + Wake screen 振動 サウンド 無し @@ -215,6 +224,8 @@ 送信する SMS メッセージの文字からアクセントを削除します 携帯電話番号のみ メッセージを作成するときは、携帯電話番号のみを表示します + Send long messages as MMS + If your longer text messages are failing to send, or sending in the wrong order, you can send them as MMS messages instead. Additional charges may apply MMS添付ファイルの自動圧縮 メッセージを同期 メッセージをネイティブの Android SMS データベースと再同期します @@ -347,13 +358,14 @@ 長い + Automatic 100KB 200KB - 300KB 【推奨】 + 300KB 600KB 1000KB 2000KB - 圧縮なし + No compression OK diff --git a/presentation/src/main/res/values-ko/strings.xml b/presentation/src/main/res/values-ko/strings.xml index 34e17c462..ebcd69262 100644 --- a/presentation/src/main/res/values-ko/strings.xml +++ b/presentation/src/main/res/values-ko/strings.xml @@ -30,16 +30,18 @@ 이름 또는 전화번호 입력 건너뛰기 계속 + 대화 상대 추가하기 전화 자세히 갤러리에 저장하기 + 공유 메뉴 열기 %d 개가 선택됨 삭제하기 보관하기 - 보관하지 않기 + 보관 해제 지우기 - Add to contacts + 연락처에 추가 상단에 고정하기 고정 해제하기 읽음으로 표시 @@ -47,15 +49,20 @@ 차단 메시지 동기화 하기 나: %s - Draft: %s + 임시 메시지: %s 메시지 검색 결과 %d 개의 새로운 메시지 대화가 여기에 나타납니다 결과없음 보관된 메시지가 여기 나타납니다 새로운 대화 시작하기 + 다시 문자 보내는게 좋네요 Message 앱을 기본 메시지 앱으로 설정하기 + + + + 변경하기 권한을 부여해 주세요 Message 앱은 메시지를 보내고 읽을 수 있는 권한이 필요합니다 @@ -71,20 +78,22 @@ 친구 초대하기 삭제 - 정말 이 대화를 삭제할까요? - -%d 개의 대화를 삭제할까요? + 정말 %d 개의 대화를 삭제할까요? 문자 복사하기 전달 삭제 + 전화번호 선택하기 + %s ∙ 기본 + 한 번만 + 항상 %d 개 선택됨 %2$d 중 %1$d 결과 그룹 메시지 보내기 - 받는 사람 및 답장이 모든 사람에게 표시 됩니다. - 새로운 대화가 시작되었어요! 인사말을 건네는 건 어떨까요? + 받는 사람 및 답장이 모든 사람에게 표시됩니다. + 새로운 대화가 시작되었어요! 재미있는 대화를 해보세요! 연락처 예정 선택한 시간은 반드시 현재 이후의 시간으로 설정해야 합니다 @@ -98,8 +107,8 @@ 삭제 세부 정보 종류: %s - %s가 - %s에게 + %s 가 + %s 에게 제목: %s 우선 순위: %s 크기: %s @@ -119,6 +128,7 @@ %s 전송됨 전송 실패, 눌러서 다시 시도하세요 자세히 + Address copied 대화 제목 알림 테마 @@ -128,7 +138,7 @@ 차단 해제 대화 삭제 미디어를 불러올 수 없습니다 - 갤러리에 저장 + 갤러리에 저장하기 백업과 복원 메시지 백업하기 복원하기 @@ -158,19 +168,19 @@ 백업과 복원 예약됨 원하는 그 시간에 자동으로 메시지를 보냅니다. - 잠깐만요! 생일이 언제였나요..? - 12 월 23 일 입니다 - 생일 축하해요! 당신의 생일을 기억하는 멋진 친구죠..? + 잠깐만! 생일이 언제였더라..? + 12월 23일이야 + 생일 축하해! 너의 생일까지 기억해주는 이렇게 좋은 친구가 어디있겠어ㅋㅋ - 12 월 23 일에 보냅니다. + 12월 23일에 보냅니다. 메시지 예약 전송하기 예약된 메시지 지금 보내기 - Copy text - Delete + 텍스트 복사하기 + 삭제 - 외관 + 디자인 일반 설정 빠른 응답 테마 @@ -178,6 +188,7 @@ 완전한 블랙 테마 시작 시간 끝나는 시간 + Automatic contact colors 글꼴 크기 시스템 글꼴 사용 자동 이모티콘 @@ -188,6 +199,7 @@ 버튼 2 버튼 3 알림 미리 보기 + Wake screen 진동 소리 없음 @@ -211,41 +223,43 @@ 전송 확인 전송 성공을 확인합니다 - Signature - Add a signature to the end of your messages + 서명 + 문자 메시지 마지막에 서명을 추가합니다 스트립 악센트 SMS를 주고받을 때, 대문자 없애기 전화번호만 보이기 메시지를 쓸 때, 전화번호만 보이게 하기 + Send long messages as MMS + If your longer text messages are failing to send, or sending in the wrong order, you can send them as MMS messages instead. Additional charges may apply 자동으로 MMS 첨부 파일 압축하기 - 메시지 동기화 하기 + 메시지 동기화하기 기본 안드로이드 SMS 데이터베이스에 다시 동기화하기 Message에 대해... 버전 정보 %s 디버그 로깅 설정됨 디버그 로깅 해제됨 기간 (초)을 입력 - Blocking - Drop messages - Drop incoming messages from blocked senders instead of hiding them - Blocked conversations - Blocking Manager + 차단 + 메시지 자동 삭제 + 차단된 사람으로부터 오는 메시지를 가리는 대신 삭제합니다 + 차단된 대화 + 차단 메니저 QKSMS - Built-in blocking functionality in QKSMS - Automatically filter your calls and messages in one convenient place! Community IQ™ allows you to prevent unwanted messages from community known spammers + 빌트인 차단기능이 있는 기본 필터. + 자동으오 전화와 메시지를 한 곳에서 필터링하세요! Community IQ™는 커뮤니티 기반으로 원하지 않는 스팸 메시지를 차단해줍니다. Should I Answer 앱을 이용해 필요없는 번호에서 보낸 메시지를 자동으로 필터링하기 - Copy blocked numbers - Continue to %s and copy over your existing blocked numbers - Blocked numbers - Your blocked numbers will appear here - Block a new number - Block texts from - Phone number - Block - Blocked messages - Your blocked messages will appear here - Block - Unblock + 차단된 번호 복사하기 + %s로 계속하고 이미 차단된 번호를 복사합니다. + 차단된 번호 + 차단한 번호가 여기에 나타납니다 + 새로운 번호 차단하기 + 차단할 번호 입력 + 전화번호 + 차단 + 차단된 메시지 + 차단된 메시지가 여기에 나옵니다. + 차단 + 차단해제 Continue to %s and block these numbers @@ -254,10 +268,7 @@ 정보 버전 - 개발자 소스 코드 - 변경 사항 - 연락처 라이선스 저작권 개발을 지원하고 모든 기능을 해제하세요. @@ -351,13 +362,14 @@ 길게 - 100 KB - 200 KB - 300 KB (권장) - 600 KB - 1000 KB - 2000 KB - 압축 안 함 + Automatic + 100KB + 200KB + 300KB + 600KB + 1000KB + 2000KB + No compression 확인 diff --git a/presentation/src/main/res/values-lt/strings.xml b/presentation/src/main/res/values-lt/strings.xml index e846e7ed8..069a20fe0 100644 --- a/presentation/src/main/res/values-lt/strings.xml +++ b/presentation/src/main/res/values-lt/strings.xml @@ -30,9 +30,11 @@ Rašyti Praleisti Tęsti + Add person Skambinti Dėtalės Save to gallery + Share Open navigation drawer %d pasirinkta(s) Ištrinti @@ -81,6 +83,10 @@ Persiųsti Ištrinti + Choose a phone number + %s ∙ Default + Just once + Always %d parinktas %1$d of %2$d results Send as group message @@ -120,6 +126,7 @@ Nusiųsta %s Nepavyko nusiųsti. Spausk dar kartą norint persiųsti Dėtalės + Address copied Conversation title Pranešimai Tema @@ -182,6 +189,7 @@ Visiškai juoda nakties tema Pradžios laikas Pabaigos laikas + Automatic contact colors Šrifto dydis Naudoti sistemos šriftą Automatinis emoji @@ -192,6 +200,7 @@ Button 2 Button 3 Pranešimų peržiūra + Wake screen Vibracija Garas None @@ -221,6 +230,8 @@ Ištrinti jūsų SMS žinučių simbolių akcentus Mobile numbers only When composing a message, only show mobile numbers + Send long messages as MMS + If your longer text messages are failing to send, or sending in the wrong order, you can send them as MMS messages instead. Additional charges may apply Auto-compress MMS attachments Sinchronizuoti pranešimus Iš naujo sinchronizuoti Android pranešimų duomenų bazę @@ -264,11 +275,8 @@ Apie Versija - Kūrėjas Šaltinio kodas - Pakeitimų sąrašas - Kontaktai - Licencija +\ Licencija Autorinės teisės Remti programos vystymąsi, atrakinti viską Galite išsaugoti badaujantį programėlės kūrėją tik už %s @@ -362,9 +370,10 @@ Ilgas + Automatic 100KB 200KB - 300KB (Recommended) + 300KB 600KB 1000KB 2000KB diff --git a/presentation/src/main/res/values-nb/strings.xml b/presentation/src/main/res/values-nb/strings.xml index 39c34cf07..054f07d36 100644 --- a/presentation/src/main/res/values-nb/strings.xml +++ b/presentation/src/main/res/values-nb/strings.xml @@ -30,9 +30,11 @@ Skriv navn eller nummer Hopp over Fortsett + Add person Ring Detaljer Lagre til galleri + Share Åpne navigasjonsskuff %d valgt Tøm @@ -79,6 +81,10 @@ Videresend Slett + Choose a phone number + %s ∙ Default + Just once + Always %d valgt %1$d av %2$d funn Send som gruppemelding @@ -119,6 +125,7 @@ Levert %s Feil ved sending. Trykk for å prøve igjen Detaljer + Address copied Tittel på samtalen Varsler Drakt @@ -180,6 +187,7 @@ Ren svart nattmodus Starttid Sluttidspunkt + Automatic contact colors Skriftstørrelse Bruk systemskrift Automatisk emoji @@ -190,6 +198,7 @@ Knapp 2 Knapp 3 Forhåndsvisninger av varsler + Wake screen Vibrasjon Lyd Ingen @@ -219,6 +228,8 @@ Ta bort akutt-tegn fra utgående meldinger Kun mobilnummre Vis kun mobilnummre når du skriver ny melding + Send long messages as MMS + If your longer text messages are failing to send, or sending in the wrong order, you can send them as MMS messages instead. Additional charges may apply Komprimer MMS-vedlegg Synkroniser meldinger Resynkroniser meldinger med Androids interne meldingslager @@ -355,13 +366,14 @@ Lang - 100 KB - 200 KB - 300 KB (anbefalt) - 600 KB - 1000 KB - 2000 KB - Ingen komprimering + Automatic + 100KB + 200KB + 300KB + 600KB + 1000KB + 2000KB + No compression Ok diff --git a/presentation/src/main/res/values-ne/strings.xml b/presentation/src/main/res/values-ne/strings.xml index 5cfce13a6..cde62f754 100644 --- a/presentation/src/main/res/values-ne/strings.xml +++ b/presentation/src/main/res/values-ne/strings.xml @@ -30,9 +30,11 @@ Compose Skip Continue + Add person सम्पर्क Details Save to gallery + Share Open navigation drawer %d selected Clear @@ -79,6 +81,10 @@ Forward Delete + Choose a phone number + %s ∙ Default + Just once + Always %d selected %1$d of %2$d results Send as group message @@ -118,6 +124,7 @@ Delivered %s Failed to send. Tap to try again Details + Address copied Conversation title Notifications Theme @@ -178,6 +185,7 @@ Pure black night mode Start time End time + Automatic contact colors Font size Use system font Automatic emoji @@ -188,6 +196,7 @@ Button 2 Button 3 Notification previews + Wake screen Vibration Sound None @@ -217,6 +226,8 @@ Remove accents from characters in outgoing SMS messages Mobile numbers only When composing a message, only show mobile numbers + Send long messages as MMS + If your longer text messages are failing to send, or sending in the wrong order, you can send them as MMS messages instead. Additional charges may apply Auto-compress MMS attachments Sync messages Re-sync your messages with the native Android SMS database @@ -352,9 +363,10 @@ Long + Automatic 100KB 200KB - 300KB (Recommended) + 300KB 600KB 1000KB 2000KB diff --git a/presentation/src/main/res/values-nl/strings.xml b/presentation/src/main/res/values-nl/strings.xml index 30fd08698..d780daf9a 100644 --- a/presentation/src/main/res/values-nl/strings.xml +++ b/presentation/src/main/res/values-nl/strings.xml @@ -30,9 +30,11 @@ Type een naam of nummer Overslaan Verder + Add person Bel Details Opslaan in galerij + Share Open navigatie lade %d geselecteerd Wissen @@ -81,6 +83,10 @@ Doorsturen Verwijderen + Choose a phone number + %s ∙ Default + Just once + Always %d geselecteerd %1$d van de %2$d resultaten Versturen als groepsbericht @@ -120,6 +126,7 @@ Afgeleverd %s Fout bij verzenden. Tik om opnieuw te proberen Details + Address copied Titel van conversatie Notificaties Thema @@ -180,6 +187,7 @@ Pure zwarte nachtmodus Starttijd Eindtijd + Automatic contact colors Tekst grootte Gebruik lettertype van systeem Automatische emoji @@ -190,6 +198,7 @@ Button 2 Button 3 Notificatievoorbeeld + Wake screen Trillen Geluid Geen @@ -219,6 +228,8 @@ Verwijder accenten van karakters voor uitgaande berichten Alleen GSM-nummers Enkel mobiele nummers tonen wanneer je een bericht opstelt + Send long messages as MMS + If your longer text messages are failing to send, or sending in the wrong order, you can send them as MMS messages instead. Additional charges may apply Auto-compress MMS bijlagen Synchroniseer berichten Hersynchroniseer jouw berichten vanuit de SMS database op je telefoon @@ -354,13 +365,14 @@ Lang + Automatic 100KB 200KB - 300KB (aanbevolen) + 300KB 600KB 1000KB 2000KB - Geen compressie + No compression Oké diff --git a/presentation/src/main/res/values-pl/strings.xml b/presentation/src/main/res/values-pl/strings.xml index 56a913234..5432e5f3d 100644 --- a/presentation/src/main/res/values-pl/strings.xml +++ b/presentation/src/main/res/values-pl/strings.xml @@ -30,9 +30,11 @@ Nowa wiadomość Pomiń Kontynuuj + Dodaj osobę Zadzwoń Szczegóły Zapisz w galerii + Udostepnij Otwórz panel nawigacyjny %d wybrano Wyczyść @@ -85,6 +87,10 @@ Przekaż dalej Usuń + Wybierz numer telefonu + %s ∙ Domyślny + Tylko raz + Zawsze Wybrano %d Wyniki: %1$d z %2$d Wyślij jako wiadomość grupową @@ -124,6 +130,7 @@ Dostarczono %s Nie udało się wysłać wiadomości. Dotknij, aby spróbować ponownie Szczegóły + Address copied Tytuł rozmowy Powiadomienia Motyw @@ -186,6 +193,7 @@ Czarne tło w trybie nocnym Czas rozpoczęcia Czas zakończenia + Automatic contact colors Rozmiar czcionki Używaj czcionki systemowej Automatyczne emoji @@ -196,6 +204,7 @@ Przycisk 2 Przycisk 3 Zawartość powiadomień + Wake screen Wibracje Dźwięki Brak @@ -220,11 +229,13 @@ Potwierdzenie dostarczenia Potwierdzaj dostarczenie każdej wysłanej wiadomości Podpis - Add a signature to the end of your messages + Dodawaj podpis na końcu wiadomości Usuwaj ogonki Usuwaj ogonki ze znaków w wiadomościach Tylko numery komórkowe Pokazuj kontakty tylko z numerami komórkowymi + Send long messages as MMS + If your longer text messages are failing to send, or sending in the wrong order, you can send them as MMS messages instead. Additional charges may apply Automatyczna kompresja załączników MMSów Synchronizuj wiadomości Zsynchronizuj wiadomości z systemową bazą danych @@ -243,7 +254,7 @@ Automatycznie filtruj twoje rozmowy i wiadomości tekstowe w jednym wygodnym miejscu! Community IQ™ pozwala zapobiegać niechcianym wiadomościom od znanych spamerów Automatycznie filtruj wiadomości od niechcianych numerów za pomocą aplikacji \"Should I Answer?\" Skopiuj zablokowane numery - Kontynuuj do %s kopiowanie twoich blokowanych numerów + Kontynuuj do %s i skopiuj twoje obecnie blokowane numery Zablokowane numery Twoje blokowane numery się tu pojawią Zablokuj nowy numer @@ -366,13 +377,14 @@ Długi + Automatic 100KB 200KB - 300KB (zalecane) + 300KB 600KB 1000KB 2000KB - Bez kompresji + No compression OK diff --git a/presentation/src/main/res/values-pt-rBR/strings.xml b/presentation/src/main/res/values-pt-rBR/strings.xml index 343f80309..67b72081b 100644 --- a/presentation/src/main/res/values-pt-rBR/strings.xml +++ b/presentation/src/main/res/values-pt-rBR/strings.xml @@ -30,9 +30,11 @@ Nova Mensagem Pular Continuar + Adicionar pessoa Ligar Detalhes Salvar na galeria + Partilhar Abrir gaveta de navegação %d selecionada(s) Limpar @@ -79,6 +81,10 @@ Encaminhar Excluir + Escolha um número de telefone + %s ∙ Padrão + Uma vez + Sempre %d selecionada(s) %1$d de %2$d resultados Enviar como mensagem de grupo @@ -87,7 +93,6 @@ Cartão de contato Agendada para O tempo selecionado deve ser no futuro! - Você deve desbloquear o QKSMS+ para agendar mensagens Adicionada às mensagens agendadas Escrever uma mensagem… Copiar texto @@ -119,6 +124,7 @@ Entregue %s Falha ao enviar. Toque para tentar novamente Detalhes + Endereço copiado Título da conversa Notificações Tema @@ -137,7 +143,6 @@ Nunca Recuperar Selecione um backup - Por favor, desbloqueie o QKSMS+ para fazer backup e recuperar Backup em andamento… Recuperação em andamento… Recuperar do backup @@ -180,6 +185,7 @@ Modo noturno preto puro Hora de início Hora de término + Cores automáticas para os contactos Tamanho da fonte Usar fonte do sistema Emoji automático @@ -190,6 +196,7 @@ Botão 2 Botão 3 Pré-visualização de notificações + Ligar ecrã Vibração Som Nenhum @@ -219,6 +226,8 @@ Remover acentos de caracteres em mensagens SMS enviadas Somente números de celular Ao escrever uma mensagem, mostrar apenas os números de celular + Enviar mensagens longas como MMS + Se as mensagens longas não estiverem a ser enviadas ou estão a ser enviadas pela ordem errada, pode tentar enviar como MMS. Podem ser aplicados custos adicionais. Comprimir anexos MMS automaticamente Sincronizar mensagens Re-sincronizar suas mensagens com o banco de dados de SMS nativo do Android @@ -258,10 +267,7 @@ Sobre Versão - Desenvolvedor Código-fonte - Changelog - Contato Licença Copyright Apoie o desenvolvimento, desbloqueie tudo @@ -355,13 +361,14 @@ Longo + Automatic 100KB 200KB - 300KB (Recomendado) + 300KB 600KB 1000KB 2000KB - Não comprimir + No compression Ok diff --git a/presentation/src/main/res/values-pt/strings.xml b/presentation/src/main/res/values-pt/strings.xml index e3a54f897..f7a44e714 100644 --- a/presentation/src/main/res/values-pt/strings.xml +++ b/presentation/src/main/res/values-pt/strings.xml @@ -30,9 +30,11 @@ Digite um nome ou um número Ignorar Continuar + Adicionar pessoa Ligar Detalhes Guardar na galeria + Partilhar Abrir menu de navegação %d selecionada(s) Limpar @@ -83,6 +85,10 @@ Reencaminhar Apagar + Escolha um número de telefone + %s ∙ Padrão + Uma vez + Sempre %d selecionada(s) %1$d de %2$d resultados Enviar como mensagem de grupo @@ -122,6 +128,7 @@ %s entregue Falha ao enviar. Toque para tentar novamente Detalhes + Endereço copiado Título da conversa Notificações Tema @@ -182,6 +189,7 @@ Modo escuro puro Hora de início Hora de fim + Cores automáticas para os contactos Tamanho do texto Usar tipo de letra do sistema Emoji automático @@ -192,6 +200,7 @@ Botão 2 Botão 3 Pré-visualização de notificações + Ligar ecrã Vibração Som Nenhum @@ -221,6 +230,8 @@ Remover acentos dos caracteres nas mensagens enviadas Apenas números de telemóvel Ao escrever uma mensagem, mostrar apenas os números móveis + Enviar mensagens longas como MMS + Se as mensagens longas não estiverem a ser enviadas ou estão a ser enviadas pela ordem errada, pode tentar enviar como MMS. Podem ser aplicados custos adicionais. Comprimir anexos MMS automaticamente Sincronizar mensagens Sincronizar as mensagens com a base de dados nativa do Android @@ -356,13 +367,14 @@ Longo + Automatic 100KB 200KB - 300KB (recomendado) + 300KB 600KB 1000KB 2000KB - Não comprimir + No compression Ok diff --git a/presentation/src/main/res/values-ro/strings.xml b/presentation/src/main/res/values-ro/strings.xml index 019252c5d..9c515c1d7 100644 --- a/presentation/src/main/res/values-ro/strings.xml +++ b/presentation/src/main/res/values-ro/strings.xml @@ -30,9 +30,11 @@ Compune Sari Continue + Add person Apel Details Save to gallery + Share Open navigation drawer %d selected Clear @@ -80,6 +82,10 @@ Forward Delete + Choose a phone number + %s ∙ Default + Just once + Always %d selected %1$d of %2$d results Send as group message @@ -119,6 +125,7 @@ Delivered %s Failed to send. Tap to try again Details + Address copied Conversation title Notifications Theme @@ -180,6 +187,7 @@ Pure black night mode Start time End time + Automatic contact colors Font size Use system font Automatic emoji @@ -190,6 +198,7 @@ Button 2 Button 3 Notification previews + Wake screen Vibration Sound None @@ -219,6 +228,8 @@ Remove accents from characters in outgoing SMS messages Mobile numbers only When composing a message, only show mobile numbers + Send long messages as MMS + If your longer text messages are failing to send, or sending in the wrong order, you can send them as MMS messages instead. Additional charges may apply Auto-compress MMS attachments Sync messages Re-sync your messages with the native Android SMS database @@ -357,9 +368,10 @@ Long + Automatic 100KB 200KB - 300KB (Recommended) + 300KB 600KB 1000KB 2000KB diff --git a/presentation/src/main/res/values-ru/strings.xml b/presentation/src/main/res/values-ru/strings.xml index 23ada9bbe..c35a8ebb6 100644 --- a/presentation/src/main/res/values-ru/strings.xml +++ b/presentation/src/main/res/values-ru/strings.xml @@ -30,9 +30,11 @@ Написать Пропустить Продолжить + Добавить контакт Позвонить Подробности Сохранить в галерее + Поделиться Открыть боковое меню %d выбрано Очистить @@ -81,6 +83,10 @@ Переслать Удалить + Выбор номера телефона + %s ∙ по умолчанию + Один раз + Всегда %d выбран(о) %1$d из %2$d результатов Отправить как групповое сообщение @@ -121,6 +127,7 @@ Доставлено %s Не удалось отправить. Нажмите, чтобы попробовать ещё раз. Подробности + Адрес скопирован Название разговора Уведомления Тема @@ -184,6 +191,7 @@ Полностью чёрный ночной режим Время начала Время окончания + Автоматические цвета контактов Размер шрифта Использовать системный шрифт Автоматические emoji @@ -194,6 +202,7 @@ Кнопка 2 Кнопка 3 Предпросмотр уведомлений + Экран блокировки Вибросигнал Звук Нет @@ -223,6 +232,8 @@ Удаление акцентов с символов в исходящих SMS-сообщениях Только мобильные номера При составлении сообщения показывать только мобильные номера + Отправлять длинные сообщения как MMS + Если не удаётся отправить длинные текстовые сообщения или они отправляются в неправильном порядке, вы можете отправить их в виде MMS. Может взиматься дополнительная плата. Автоматически сжимать вложения MMS Синхронизация сообщений Принудительная синхронизация сообщений с собственной базой данных SMS Android @@ -365,12 +376,13 @@ Длинная - 100 КБ - 200 КБ - 300 КБ (рекомендуется) - 600 КБ - 1000 КБ - 2000 КБ + Автоматически + 100 Кб + 200 Кб + 300 Кб + 600 Кб + 1000 Кб + 2000 Кб Без сжатия diff --git a/presentation/src/main/res/values-sk/strings.xml b/presentation/src/main/res/values-sk/strings.xml index d2818ce3b..668b8b4b3 100644 --- a/presentation/src/main/res/values-sk/strings.xml +++ b/presentation/src/main/res/values-sk/strings.xml @@ -30,9 +30,11 @@ Nová správa Preskočiť Pokračovať + Add person Volať Podrobnosti Uložiť do galérie + Share Otvoriť navigačné menu %d vybraných Vymazať @@ -83,6 +85,10 @@ Preposlať Odstrániť + Choose a phone number + %s ∙ Default + Just once + Always %d selected %1$d of %2$d results Poslať ako skupinovú správu @@ -122,6 +128,7 @@ Doručené %s Failed to send. Tap to try again Detaily + Address copied Conversation title Upozornenia Motív @@ -184,6 +191,7 @@ Pure black night mode Čas spustenia Čas ukončenia + Automatic contact colors Veľkosť písma Použiť systémové písmo Automatické emoji @@ -194,6 +202,7 @@ Tlačidlo 2 Tlačidlo 3 Obsah upozornení + Wake screen Vibration Sound None @@ -223,6 +232,8 @@ Remove accents from characters in outgoing SMS messages Mobile numbers only When composing a message, only show mobile numbers + Send long messages as MMS + If your longer text messages are failing to send, or sending in the wrong order, you can send them as MMS messages instead. Additional charges may apply Auto-compress MMS attachments Sync messages Re-sync your messages with the native Android SMS database @@ -364,13 +375,14 @@ Long + Automatic 100KB 200KB - 300KB (odporúča sa) + 300KB 600KB 1000KB 2000KB - Bez kompresie + No compression V poriadku diff --git a/presentation/src/main/res/values-sl/strings.xml b/presentation/src/main/res/values-sl/strings.xml new file mode 100644 index 000000000..eb4a56a04 --- /dev/null +++ b/presentation/src/main/res/values-sl/strings.xml @@ -0,0 +1,407 @@ + + + + + Nov pogovor + Sestavi + Bližnjica onemogočena + Arhivirano + Nastavitve + Obvestila + Tema + Išči po prejetih sporočilih… + Vnesi ime ali številko + Preskoči + Nadaljuj + Dodaj osebo + Klic + Podrobnosti + Shrani v galerijo + Deli + Odpri nacigacijski meni + %d izbranih + Počisti + Arhiv + Odstrani iz arhiva + Izbriši + Shrani v imenik + Pripni na vrh + Odpni + Označi kot prebrano + Označi kot neprebrano + Blokiraj + Sinhroniziranje sporočil… + Vi: %s + Osnutek: %s + Rezultati v sporočilih + %d sporočil + Vaši pogovori se bodo pojavili tukaj + Ni rezultatov + Vaši arhivirani pogovori se bodo pojavili tukaj + Začni nov pogovor + Znova vzljubite sporočanje + Napravi QKSMS privzet program za sporočanje + Spremeni + Potrebno dovoljenje + QKSMS potrebuje dovoljenje za pošiljanje in branje SMS sporočil + QKSMS potrebuje dovoljenje za dostop do stikov + Dovoli + Prejeto + Arhiv + Načrtovano + Blokiranje + Več + Nastavitve + Pomagaj & povrate informacije + Povabi prijatelje + Odkleni neverjetne nove funkcije in podpri razvoj + Uživate v QKSMS? + Deli nekaj ljubezni in nas oceni na Google Play! + V REDU! + OPUSTI + Izbriši + + Ali ste prepričani, da želite izbrisati %d pogovor? + Ali ste prepričani, da želite izbrisati %d pogovora? + Ali ste prepričani, da želite izbrisati %d pogovorov? + Ali ste prepričani, da želite izbrisati %d pogovorov? + + + Kopiraj besedilo + Naprej + Izbriši + + Izberi telefonsko številko + %s ∙ Privzeto + Samo enkrat + Vedno + %d izbran + %1$d od %2$d rezultatov + Pošlji kot skupinsko sporočilo + Prejemniki in odgovori bodo vidni vsem + To je začetek vašega pogovora. Povej nekaj lepega! + Kontaktna kartica + Načrtovano za + Izbran čas mora biti v prihodnosti! + Da lahko uporabiš načrtovano sporočanje, moraš odkleniti QKSMS+ + Dodano k načrtovanim sporočilom + Napiši sporočilo… + Kopiraj besedilo + Naprej + Izbriši + Prejšnje + Naslednje + Počisti + Podrobnosti sporočila + Vnesi: %s + Od: %s + Za: %s + Zadeva %s + Prioriteta: %s + Velikost: %s + Poslano: %s + Prejeto: %s + Dostavljeno: %s + Koda napake: %d + Vstavi priponko + Vstavi sliko + Zajemi sliko + Načrtuj sporočilo + Vstavi stik + Napaka pri branju stika + %s izbran, zamenjaj kartico SIM + Pošlji sporočilo + Pošiljanje… + Dostavljeno %s + Napaka pri pošiljanju. Pritisni za ponovni poskus + Podrobnosti + Naslov kopiran + Naslov pogovora + Obvestila + Teme + Arhiv + Ostrani iz arhiva + Blokiraj + Odblokiraj + Izbriši pogovor + Napaka pri nalaganju večpredstavnosti + Shranjeno v galerijo + Varnostna kopija in obnovitev + Ustvarjanje varnostne kopije sporočil + Obnovitev varnostne kopije + Zadnja varnostna kopija + Nalaganje… + Nikoli + Obnovi + Izberi varnostno kopijo + Prosimo odklenite QKSMS+ da omogočite uporabo varnostnih kopij in njihove obnove + Varnostna kopija se izvaja… + Obnavljanje varnostne kopije se izvaja… + Obnovi iz varnotsne kopije + Ali ste prepričani, da želite obnoviti sporočila iz te varnostne kopije? + Prekliči obnovitev varnostne kopije + Sporočila, ki so že bila obnovljena, bodo ostala na vaši napravi + Varnostne kopije + Ni najdenih varnostnih kopij + + %d sporočilo + %d sporočila + %d sporočil + %d sporočila + + Trenutno so za ustvarjanje in obnovo varnostnih kopij podprta le sporočila SMS. MMS podpora in varnostne kopije pridejo kmalu! + Ustvari varnostno kopijo sedaj + Razčlenjevanje varnostne kopije… + %d/%d sporočil + Shranjevanje varnostne kopije… + Sinhroniziranje sporočil… + Končano! + Varnostna kopija in obnovitev + Načrtovano + Samodejno pošlji sporočilo ob nastavljenem točno določenem trenutku + Hej! Kdaj je že tvoj rojstni dan? + Je 23. Decembra + Vse najboljše! Poglej kako dober prijatelj sem, da se spomnim tvojega rojstnega dneva + + Pošiljanje na 23. December + Načrtuj sporočilo + Načrtovano sporočilo + + Pošlji sedaj + Kopiraj besedilo + Izbriši + + Izgled + Glavno + QK Odgovor + Teme + Nočni način + Čisto črni nočni način + Začetni čas + Čas končanja + Samodejne barve stikov + Velikost pisave + Uporabi sistemsko pisavo + Samodejni čustvenček + Obvestila + Tapni za prilagoditev + Dejanja + Gumb 1 + Gumb 2 + Gumb 3 + Predogledi obvestil + Zbudi zaslon + Vibriranje + Zvok + Brez + QK Odgovor + Pojavno obvestilo za nova sporočila + Tapni da opustiš + Tapni izven obvestila, da ga zapreš + Zakasnjeno sporočanje + Dejanja drsenja + Nastavi dejanja drsenja za pogovore + Podrsaj desno + Podrsaj levo + SPREMENI + + Brez + Arhiviraj + Izbriši + Pokliči + Označi kot prebrano + Označi kot neprebrano + + Potrditev dostavitve sporočila + Potrdi, da so bila sporočila dostavljena uspešno + Podpis + Dodaj podpis na konec vaših sporočil + Odstrani narečja + Odstrani narečja iz besedila odhodnih SMS sporočil + Samo telefonske številke + Ko sestavljaš sporočilo, prikaži samo telefonske številke + Pošlji dolga sporočila kot MMS + Če se vaša daljša sporočila ne pošljejo, ali se pošiljajo v napačnem vrstnem redu, jih lahko pošljete kot MMS sporočilo. To lahko povzroči dodatne stroške + Samodejno stisni MMS priponke + Sinhroniziraj sporočila + Znova sinhroniziraj sporočila s sistemsko SMS bazo + Več o QKSMS + Različica %s + Beleženje dnevnika za odpravljanje napak omogočeno + Beleženje dnevnika za odpravljanje napak onemogočeno + Vnesi trajanje (v sekundah) + Blokiranje + Opusti sporočila + Opusti prihajajoča sporočila blokiranih pošiljateljev, namesto da jih skriješ + Blokirani pogovori + Upravljalnik Blokiranja + QKSMS + Vgrajena sposobnost blokiranja v QKSMS + Samodejno razvrstite vaše klice in sporočila v eno priročno mesto! Skupnostni IQ™ omogoča, da preprečite prejemanje sporočil pošiljateljev neželenih sporočil, kateri so skupnosti poznani + Samodejno razvrstite sporočila neželenih številk tako, da uporabiš \"Should I Answer\" aplikacijo + Kopiraj blokirane številke + Nadaljuj z %s in kopiraj obstoječe blokirane številke + Blokirane številke + Vaše blokirane številke se bodo pojavile tukaj + Blokiraj novo številko + Blokiraj besedila od + Telefonska številka + Blokirano + Blokirana sporočila + Blokirana sporočila bodo prikazana tukaj + Blokiraj + Odblokiraj + + Nadaljuj z %s in blokiraj to številko + Nadaljuj z %s in blokiraj ti številki + Nadaljuj z %s in blokiraj te številke + Nadaljuj z %s in blokiraj te številke + + + Nadaljuj z %s in dovoli to številko + Nadaljuj z %s in dovoli ti številki + Nadaljuj z %s in dovoli te številke + Nadaljuj z %s in dovoli te številke + + O aplikaciji + Različica + Razvijalec + Izvirna koda + Dnevnik sprememb + Stik + Licenca + Avtorske pravice + Podpri razvoj, odkleni vse + Že s %s lahko rešiš stradajočega razvijalca + + Doživljenjska nadgradnja za %1$s %2$s + + Odkleni + doniraj za %1$s %2$s + Hvala da podpirate QKSMS! + Zdaj imate dostop do vseh QKSMS+ funkcij + QKSMS+ je brezplačen za F-Droid uporabnike! Če želite podpirati razvoj, so donacije zelo dobrodošle. + Doniraj preko PayPal + Prihaja kmalu + Premium teme + Odkleni čudovite barve tem, ki niso vidne na paleti materialnega dizajna + Auto-čustvenček po meri + Ustvari avto-čustvenček bližnjico po meri + Varnostna kopija sporočila + Samodejno varnostno kopiraj sporočila. Nikoli več ne skrbi za izgubljeno zgodovino, če zamenjaš ali izgubiš telefon + Načrtovana sporočila + Načrtuj, da bodo sporočila poslana ob določenem datumu in času + Zakasnjeno pošiljanje + Počakaj nekaj sekund, preden pošlješ svoje sporočilo + Samodejni temni način + Omogoči temni način, glede na čas dneva + Napredno blokiranje + Blokiraj sporočila, ki vsebujejo ključne besede ali se ujemajo z vzorci + Samodejno-posreduj + Samodejno posreduj sporočila določenih pošiljateljev + Samodejen odgovor + Samodejno odgovori na prihajajoča sporočila s prednastavljenim sporočilom + Več + QKSMS je pod aktivnim razvojem in vaš nakup QKSMS+ bo vseboval vse prihodnje funkcije! + Nalaganje… + Poglej več pogovorov + Označi kot prebrano + Pokliči + Izbriši + Pokaži več + Pokaži manj + Odpri pogovor + Material + HEX + Potrdi + + Brez + Označi kot prebrano + Odgovori + Klic + Izbriši + + Da + Nadaljuj + Prekliči + Izbriši + Shrani + Ustavi + Več + Nastavi + Razveljavi + Kopirano + Arhivirani pogovori + Da uporabiš to, moraš odkleniti QKSMS+ + + Novo sporočilo + %s novi sporočili + %s novih sporočil + %s novih sporočil + + Sporočilo ni poslano + Sporočilo za %s ni bilo poslano + + Sistem + Onemogočeno + Vedno vključen + Samodejno + + + Prikaži ime in sporočilo + Prikaži ime + Skrij vsebino + + + Majhno + Običajno + Veliko + Večje + + + Brez zakasnitve + Kratko + Srednje + Dolgo + + + Automatic + 100KB + 200KB + 300KB + 600KB + 1000KB + 2000KB + No compression + + + V redu + Počakaj trenutek + Na poti + Hvala + Zveni dobro + Kako si? + Se strinjam + Ne + Ljubim te + Oprosti + LOL + To je v redu + + diff --git a/presentation/src/main/res/values-sr/strings.xml b/presentation/src/main/res/values-sr/strings.xml index 14f9b6a8a..b42538b2f 100644 --- a/presentation/src/main/res/values-sr/strings.xml +++ b/presentation/src/main/res/values-sr/strings.xml @@ -30,9 +30,11 @@ Састављање Skip Continue + Add person Позови Details Save to gallery + Share Open navigation drawer %d selected Clear @@ -80,6 +82,10 @@ Forward Delete + Choose a phone number + %s ∙ Default + Just once + Always %d selected %1$d of %2$d results Send as group message @@ -119,6 +125,7 @@ Delivered %s Failed to send. Tap to try again Details + Address copied Conversation title Notifications Theme @@ -180,6 +187,7 @@ Pure black night mode Start time End time + Automatic contact colors Font size Use system font Automatic emoji @@ -190,6 +198,7 @@ Button 2 Button 3 Notification previews + Wake screen Vibration Sound None @@ -219,6 +228,8 @@ Remove accents from characters in outgoing SMS messages Mobile numbers only When composing a message, only show mobile numbers + Send long messages as MMS + If your longer text messages are failing to send, or sending in the wrong order, you can send them as MMS messages instead. Additional charges may apply Auto-compress MMS attachments Sync messages Re-sync your messages with the native Android SMS database @@ -357,9 +368,10 @@ Long + Automatic 100KB 200KB - 300KB (Recommended) + 300KB 600KB 1000KB 2000KB diff --git a/presentation/src/main/res/values-sv/strings.xml b/presentation/src/main/res/values-sv/strings.xml index 982c76d04..37327cbe6 100644 --- a/presentation/src/main/res/values-sv/strings.xml +++ b/presentation/src/main/res/values-sv/strings.xml @@ -30,16 +30,18 @@ Ange ett namn eller nummer Hoppa över Fortsätt + Lägg till person Ring Detaljer Spara till galleri + Dela Öppna navigeringspanelen %d vald Rensa Arkiv Avarkivera Radera - Add to contacts + Lägg till i kontakter Fäst på toppen Frigör Markera som läst @@ -83,6 +85,10 @@ Vidarebefordra Radera + Välj ett telefonnummer + %s ∙ Default + Bara en gång + Alltid %d vald %1$d av %2$d resultat Skicka som ett grupp-meddelande @@ -122,6 +128,7 @@ Levererade %s Misslyckades med att skicka. Knacka för att försöka igen Detaljer + Address copied Titel för konversation Aviseringar Tema @@ -172,7 +179,7 @@ Skicka nu Copy text - Delete + Radera Utseende Generellt @@ -182,6 +189,7 @@ Svart nattläge Starttid Sluttid + Automatic contact colors Storlek på typsnitt Använd systemteckensnitt Automatisk uttryckssymbol @@ -192,6 +200,7 @@ Knapp 2 Knapp 3 Notifikations förhandsvisningar + Wake screen Vibration Ljud Ingen @@ -211,16 +220,18 @@ Ta bort Ring Markera som läst - Mark as unread + Markera som oläst Leveransrapporter Bekräfta att meddelanden har skickats - Signature - Add a signature to the end of your messages + Signatur + Lägg till en signatur i slutet av dina meddelanden Ta bort accenter Ta bort accenter från tecken i utåtgående SMS-meddelanden Endast mobilnummer Visa endast mobilnummer när ett meddelanden skrivs + Skicka långa meddelanden som MMS + If your longer text messages are failing to send, or sending in the wrong order, you can send them as MMS messages instead. Additional charges may apply Komprimera MMS-bilagor automatiskt Synkronisera meddelanden Synkronisera dina meddelanden med telefonens SMS-databas @@ -229,27 +240,27 @@ Felsökningsloggning aktiverades Felsökningsloggning inaktiverad Fyll i varaktighet (sekunder) - Blocking + Blockerar Drop messages Drop incoming messages from blocked senders instead of hiding them - Blocked conversations - Blocking Manager + Blockerade konversationer + Blockerings-hanterare QKSMS Built-in blocking functionality in QKSMS Automatically filter your calls and messages in one convenient place! Community IQ™ allows you to prevent unwanted messages from community known spammers Filtrera automatiskt meddelanden från oönskade nummer genom att använda appen \"Borde jag svara\" - Copy blocked numbers - Continue to %s and copy over your existing blocked numbers - Blocked numbers - Your blocked numbers will appear here - Block a new number - Block texts from - Phone number - Block - Blocked messages - Your blocked messages will appear here + Kopiera blockerade nummer + Fortsätt till %s och kopiera över dina befintliga blockerade nummer + Blockerade nummer + Dina blockerade nummer kommer att visas här + Blockera ett nytt nummer + Blockera SMS från + Telefonnummer + Blockera + Blockerade meddelanden + Dina blockerade meddelanden visas här Block - Unblock + Avblockera Continue to %s and block this number Continue to %s and block these numbers @@ -315,8 +326,8 @@ Ring Ta bort - Yes - Continue + Ja + Fortsätt Avbryt Radera Spara @@ -334,8 +345,8 @@ Meddelandet till %s misslyckades att skicka System - Disabled - Always on + Inaktiverad + Alltid på Automatic @@ -356,13 +367,14 @@ Lång + Automatic 100KB 200KB - 300KB (Rekommenderas) + 300KB 600KB 1000KB 2000KB - Ingen kompression + No compression Okej diff --git a/presentation/src/main/res/values-th/strings.xml b/presentation/src/main/res/values-th/strings.xml index 078b20333..054100ed9 100644 --- a/presentation/src/main/res/values-th/strings.xml +++ b/presentation/src/main/res/values-th/strings.xml @@ -30,9 +30,11 @@ พิมพ์ชื่อหรือหมายเลข ข้าม ทำต่อ + Add person โทร รายละเอียด บันทึกรูปภาพ + Share เปิดเมนูลัด %d ถูกเลือก ลบออก @@ -78,6 +80,10 @@ ส่งต่อ ลบ + Choose a phone number + %s ∙ Default + Just once + Always %d ถูกเลือก %1$d ใน %2$d ของผลลัพท์ ส่งเป็นข้อความกลุ่ม @@ -117,6 +123,7 @@ %s ถูกส่งแล้ว การส่งล้มเหลว ลองแตะเพื่อส่งอีกครั้ง รายละเอียด + Address copied หัวข้อสนทนา การแจ้งเตือน รูปแบบการแสดงผล @@ -176,6 +183,7 @@ โหมดกลางคืนแบบมืดสนิท เวลาเริ่มต้น เวลาสิ้นสุด + Automatic contact colors ขนาดแบบอักษร ใช้แบบอักษรของระบบ อีโมจิอัตโนมัติ @@ -186,6 +194,7 @@ Button 2 Button 3 ตัวอย่างการแจ้งเตือน + Wake screen การสั่น เสียง ไม่มี @@ -215,6 +224,8 @@ ลบอักษรพิเศษ accents ออกจากข้อความ SMS ที่กำลังจะส่งออก Mobile numbers only When composing a message, only show mobile numbers + Send long messages as MMS + If your longer text messages are failing to send, or sending in the wrong order, you can send them as MMS messages instead. Additional charges may apply บีบอัดสิ่งที่แนบมาใน MMS อัตโนมัติ ซิงค์ข้อความ ซิงค์ข้อความของคุณกับฐานข้อมูลของ Android SMS @@ -347,13 +358,14 @@ ยาว + Automatic 100KB 200KB - 300KB (แนะนำ) + 300KB 600KB - 1000 กิโลไบต์ - 2000 กิโลไบต์ - ไม่มีการบีบอัด + 1000KB + 2000KB + No compression ตกลง diff --git a/presentation/src/main/res/values-tl/strings.xml b/presentation/src/main/res/values-tl/strings.xml index 2426c7eac..3c3e47a71 100644 --- a/presentation/src/main/res/values-tl/strings.xml +++ b/presentation/src/main/res/values-tl/strings.xml @@ -30,9 +30,11 @@ Sumulat Laktawan Magpatuloy + Add person Tawagan Mga detalye Save to gallery + Share Open navigation drawer %d selected Clear @@ -79,6 +81,10 @@ Forward Delete + Choose a phone number + %s ∙ Default + Just once + Always %d selected %1$d of %2$d results Send as group message @@ -118,6 +124,7 @@ Delivered %s Failed to send. Tap to try again Details + Address copied Conversation title Notifications Theme @@ -178,6 +185,7 @@ Pure black night mode Start time End time + Automatic contact colors Font size Use system font Automatic emoji @@ -188,6 +196,7 @@ Button 2 Button 3 Notification previews + Wake screen Vibration Sound None @@ -217,6 +226,8 @@ Remove accents from characters in outgoing SMS messages Mobile numbers only When composing a message, only show mobile numbers + Send long messages as MMS + If your longer text messages are failing to send, or sending in the wrong order, you can send them as MMS messages instead. Additional charges may apply Auto-compress MMS attachments Sync messages Re-sync your messages with the native Android SMS database @@ -352,9 +363,10 @@ Long + Automatic 100KB 200KB - 300KB (Recommended) + 300KB 600KB 1000KB 2000KB diff --git a/presentation/src/main/res/values-tr/strings.xml b/presentation/src/main/res/values-tr/strings.xml index 04af6a62d..7c07d2f28 100644 --- a/presentation/src/main/res/values-tr/strings.xml +++ b/presentation/src/main/res/values-tr/strings.xml @@ -30,9 +30,11 @@ Bir isme veya numaraya yazın Atla Devam + Kişi ekle Ara Ayrıntılar Galeriye Kaydet + Paylaş Gezinti çubuğunu aç %d Seçili Temizle @@ -83,6 +85,10 @@ Yönlendir Sil + Bir telefon numarası seçin + %s ∙ Varsayılan + Sadece bir kez + Her zaman %d Seçili %1$d / %2$d sonuçlar Grup mesajı gönder @@ -122,6 +128,7 @@ Teslim Edildi %s Gönderilemedi. Yeniden denemek için dokunun Ayrıntılar + Adres kopyalandı Konuşma başlığı Bildirimler Tema @@ -182,6 +189,7 @@ Saf siyah gece modu Başlama zamanı Bitiş Zamanı + Otomatik kişi renkleri Yazı tipi boyutu Sistem yazı tipini kullan Otomatik emoji @@ -192,6 +200,7 @@ Buton 2 Buton 3 Bildirim ön izleme + Uyandırma ekranı Titreşim Ses Hiçbiri @@ -221,6 +230,8 @@ Giden SMS mesajlarından aksanlı karakterleri kaldırın Sadece cep telefonu numaraları Bir mesaj oluştururken, yalnızca cep telefonu numaralarını göster + Uzun mesajları MMS olarak gönder + Uzun metin mesajlarınız gönderilemiyorsa veya yanlış sırada gönderiliyorsa, onları MMS mesajları olarak gönderebilirsiniz. Ek ücretler uygulanabilir Otomatik-MMS ekleri sıkıştır İletileri eşitle İletilerinizi Android SMS veritabanı ile yeniden senkronize edin @@ -358,12 +369,13 @@ BAĞLAM İSTEĞİ Uzun - 100 kilobayt - 200 kilobayt - 300KB (önerilen) - 600 kilobayt - 1000 kilobayt - 2000 kilobayt + Otomatik + 100KB + 200KB + 300KB + 600KB + 1000KB + 2000KB Sıkıştırma yok diff --git a/presentation/src/main/res/values-uk/strings.xml b/presentation/src/main/res/values-uk/strings.xml index 5c932e457..0dd7b36f5 100644 --- a/presentation/src/main/res/values-uk/strings.xml +++ b/presentation/src/main/res/values-uk/strings.xml @@ -30,9 +30,11 @@ Кому: ім\'я або номер Пропустити Продовжити + Додати контакт Зателефонувати Деталі Зберегти в галереї + Поділитися Відкрити панель навігації Обрано: %d Очистити @@ -83,6 +85,10 @@ Переслати Видалити + Виберіть номер телефону + %s ∙ початково + Один раз + Завжди Обрано: %d %1$d із %2$d результатів Надіслати як групове повідомлення @@ -122,6 +128,7 @@ Доставлено %s Не вдалося надіслати. Торкніться, щоб повторити спробу Деталі + Адреса скопійована Заголовок бесіди Сповіщення Тема @@ -132,7 +139,7 @@ Видалити бесіду Не вдалося завантажити медіа Збережено в галереї - Резервне копіювання та відновлення + Резервне копіювання Резервне копіювання повідомлень Відновити з резервної копії Остання резервна копія @@ -161,7 +168,7 @@ Збереження резервної копії… Синхронізація повідомлень… Готово! - Резервне копіювання та відновлення + Резервне копіювання Заплановані Автоматично надсилайте повідомлення саме в той час, який ви захочете Агов! Коли в тебе день народження? @@ -184,6 +191,7 @@ Повністю чорний нічний режим Час початку Час завершення + Автоматичні кольори контактів Розмір шрифта Використовувати системний шрифт Автоматичні emoji @@ -194,6 +202,7 @@ Кнопка 2 Кнопка 3 Попередній перегляд сповіщень + Екран заблокований Вібро Звук Ні @@ -223,6 +232,8 @@ Видалення акцентів з символів у вихідних SMS-повідомленнях Лише номери мобільних При написанні повідомлення показувати лише мобільні номери + Надсилати довгі повідомлення як MMS + Якщо частини довгого повідомлення не відправляються або роблять це в хибному порядку, ви можете відправити їх як MMS натомість. Оплата згідно тарифів вашого оператора Автоматичне стиснення MMS-вкладень Синхронізація повідомлень Повторна синхронізація ваших повідомлень з власною базою даних SMS Android @@ -266,10 +277,7 @@ Про додаток Версія - Розробник Вихідний код - Журнал змін - Контакт Ліцензія Авторське право Підтримати подальший розвиток, розблокувати все @@ -364,9 +372,10 @@ Довга + Автоматично 100 Кб 200 Кб - 300 Кб (рекомендовано) + 300 Кб 600 Кб 1000 Кб 2000 Кб diff --git a/presentation/src/main/res/values-ur/strings.xml b/presentation/src/main/res/values-ur/strings.xml index 8270ae2c2..ded507f87 100644 --- a/presentation/src/main/res/values-ur/strings.xml +++ b/presentation/src/main/res/values-ur/strings.xml @@ -30,9 +30,11 @@ Type a name or number Skip Continue + Add person Call Details Save to gallery + Share Open navigation drawer %d selected Clear @@ -79,6 +81,10 @@ Forward Delete + Choose a phone number + %s ∙ Default + Just once + Always %d selected %1$d of %2$d results Send as group message @@ -118,6 +124,7 @@ Delivered %s Failed to send. Tap to try again Details + Address copied Conversation title Notifications Theme @@ -178,6 +185,7 @@ Pure black night mode Start time End time + Automatic contact colors Font size Use system font Automatic emoji @@ -188,6 +196,7 @@ Button 2 Button 3 Notification previews + Wake screen Vibration Sound None @@ -217,6 +226,8 @@ Remove accents from characters in outgoing SMS messages Mobile numbers only When composing a message, only show mobile numbers + Send long messages as MMS + If your longer text messages are failing to send, or sending in the wrong order, you can send them as MMS messages instead. Additional charges may apply Auto-compress MMS attachments Sync messages Re-sync your messages with the native Android SMS database @@ -352,9 +363,10 @@ Long + Automatic 100KB 200KB - 300KB (Recommended) + 300KB 600KB 1000KB 2000KB diff --git a/presentation/src/main/res/values-vi/strings.xml b/presentation/src/main/res/values-vi/strings.xml index 6a292a30c..59b58b46c 100644 --- a/presentation/src/main/res/values-vi/strings.xml +++ b/presentation/src/main/res/values-vi/strings.xml @@ -30,9 +30,11 @@ Nhập tên hoặc số điện thoại Bỏ qua Tiếp tục + Thêm người Cuộc gọi Chi tiết Lưu vào bộ sưu tập + Chia sẻ Mở menu điều hướng %d đã chọn Dọn sạch @@ -78,6 +80,10 @@ Chuyển tiếp Xóa + Chọn một số điện thoại + %s ∙ Mặc định + Chỉ lần này + Luôn luôn %d đã chọn %1$d trên %2$d kết quả Gửi theo nhóm tin nhắn @@ -117,6 +123,7 @@ Đã chuyển %s Gửi thất bại. Bấm để thử lại lần nữa Chi tiết + Địa chỉ đã được sao chép Tiêu đề cuộc trò chuyện Thông báo Chủ đề @@ -156,7 +163,7 @@ Sao lưu và khôi phục Hẹn giờ tin nhắn Tự động gửi tin nhắn vào lúc mà bạn muốn - Này, khi nào thì tới sinh nhật bạn ? + Này, khi nào thì đến sinh nhật bạn? Vào ngày 23 tháng 12 đó Chúc mừng sinh nhật! Nhìn xem, tôi đúng là một người bạn tuyệt vời, nhớ ngày sinh nhật của bạn đấy @@ -176,6 +183,7 @@ Chế độ ban đêm tinh khiết Thời gian bắt đầu Thời gian kết thúc + Tự động hiện màu sắc của liên hệ Cỡ chữ Sử dụng font chữ hệ thống Emoji tự động @@ -186,6 +194,7 @@ Nút 2 Nút 3 Xem trước các thông báo + Đánh thức màn hình Rung Âm thanh Không dùng @@ -215,6 +224,8 @@ Bỏ dấu ký tự khi gửi SMS Lọc số di động Khi soạn tin mới, chỉ hiển thị liên hệ có số di động + Gửi đoạn tin nhắn dài như MMS + Nếu đoạn tin nhắn dài bị lỗi khi gửi, hoặc gửi sai thứ tự, bạn có thể gửi chúng như tin nhắn MMS thay thế. Cước phí bổ sung có thể bị áp dụng Tự động nén các tệp đính kèm MMS Đồng bộ tin nhắn Đồng bộ lại tin nhắn với thiết bị @@ -347,12 +358,13 @@ Dài + Tự động 100KB 200KB - 300KB (khuyến nghị) + 300KB 600KB 1000KB - 2M + 2000KB Không nén diff --git a/presentation/src/main/res/values-zh-rCN/strings.xml b/presentation/src/main/res/values-zh-rCN/strings.xml index dfc9f9184..6eb7378b1 100644 --- a/presentation/src/main/res/values-zh-rCN/strings.xml +++ b/presentation/src/main/res/values-zh-rCN/strings.xml @@ -30,16 +30,18 @@ 撰写 跳过 继续 + 添加人物 致电 详情 保存到图库 + 分享 打开导航栏 已选择 %d 项 清除 存档 取消存档 删除 - Add to contacts + 添加到通讯录 固定到顶部 取消固定 标记为已读 @@ -47,7 +49,7 @@ 拦截 正在同步信息… 你:%s - Draft: %s + 草稿: %s 信息中的结果 %d 条信息 对话会在这里显示 @@ -78,6 +80,10 @@ 转发 删除 + 选择电话号码 + %s ∙ 默认 + 仅一次 + 始终 已选择%d个项目 已显示%2$d中的%1$d条结果 群发消息 @@ -117,6 +123,7 @@ 已送达 %s 发送失败。点击再试一次 详情 + 地址已复制 对话标题 通知 主题 @@ -165,8 +172,8 @@ 预约短信 现在发送 - Copy text - Delete + 复制文本 + 删除 外观 通用 @@ -176,6 +183,7 @@ 纯黑夜间模式 开始时间 结束时间 + 自动化联系人颜色 字体大小 使用系统字体 自动颜文字 @@ -186,6 +194,7 @@ 按钮2 按钮3 通知预览 + 唤醒屏幕 振动 铃声 @@ -210,11 +219,13 @@ 送达确认 确认短信已成功送达 签名 - Add a signature to the end of your messages + 在您的信息结尾添加签名 删除读音符号 删除短信中字母上的读音符号 仅限手机号码 撰写邮件时, 只显示手机号码 + 将长信息作为彩信发送 + 如果您无法发送较长的信息,或者是以错误的顺序发送,您可以将其作为彩信发送。可能会收取额外的费用 自动压缩彩信附件 同步消息 重新与安卓原生短信数据库进行同步 @@ -223,32 +234,32 @@ 启用了调试日志记录 调试日志记录已禁用 输入时长 (秒) - Blocking - Drop messages - Drop incoming messages from blocked senders instead of hiding them - Blocked conversations - Blocking Manager + 屏蔽 + 丢弃信息 + 丢弃屏蔽的发件人所发送的信息 + 已屏蔽的会话 + 屏蔽管理器 QKSMS - Built-in blocking functionality in QKSMS - Automatically filter your calls and messages in one convenient place! Community IQ™ allows you to prevent unwanted messages from community known spammers + QKSMS中内建的屏蔽功能 + 自动在适当的位置过滤您的来电和信息!Community IQ™允许您阻止社区中已知的垃圾发件人所发送的垃圾信息 使用\"Should I Answer\"应用过滤未知号码的消息 - Copy blocked numbers - Continue to %s and copy over your existing blocked numbers - Blocked numbers - Your blocked numbers will appear here - Block a new number - Block texts from - Phone number - Block - Blocked messages - Your blocked messages will appear here - Block - Unblock + 复制屏蔽的号码 + 继续前往%s并复制到您现有的已屏蔽号码 + 已屏蔽的号码 + 您所屏蔽的号码将会在这里显示 + 屏蔽新的号码 + 屏蔽文本自 + 电话号码 + 屏蔽 + 已屏蔽的信息 + 您所屏蔽的号码将会在这里显示 + 屏蔽 + 取消屏蔽 - Continue to %s and block these numbers + 继续前往%s并屏蔽这个(些)号码 - Continue to %s and allow these numbers + 继续前往%s并允许这个(些)号码 关于 版本 @@ -307,8 +318,8 @@ 呼叫 删除 - Yes - Continue + + 继续 取消 删除 保存 @@ -324,10 +335,10 @@ 消息未发送 给%s的短信发送失败 - System - Disabled - Always on - Automatic + 系统 + 已禁用 + 始终打开 + 自动 显示姓名和消息 @@ -347,13 +358,14 @@ + Automatic 100KB 200KB - 300KB (推荐) + 300KB 600KB 1000KB 2000KB - 不压缩 + No compression 好的 diff --git a/presentation/src/main/res/values-zh/strings.xml b/presentation/src/main/res/values-zh/strings.xml index b07969c26..5419f133e 100644 --- a/presentation/src/main/res/values-zh/strings.xml +++ b/presentation/src/main/res/values-zh/strings.xml @@ -30,9 +30,11 @@ 編輯 跳过 繼續 + 增加傳送人 致電 詳細資訊 保存到圖庫 + 分享 打開導航抽屜 已選擇%d項 清除 @@ -78,6 +80,10 @@ 轉寄 刪除 + 選一個號碼 + %s ∙ 預設 + 就這一次 + 總是如此 已選取 %d 個項目 已顯示 %2$d 條中的 %1$d 條結果 群發訊息 @@ -86,7 +92,6 @@ 連絡人卡片 排程於 所選時間必須是未來時間 - 您必須解鎖 QKSMS+ 以使用排程簡訊 已新增至排程訊息 寫一條訊息... 複製文本 @@ -118,7 +123,8 @@ 已傳遞 %s 發送失敗,點擊重試 詳細資訊 - 會話標題 + Address copied + 對話標題 通知 主題 存檔 @@ -136,7 +142,6 @@ 從未備份 還原 選擇備份 - 請解鎖 QKSMS+ 以使用備份和還原功能 正在進行備份… 正在進行還原… 從備份還原 @@ -178,6 +183,7 @@ 純黑色夜間模式 開始時間 結束時間 + Automatic contact colors 字體大小 使用系統字體 自動 emoji 表情 @@ -188,6 +194,7 @@ 按鈕2 按鈕3 通知預覽 + Wake screen 震動 音效 @@ -217,6 +224,8 @@ 刪除訊息中字母上的讀音符號 僅限手機號碼 撰寫訊息時,只顯示手機號碼 + Send long messages as MMS + If your longer text messages are failing to send, or sending in the wrong order, you can send them as MMS messages instead. Additional charges may apply 自動壓縮MMS附件 同步消息 重新與安卓原生訊息數據庫進行同步 @@ -254,10 +263,7 @@ 關於 版本 - 開發者 源代碼 - 更新日誌 - 聯絡資訊 授權 版權 支持開發,解鎖全部功能 @@ -319,7 +325,12 @@ 設置 復原 已複製 - 存檔的會話 + + + + 存檔的對話 + 您必須解鎖QKSMS+才能使用此功能 + %s條新訊息 @@ -349,13 +360,14 @@ + Automatic 100KB 200KB - 300KB (推荐) + 300KB 600KB 1000KB 2000KB - 不压缩 + No compression 好的 -- GitLab From b12440e2966a5f89f3db012338f55469de6d7f0f Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Sun, 12 Jan 2020 23:39:31 -0500 Subject: [PATCH 097/109] Increment to v3.8.0-beta1 --- presentation/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/presentation/build.gradle b/presentation/build.gradle index bcd8ef6bd..d7602dc76 100644 --- a/presentation/build.gradle +++ b/presentation/build.gradle @@ -31,8 +31,8 @@ android { applicationId "foundation.e.message" minSdkVersion 21 targetSdkVersion 29 - versionCode 2209 - versionName "3.7.10" + versionCode 2210 + versionName "3.8.0-beta1" setProperty("archivesBaseName", "QKSMS-v${versionName}") testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" -- GitLab From 00b791a9d87b358ac6ca51e43b61056f0db128e6 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Mon, 13 Jan 2020 18:53:57 -0500 Subject: [PATCH 098/109] Fix crash when starting new conversation Fixes #1555 --- .../java/com/moez/QKSMS/feature/compose/ComposeViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 618b6ec67..b9ed2df0f 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 @@ -254,7 +254,7 @@ class ComposeViewModel @Inject constructor( conversationRepo.getRecipients() .asSequence() .filter { recipient -> recipient.contact?.lookupKey == lookupKey } - .first { recipient -> phoneNumberUtils.compare(recipient.address, address) } + .firstOrNull { recipient -> phoneNumberUtils.compare(recipient.address, address) } ?: Recipient( address = address, contact = lookupKey?.let(contactRepo::getUnmanagedContact)) -- GitLab From a6803dd165d0917fa2f0998d0b3d6c9405ee95a7 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Mon, 13 Jan 2020 19:00:20 -0500 Subject: [PATCH 099/109] Fix conversation swipe actions --- .../QKSMS/feature/conversations/ConversationsAdapter.kt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/conversations/ConversationsAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/conversations/ConversationsAdapter.kt index cf741f840..8f1c00410 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/conversations/ConversationsAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/conversations/ConversationsAdapter.kt @@ -45,6 +45,11 @@ class ConversationsAdapter @Inject constructor( private val phoneNumberUtils: PhoneNumberUtils ) : QkRealmAdapter() { + init { + // This is how we access the threadId for the swipe actions + setHasStableIds(true) + } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkViewHolder { val layoutInflater = LayoutInflater.from(parent.context) val view = layoutInflater.inflate(R.layout.conversation_list_item, parent, false) @@ -109,6 +114,10 @@ class ConversationsAdapter @Inject constructor( holder.unread.setTint(colors.theme(recipient).theme) } + override fun getItemId(position: Int): Long { + return getItem(position)?.id ?: -1 + } + override fun getItemViewType(position: Int): Int { return if (getItem(position)?.unread == false) 0 else 1 } -- GitLab From 407b3a5d8d885e0822fc9a2080a378442f2ee574 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Mon, 13 Jan 2020 21:01:01 -0500 Subject: [PATCH 100/109] Increment to v3.8.0-beta2 --- presentation/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/presentation/build.gradle b/presentation/build.gradle index d7602dc76..50e473ef5 100644 --- a/presentation/build.gradle +++ b/presentation/build.gradle @@ -31,8 +31,8 @@ android { applicationId "foundation.e.message" minSdkVersion 21 targetSdkVersion 29 - versionCode 2210 - versionName "3.8.0-beta1" + versionCode 2211 + versionName "3.8.0-beta2" setProperty("archivesBaseName", "QKSMS-v${versionName}") testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" -- GitLab From 760f720a5581f38bc95cc8701e15aece68fb59a5 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Fri, 17 Jan 2020 20:17:43 -0500 Subject: [PATCH 101/109] Updated translations --- presentation/src/main/res/values-in/strings.xml | 4 ++-- presentation/src/main/res/values-it/strings.xml | 4 ++-- .../src/main/res/values-pt-rBR/strings.xml | 16 ++++++++-------- presentation/src/main/res/values-pt/strings.xml | 16 ++++++++-------- presentation/src/main/res/values-sv/strings.xml | 8 ++++---- .../src/main/res/values-zh-rCN/strings.xml | 4 ++-- 6 files changed, 26 insertions(+), 26 deletions(-) diff --git a/presentation/src/main/res/values-in/strings.xml b/presentation/src/main/res/values-in/strings.xml index a3e8b1f69..ac7f4762c 100644 --- a/presentation/src/main/res/values-in/strings.xml +++ b/presentation/src/main/res/values-in/strings.xml @@ -361,14 +361,14 @@ Lama - Automatic + Otomatis 100KB 200KB 300KB 600KB 1000KB 2000KB - No compression + Tanpa kompresi Oke diff --git a/presentation/src/main/res/values-it/strings.xml b/presentation/src/main/res/values-it/strings.xml index f1c9144f8..78a3af64c 100644 --- a/presentation/src/main/res/values-it/strings.xml +++ b/presentation/src/main/res/values-it/strings.xml @@ -361,14 +361,14 @@ Lungo - Automatic + Automatico 100KB 200KB 300KB 600KB 1000KB 2000KB - No compression + Nessuna compressione Okay diff --git a/presentation/src/main/res/values-pt-rBR/strings.xml b/presentation/src/main/res/values-pt-rBR/strings.xml index 67b72081b..5e9715bbb 100644 --- a/presentation/src/main/res/values-pt-rBR/strings.xml +++ b/presentation/src/main/res/values-pt-rBR/strings.xml @@ -361,14 +361,14 @@ Longo - Automatic - 100KB - 200KB - 300KB - 600KB - 1000KB - 2000KB - No compression + Automático + 100 KB + 200 KB + 300 KB + 600 KB + 1000 KB + 2000 KB + Sem compressão Ok diff --git a/presentation/src/main/res/values-pt/strings.xml b/presentation/src/main/res/values-pt/strings.xml index f7a44e714..183372e84 100644 --- a/presentation/src/main/res/values-pt/strings.xml +++ b/presentation/src/main/res/values-pt/strings.xml @@ -367,14 +367,14 @@ Longo - Automatic - 100KB - 200KB - 300KB - 600KB - 1000KB - 2000KB - No compression + Automático + 100 KB + 200 KB + 300 KB + 600 KB + 1000 KB + 2000 KB + Sem compressão Ok diff --git a/presentation/src/main/res/values-sv/strings.xml b/presentation/src/main/res/values-sv/strings.xml index 37327cbe6..b869fada8 100644 --- a/presentation/src/main/res/values-sv/strings.xml +++ b/presentation/src/main/res/values-sv/strings.xml @@ -367,14 +367,14 @@ Lång - Automatic + Automatisk 100KB - 200KB + 200kB 300KB - 600KB + 600kB 1000KB 2000KB - No compression + Ingen komprimering Okej diff --git a/presentation/src/main/res/values-zh-rCN/strings.xml b/presentation/src/main/res/values-zh-rCN/strings.xml index 6eb7378b1..0f1a1c219 100644 --- a/presentation/src/main/res/values-zh-rCN/strings.xml +++ b/presentation/src/main/res/values-zh-rCN/strings.xml @@ -358,14 +358,14 @@ - Automatic + 自动 100KB 200KB 300KB 600KB 1000KB 2000KB - No compression + 不压缩 好的 -- GitLab From 24b37b7fc1db7ccea56a320966bfd3e3833930ba Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Fri, 17 Jan 2020 20:17:57 -0500 Subject: [PATCH 102/109] Increment to v3.8.0 --- presentation/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/presentation/build.gradle b/presentation/build.gradle index 50e473ef5..e31827235 100644 --- a/presentation/build.gradle +++ b/presentation/build.gradle @@ -31,8 +31,8 @@ android { applicationId "foundation.e.message" minSdkVersion 21 targetSdkVersion 29 - versionCode 2211 - versionName "3.8.0-beta2" + versionCode 2212 + versionName "3.8.0" setProperty("archivesBaseName", "QKSMS-v${versionName}") testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" -- GitLab From 0f7fd502cd70d04f833e6a15412fc9ded41a6f17 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Sat, 25 Jan 2020 13:35:47 -0500 Subject: [PATCH 103/109] Set the default MMS size back to 300kb --- domain/src/main/java/com/moez/QKSMS/util/Preferences.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt b/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt index e994247df..d783e0249 100644 --- a/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt +++ b/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt @@ -110,7 +110,7 @@ class Preferences @Inject constructor( val unicode = rxPrefs.getBoolean("unicode", false) val mobileOnly = rxPrefs.getBoolean("mobileOnly", false) val longAsMms = rxPrefs.getBoolean("longAsMms", false) - val mmsSize = rxPrefs.getInteger("mmsSize", -1) + val mmsSize = rxPrefs.getInteger("mmsSize", 300) val logging = rxPrefs.getBoolean("logging", false) init { -- GitLab From 84d9c7dda734848c4841828e953e7e98ac9c7fa3 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Mon, 27 Jan 2020 18:24:14 -0500 Subject: [PATCH 104/109] Perform QK migration only after realm migration --- data/src/main/java/com/moez/QKSMS/migration/QkMigration.kt | 4 ++-- .../src/main/java/com/moez/QKSMS/common/QKApplication.kt | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/data/src/main/java/com/moez/QKSMS/migration/QkMigration.kt b/data/src/main/java/com/moez/QKSMS/migration/QkMigration.kt index 32e192d38..beafeca1f 100644 --- a/data/src/main/java/com/moez/QKSMS/migration/QkMigration.kt +++ b/data/src/main/java/com/moez/QKSMS/migration/QkMigration.kt @@ -28,13 +28,13 @@ import kotlinx.coroutines.launch import javax.inject.Inject class QkMigration @Inject constructor( - context: Context, + private val context: Context, private val conversationRepo: ConversationRepository, private val prefs: Preferences, private val qksmsBlockingClient: QksmsBlockingClient ) { - init { + fun performMigration() { GlobalScope.launch { val oldVersion = prefs.version.get() diff --git a/presentation/src/main/java/com/moez/QKSMS/common/QKApplication.kt b/presentation/src/main/java/com/moez/QKSMS/common/QKApplication.kt index 8f94dbe03..51440e10a 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/QKApplication.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/QKApplication.kt @@ -82,6 +82,8 @@ class QKApplication : Application(), HasActivityInjector, HasBroadcastReceiverIn .schemaVersion(QkRealmMigration.SchemaVersion) .build()) + qkMigration.performMigration() + GlobalScope.launch(Dispatchers.IO) { referralManager.trackReferrer() } -- GitLab From e3ca866b9eaeb30c4d8c63689d3ef28e496dc9a2 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Mon, 27 Jan 2020 18:50:17 -0500 Subject: [PATCH 105/109] Fix crash when deleting conversation --- .../conversationinfo/ConversationInfoPresenter.kt | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoPresenter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoPresenter.kt index 2ad141bd7..07860ca2c 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoPresenter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoPresenter.kt @@ -80,14 +80,18 @@ class ConversationInfoPresenter @Inject constructor( disposables += markUnarchived disposables += deleteConversations - val partsObservable = messageRepo.getPartsForConversation(threadId) - .asObservable() - .filter { parts -> parts.isLoaded && parts.isValid} - disposables += Observables - .combineLatest(conversation, partsObservable) { conversation, parts -> + .combineLatest( + conversation, + messageRepo.getPartsForConversation(threadId).asObservable() + ) { conversation, parts -> val data = mutableListOf() + // If some data was deleted, this isn't the place to handle it + if (!conversation.isLoaded || !conversation.isValid || !parts.isLoaded || !parts.isValid) { + return@combineLatest + } + data += conversation.recipients.map(::ConversationInfoRecipient) data += ConversationInfoItem.ConversationInfoSettings( name = conversation.name, -- GitLab From 27a5333ae3c5418b6d2d5aac8458369db556002e Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Mon, 27 Jan 2020 18:57:54 -0500 Subject: [PATCH 106/109] Query for non-null contact group titles --- .../java/com/moez/QKSMS/mapper/CursorToContactGroupImpl.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/data/src/main/java/com/moez/QKSMS/mapper/CursorToContactGroupImpl.kt b/data/src/main/java/com/moez/QKSMS/mapper/CursorToContactGroupImpl.kt index f2f290b5d..ad9d6532c 100644 --- a/data/src/main/java/com/moez/QKSMS/mapper/CursorToContactGroupImpl.kt +++ b/data/src/main/java/com/moez/QKSMS/mapper/CursorToContactGroupImpl.kt @@ -35,7 +35,8 @@ class CursorToContactGroupImpl @Inject constructor( ContactsContract.Groups.TITLE) private const val SELECTION = "${ContactsContract.Groups.AUTO_ADD}=0 " + "AND ${ContactsContract.Groups.DELETED}=0 " + - "AND ${ContactsContract.Groups.FAVORITES}=0" + "AND ${ContactsContract.Groups.FAVORITES}=0 " + + "AND ${ContactsContract.Groups.TITLE} IS NOT NULL" private const val ID = 0 private const val TITLE = 1 -- GitLab From 7660e84f46d1ec3cc1447c974af299e43feaf51a Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Mon, 27 Jan 2020 19:03:04 -0500 Subject: [PATCH 107/109] Silence error --- .../java/com/moez/QKSMS/repository/MessageRepositoryImpl.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/src/main/java/com/moez/QKSMS/repository/MessageRepositoryImpl.kt b/data/src/main/java/com/moez/QKSMS/repository/MessageRepositoryImpl.kt index ca45deae9..1eb62d234 100644 --- a/data/src/main/java/com/moez/QKSMS/repository/MessageRepositoryImpl.kt +++ b/data/src/main/java/com/moez/QKSMS/repository/MessageRepositoryImpl.kt @@ -475,7 +475,7 @@ class MessageRepositoryImpl @Inject constructor( val addresses = pdu.to.map { it.string }.filter { it.isNotBlank() } val parts = message.parts.mapNotNull { part -> - val bytes = tryOrNull { + val bytes = tryOrNull(false) { context.contentResolver.openInputStream(part.getUri())?.use { inputStream -> inputStream.readBytes() } } ?: return@mapNotNull null -- GitLab From b31974271ea169acda184a5d0cad7042026c83ab Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Mon, 27 Jan 2020 19:06:37 -0500 Subject: [PATCH 108/109] Update translations --- .../src/main/res/values-ar/strings.xml | 34 ++-- .../src/main/res/values-cs/strings.xml | 16 +- .../src/main/res/values-el/strings.xml | 29 ++- .../src/main/res/values-fa/strings.xml | 167 +++++++++--------- .../src/main/res/values-ja/strings.xml | 14 +- .../src/main/res/values-pl/strings.xml | 32 ++-- 6 files changed, 142 insertions(+), 150 deletions(-) diff --git a/presentation/src/main/res/values-ar/strings.xml b/presentation/src/main/res/values-ar/strings.xml index df30ec316..efa7c8c43 100644 --- a/presentation/src/main/res/values-ar/strings.xml +++ b/presentation/src/main/res/values-ar/strings.xml @@ -30,18 +30,18 @@ أكتب اسماً أو رقماً تخطي متابعة - Add person + اضافة شخص اتصال تفاصيل حفظ إلى المعرض - Share + مشاركة فتح درج التنقل %d محددة مسح أرشفة إلغاء الأرشفة حذف - Add to contacts + اضافة الى القائمة التثبيت فوق إلغاء التثبيت علّمه مقروء @@ -85,10 +85,10 @@ إعادة توجيه حذف - Choose a phone number + اختر رقم الهاتف %s ∙ Default - Just once - Always + فقط واحد + دائما %d مختارة %1$d من %2$d النتائج إرسال رسالة جماعية @@ -128,7 +128,7 @@ تم إرسال %s فشل الإرسال. إلمس لإعادة المحاولة التفاصيل - Address copied + تم نسخ العنوان عنوان المحادثة الإشعارات السمة @@ -182,8 +182,8 @@ الرسالة المجدولة أرسل الآن - Copy text - Delete + نسخ النص + حذف المظهر عام @@ -204,7 +204,7 @@ الزر 2 الزر 3 معاينات الإشعارات - Wake screen + فتح الشاشة الاهتزاز الصوت بدون نغمة @@ -384,13 +384,13 @@ طويل - Automatic - 100KB - 200KB - 300KB - 600KB - 1000KB - 2000KB + تلقائي + 100كيلوبايت + 200كيلوبايت + 300كيلوبايت + 600كيلوبايت + 1000كيلوبايت + 2000كيلوبايت No compression diff --git a/presentation/src/main/res/values-cs/strings.xml b/presentation/src/main/res/values-cs/strings.xml index 3f2e4016b..69cae7a2d 100644 --- a/presentation/src/main/res/values-cs/strings.xml +++ b/presentation/src/main/res/values-cs/strings.xml @@ -377,14 +377,14 @@ Dlouhá - Automatic - 100KB - 200KB - 300KB - 600KB - 1000KB - 2000KB - No compression + Automaticky + 100 kB + 200 kB + 300 kB + 600 kB + 1 000 kB + 2 000 kB + Bez komprese OK diff --git a/presentation/src/main/res/values-el/strings.xml b/presentation/src/main/res/values-el/strings.xml index 8db4d043f..0afab790c 100644 --- a/presentation/src/main/res/values-el/strings.xml +++ b/presentation/src/main/res/values-el/strings.xml @@ -30,11 +30,11 @@ Πληκτρολογήστε ένα όνομα ή αριθμό Παράλειψη Συνέχεια - Add person + Προσθήκη ατόμου Κλήση Λεπτομέρειες Αποθήκευση στη συλλογή - Share + Κοινή χρήση Άνοιγμα πλαισίου πλοήγησης Επιλεγμένα: %d Εκκαθάριση @@ -81,10 +81,10 @@ Προώθηση Διαγραφή - Choose a phone number - %s ∙ Default - Just once - Always + Επιλέξτε έναν αριθμό τηλεφώνου + %s ∙ Προεπιλεγμένος + Μόνο μια φορά + Πάντα Επιλέχθηκαν: %d %1$d από %2$d αποτελέσματα Αποστολή ως ομαδικό μήνυμα @@ -93,7 +93,6 @@ Κάρτα επαφής Προγραμματίστηκε για Η χρονική στιγμή πρέπει να βρίσκεται στο μέλλον! - Πρέπει να ξεκλειδώσετε το QKSMS+ για να αποκτήσετε τη δυνατότητα προγραμματισμού μηνυμάτων Προστέθηκε στα προγραμματισμένα μηνύματα Γράψτε ένα μήνυμα… Αντιγραφή κειμένου @@ -125,7 +124,7 @@ Παραδόθηκε: %s Αποτυχία αποστολής. Πατήστε για νέα προσπάθεια Λεπτομέρειες - Address copied + Η διεύθυνση αντεγράφη Τίτλος συζήτησης Ειδοποιήσεις Θέμα εμφάνισης @@ -144,7 +143,6 @@ Ποτέ Επαναφορά Επιλέξτε ένα αντίγραφο ασφαλείας - Παρακαλώ ξεκλειδώστε το QKSMS+ για τη δημιουργία και επαναφορά αντιγράφων ασφαλείας Δημιουργία αντιγράφου ασφαλείας… Επαναφορά αντιγράφου ασφαλείας… Επαναφορά αντιγράφου ασφαλείας @@ -187,7 +185,7 @@ Νυχτερινή λειτουργία καθαρού μαύρου Ώρα έναρξης Ώρα λήξης - Automatic contact colors + Αυτόματος χρωματισμός επαφών Μέγεθος γραμματοσειράς Χρήση γραμματοσειράς συστήματος Αυτόματα emoji @@ -198,7 +196,7 @@ Κουμπί 2 Κουμπί 3 Προεπισκοπήσεις ειδοποιήσεων - Wake screen + Ενεργοποίηση οθόνης Δόνηση Ήχος Κανένα @@ -222,7 +220,7 @@ Επιβεβαιώσεις παράδοσης Confirm that messages were sent successfully - Signature + Υπογραφή Add a signature to the end of your messages Strip accents Remove accents from characters in outgoing SMS messages @@ -247,9 +245,9 @@ Ενσωματωμένη λειτουργία φραγής του QKSMS Automatically filter your calls and messages in one convenient place! Community IQ™ allows you to prevent unwanted messages from community known spammers Automatically filter messages from unsolicited numbers by using the \"Should I Answer\" app - Copy blocked numbers + Αντιγραφή αριθμών με φραγή Continue to %s and copy over your existing blocked numbers - Blocked numbers + Αριθμοί με φραγή Your blocked numbers will appear here Block a new number Block texts from @@ -269,10 +267,7 @@ Σχετικά με την εφαρμογή Έκδοση - Προγραμματιστής Πηγαίος κώδικας - Αρχείο αλλαγών - Επικοινωνία Άδεια χρήσης Πνευματικά δικαιώματα Support development, unlock everything diff --git a/presentation/src/main/res/values-fa/strings.xml b/presentation/src/main/res/values-fa/strings.xml index 239362ec8..b1f85dcbc 100644 --- a/presentation/src/main/res/values-fa/strings.xml +++ b/presentation/src/main/res/values-fa/strings.xml @@ -30,26 +30,26 @@ یک نام یا شماره را وارد کنید بیخیال ادامه - Add person + اضافه کردن شخص تماس جزئیات ذخیره در گالری - Share + اشتراک گذاری بازکردن کشو ناوبری %d انتخاب شده پاکسازی بایگانی بیرون آوردن از بایگانی حذف - Add to contacts + افزودن به مخاطبین اتصال به بالای صفحه - Unpin + رها کردن علامت گذاری به عنوان خوانده شده علامت گذاری بعنوان خوانده نشده مسدود کردن همگام سازی پیام ها… شما: %s - Draft: %s + پیش نویس: %s نتایج در پیام ها %d پیام مکالمات اینجا نمایش داده می‌شود @@ -83,28 +83,28 @@ فوروارد حذف - Choose a phone number - %s ∙ Default - Just once - Always + یک شماره تلفن انتخاب کنید + %s ∙ پیش فرض + فقط یک بار + همیشه %d انتخاب شده - %1$d of %2$d results + نتایج %1$d از %2$d ارسال پیام به گروه - Recipients and replies will be visible to everyone - This is the start of your conversation. Say something nice! + دریافت کنندگان و پاسخ ها برای همه قابل مشاهده خواهد بود + این آغاز مکالمه شماست. یه چیز خوب بگو! کارت تماس Scheduled for Selected time must be in the future! Added to scheduled messages نوشتن پیام… کپی کردن متن - Forward + ارسال به دیگری حذف قبلی بعدی پاکسازی جزئیات پیام - Type: %s + نوع: %s از: %s به: %s موضوع: %s @@ -116,17 +116,17 @@ کد خطا: %d افزودن پیوست پیوست یک عکس - Take a photo + عکس گرفتن زمان بندی پیام - Attach a contact - Error reading contact + یک مخاطب ضمیمه کنید + خطا در خواندن مخاطب %s برگزیده شده، تغییر سیم کارت فرستادن پیام فرستادن… رسیده %s در ارسال خطایی رخ داد. برای ارسال دوباره، ضربه بزنید جزئیات - Address copied + آدرس کپی شد عنوان گفتگو اعلانها پوسته @@ -135,7 +135,7 @@ مسدود کردن رفع مسدودیت حذف مکالمه - Couldn\'t load media + بارگیری رسانه امکان پذیر نیست ذخیره در گالری پشتیبان گیری و بازیابی پشتیبان گیری از پیام‌ها @@ -150,64 +150,64 @@ Restore from backup آیا مطمئن هسیتد که می‌خواهید پیغام‌هایتان را از پشتيباني بازیابی کنید? بازیابی را متوقف کن - Messages that have already been restored will remain on your device + پیام هایی که قبلاً بازیابی شده اند در دستگاه شما باقی خواهند ماند پشتیبان‌ها نسخه ی پشتیبان پیدا نشد - %d message - %d messages + %d پیام + %d پیام - Currently, only SMS is supported by Backup and Restore. MMS support and scheduled backups will be coming soon! + در حال حاضر ، فقط پیامک پشتیبان گیری و بازیابی پشتیبانی می شود. پشتیبان‌گیری و برنامه‌ریزی برای پشتیبان‌گیری از MMS به زودی ارائه می شود! همین حالا نسخه پشتیبان تهیه کن برگردانی بک اپ %d/%d پیام - Saving backup… - Syncing messages… - Finished! - Backup and restore - Scheduled - Automatically send a message, at the exact moment you\'d like - Hey! When was your birthday again? - It\'s on December 23rd - Happy birthday! Look at what a great friend I am, remembering your birthday + ذخیره نسخه پشتیبان… + در حال همگام سازی پیام‌ها… + تمام شد! + پشتیبان گیری و بازیابی + برنامه ریزی + در همان لحظه ای که می خواهید پیام بصورت خودکار ارسال کنید + سلام! کی دوباره تولدت بود؟ + 23 دسامبر است + تولدت مبارک! نگاه کنید که چه دوست خوبی دارم ، به یاد تولدت - Sending on December 23rd  - Schedule a message - Scheduled message + ارسال در 23 دسامبر + یک پیام تنظیم کنید + پیام برنامه ریزی شده ارسال هم‌اکنون - Copy text - Delete + کپی متن + حذف ظاهر عمومی - QK Reply + پاسخ QK پوسته حالت شب حالت شب کامل زمان شروع زمان پایان - Automatic contact colors + رنگ مخاطب خودکار اندازه قلم از فونت سیستم استفاده کن ایموجی خودکار اعلان‌ها از فونت سیستم استفاده کن - Actions - Button 1 - Button 2 - Button 3 + اقدامات + دکمه 1 + دکمه 2 + دکمه 3 بازبینی اعلان - Wake screen + صفحه بیدار لرزش صدای هیچی پاسخ QK پنجره پیام های جدید برای رد کردن ضربه بزنید - Tap outside of the popup to close it + برای بستن آن ، به بیرون پنجره ضربه بزنید ارسال با تاخیر - Swipe actions + اقدامات کشیدن تنظیم کشیدن برای گفتگو به راست بکشید به چپ بکشید @@ -223,13 +223,13 @@ تأیید تحویل تأیید ارسال موفق پیام امضا - Add a signature to the end of your messages + به انتهای پیام های خود یک امضا اضافه کنید حذف لهجه‌ها حذف کاراکترهای اضافه پیام ارسالی فقط شماره های تلفن وقتی در حال ارسال پیام هستید فقط می توانید شماره تلفن را ببنید - Send long messages as MMS - If your longer text messages are failing to send, or sending in the wrong order, you can send them as MMS messages instead. Additional charges may apply + پیام های طولانی را به عنوان MMS ارسال کن + اگر پیام های متنی طولانی شما نتوانسته اند ارسال شوند یا به ترتیب اشتباه ارسال شوند ، می توانید به جای آنها پیام های MMS ارسال کنید. هزینه های اضافی ممکن است اعمال شود فشرده سازی خودکار پیوست ها همگام سازی پیام ها همگام سازی پیام ها با پایگاه داده @@ -238,41 +238,38 @@ ورود به سیستم دیباگ فعال است ورد به سیستم دیباگ غیرفعال است زمان (ثانیه) را وارد کنید - Blocking - Drop messages - Drop incoming messages from blocked senders instead of hiding them - Blocked conversations - Blocking Manager + انسداد + دور انداختم پیام‌ها + پیام های ورودی را از فرستنده های مسدود شده به جای مخفی کردن آنها دور اندازید + مکالمات مسدود شده + مدیریت انسداد QKSMS - Built-in blocking functionality in QKSMS - Automatically filter your calls and messages in one convenient place! Community IQ™ allows you to prevent unwanted messages from community known spammers + عملکرد مسدود کننده داخلی در QKSMS + تماس ها و پیام های خود را بطور خودکار فیلتر کنید! IQ Community به شما اجازه می دهد تا از پیام های ناخواسته توسط اسپم های شناخته شده در جامعه جلوگیری کنید با استفاده از برنامه «باید جواب بده»، پیامها را از شماره های ناخواسته به طور خودکار فیلتر کنید - Copy blocked numbers - Continue to %s and copy over your existing blocked numbers - Blocked numbers - Your blocked numbers will appear here - Block a new number - Block texts from - Phone number - Block - Blocked messages - Your blocked messages will appear here - Block - Unblock + کپی شماره های مسدود شده + به %s ادامه دهید و از شماره های مسدود شده موجود خود کپی کنید + شماره‌های مسدود شده + شماره های مسدود شده شما در اینجا ظاهر می شوند + انسداد شماره جدید + انسداد پیام از طرف + شماره تلفن + انسداد + پیام‌های مسدود شده + پیامهای مسدود شده شما در اینجا ظاهر می شوند + انسداد + رفع مسدوديت - Continue to %s and block this number - Continue to %s and block these numbers + به %s ادامه دهید و این شماره را مسدود کن + به %s ادامه دهید و این شماره‌ها را مسدود کن - Continue to %s and allow this number - Continue to %s and allow these numbers + به %s ادامه دهید و به این شماره اجازه بده + به %s ادامه دهید و به این شماره‌ها اجازه بده درباره نسخه - توسعه دهنده کد منبع - Changelog - مخاطب مجوز حق نشر حمایت از تولید کننده،بازشدن همه چیز @@ -324,8 +321,8 @@ تماس پاک کردن - Yes - Continue + بله + ادامه لغو حذف ذخیره @@ -365,14 +362,14 @@ طولانی - Automatic - 100KB - 200KB - 300KB - 600KB - 1000KB - 2000KB - No compression + خودکار + ۱۰۰ کیلوبایت + ۲۰۰ کیلوبایت + ۳۰۰ کیلوبایت + ۶۰۰ کیلوبایت + ۱۰۰۰ کیلوبایت + ۲۰۰۰ کیلوبایت + بدون فشرده‌سازی باشه diff --git a/presentation/src/main/res/values-ja/strings.xml b/presentation/src/main/res/values-ja/strings.xml index 3e2eec64b..c8c21eac1 100644 --- a/presentation/src/main/res/values-ja/strings.xml +++ b/presentation/src/main/res/values-ja/strings.xml @@ -123,7 +123,7 @@ %s を配信しました 送信に失敗しました。タップすると、もう一度やり直します 詳細 - Address copied + アドレスをコピーしました 会話のタイトル 通知 テーマ @@ -183,7 +183,7 @@ ピュアブラック夜間モード 開始時刻 終了時刻 - Automatic contact colors + 自動的に連絡先に色をつける フォントサイズ システムフォントを使用する 自動絵文字 @@ -194,7 +194,7 @@ ボタン 2 ボタン 3 通知プレビュー - Wake screen + ウェイクスクリーン 振動 サウンド 無し @@ -224,8 +224,8 @@ 送信する SMS メッセージの文字からアクセントを削除します 携帯電話番号のみ メッセージを作成するときは、携帯電話番号のみを表示します - Send long messages as MMS - If your longer text messages are failing to send, or sending in the wrong order, you can send them as MMS messages instead. Additional charges may apply + 長いメッセージをMMSとして送信する + 長いテキストメッセージの送信が失敗する場合、または間違った順序で送信される場合は、代わりにMMSメッセージとして送信できます。 追加料金が適用される場合があります MMS添付ファイルの自動圧縮 メッセージを同期 メッセージをネイティブの Android SMS データベースと再同期します @@ -358,14 +358,14 @@ 長い - Automatic + 自動 100KB 200KB 300KB 600KB 1000KB 2000KB - No compression + 圧縮なし OK diff --git a/presentation/src/main/res/values-pl/strings.xml b/presentation/src/main/res/values-pl/strings.xml index 5432e5f3d..33f10e837 100644 --- a/presentation/src/main/res/values-pl/strings.xml +++ b/presentation/src/main/res/values-pl/strings.xml @@ -130,7 +130,7 @@ Dostarczono %s Nie udało się wysłać wiadomości. Dotknij, aby spróbować ponownie Szczegóły - Address copied + Adres został skopiowany Tytuł rozmowy Powiadomienia Motyw @@ -193,7 +193,7 @@ Czarne tło w trybie nocnym Czas rozpoczęcia Czas zakończenia - Automatic contact colors + Automatyczne kolory kontaktów Rozmiar czcionki Używaj czcionki systemowej Automatyczne emoji @@ -204,7 +204,7 @@ Przycisk 2 Przycisk 3 Zawartość powiadomień - Wake screen + Wybudzaj ekran Wibracje Dźwięki Brak @@ -234,9 +234,9 @@ Usuwaj ogonki ze znaków w wiadomościach Tylko numery komórkowe Pokazuj kontakty tylko z numerami komórkowymi - Send long messages as MMS - If your longer text messages are failing to send, or sending in the wrong order, you can send them as MMS messages instead. Additional charges may apply - Automatyczna kompresja załączników MMSów + Wysyłaj długie wiadomości jako MMS + Jeśli dłuższe wiadomości tekstowe nie są wysyłane lub wysyłane są w złej kolejności, możesz wysyłać je jako wiadomości MMS. Mogą zostać naliczone dodatkowe opłaty + Kompresja załączników MMSów Synchronizuj wiadomości Zsynchronizuj wiadomości z systemową bazą danych O aplikacji Message @@ -268,13 +268,13 @@ Kontynuuj do %s i blokuj ten numer Kontynuuj do %s i blokuj te numery - Continue to %s and block these numbers + Kontynuuj do %s i blokuj te numery Kontynuuj do %s i blokuj te numery Kontynuuj do %s i odblokuj ten numer Kontynuuj do %s i odblokuj te numery - Continue to %s and allow these numbers + Kontynuuj do %s i odblokuj te numery Kontynuuj do %s i odblokuj te numery O aplikacji @@ -377,14 +377,14 @@ Długi - Automatic - 100KB - 200KB - 300KB - 600KB - 1000KB - 2000KB - No compression + Automatyczna + 100 KB + 200 KB + 300 KB + 600 KB + 1000 KB + 2000 KB + Bez kompresji OK -- GitLab From f9960ac2eefde8bd3cb6c705e1aa4e32f55acd86 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Mon, 27 Jan 2020 19:07:04 -0500 Subject: [PATCH 109/109] Update to v3.8.1 --- presentation/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/presentation/build.gradle b/presentation/build.gradle index e31827235..271e21dce 100644 --- a/presentation/build.gradle +++ b/presentation/build.gradle @@ -31,8 +31,8 @@ android { applicationId "foundation.e.message" minSdkVersion 21 targetSdkVersion 29 - versionCode 2212 - versionName "3.8.0" + versionCode 2213 + versionName "3.8.1" setProperty("archivesBaseName", "QKSMS-v${versionName}") testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" -- GitLab