From bd6c5ca5e549760c0e3d40346ae06221db9934c3 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Sat, 9 Jun 2018 01:23:22 -0400 Subject: [PATCH] #45 - Rename group chats --- .../main/java/migration/QkRealmMigration.kt | 10 +++++- .../java/repository/MessageRepositoryImpl.kt | 11 +++++++ .../java/repository/SyncRepositoryImpl.kt | 13 ++++++-- domain/src/main/java/model/Conversation.kt | 7 ++++- .../main/java/repository/MessageRepository.kt | 2 ++ .../java/feature/compose/ComposeViewModel.kt | 3 +- .../ConversationInfoActivity.kt | 31 +++++++++++++++++++ .../conversationinfo/ConversationInfoState.kt | 1 + .../conversationinfo/ConversationInfoView.kt | 5 ++- .../ConversationInfoViewModel.kt | 31 ++++++++++++++++--- .../res/drawable/ic_people_black_24dp.xml | 27 ++++++++++++++++ .../res/layout/conversation_info_activity.xml | 8 +++++ presentation/src/main/res/values/strings.xml | 3 ++ 13 files changed, 140 insertions(+), 12 deletions(-) create mode 100644 presentation/src/main/res/drawable/ic_people_black_24dp.xml diff --git a/data/src/main/java/migration/QkRealmMigration.kt b/data/src/main/java/migration/QkRealmMigration.kt index eb3fb10a5..24cc05988 100644 --- a/data/src/main/java/migration/QkRealmMigration.kt +++ b/data/src/main/java/migration/QkRealmMigration.kt @@ -19,13 +19,14 @@ package migration import io.realm.DynamicRealm +import io.realm.FieldAttribute import io.realm.RealmMigration class QkRealmMigration : RealmMigration { companion object { - const val SCHEMA_VERSION: Long = 2 + const val SCHEMA_VERSION: Long = 3 } override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { @@ -45,6 +46,13 @@ class QkRealmMigration : RealmMigration { version++ } + if (version == 2L) { + realm.schema.get("Conversation") + ?.addField("name", String::class.java, FieldAttribute.REQUIRED) + + version++ + } + if (version < newVersion) { throw IllegalStateException("Migration missing from v$oldVersion to v$newVersion") } diff --git a/data/src/main/java/repository/MessageRepositoryImpl.kt b/data/src/main/java/repository/MessageRepositoryImpl.kt index 3e80ef9d1..f84c612eb 100644 --- a/data/src/main/java/repository/MessageRepositoryImpl.kt +++ b/data/src/main/java/repository/MessageRepositoryImpl.kt @@ -93,6 +93,17 @@ class MessageRepositoryImpl @Inject constructor( .findAll()) } + override fun setConversationName(id: Long, name: String) { + Realm.getDefaultInstance().use { realm -> + realm.executeTransaction { + realm.where(Conversation::class.java) + .equalTo("id", id) + .findFirst() + ?.name = name + } + } + } + override fun searchConversations(query: String): List { val conversations = getConversationsSnapshot() diff --git a/data/src/main/java/repository/SyncRepositoryImpl.kt b/data/src/main/java/repository/SyncRepositoryImpl.kt index 0acd67be5..b489dce31 100644 --- a/data/src/main/java/repository/SyncRepositoryImpl.kt +++ b/data/src/main/java/repository/SyncRepositoryImpl.kt @@ -59,7 +59,11 @@ class SyncRepositoryImpl @Inject constructor( /** * Holds data that should be persisted across full syncs */ - private data class PersistedData(val id: Long, val archived: Boolean, val blocked: Boolean) + private data class PersistedData( + val id: Long, + val archived: Boolean, + val blocked: Boolean, + val name: String) override val syncProgress: Subject = BehaviorSubject.createDefault(SyncRepository.SyncProgress.Idle()) @@ -77,9 +81,11 @@ class SyncRepositoryImpl @Inject constructor( .equalTo("archived", true) .or() .equalTo("blocked", true) + .or() + .isNotEmpty("name") .endGroup() .findAll() - .map { PersistedData(it.id, it.archived, it.blocked) } + .map { PersistedData(it.id, it.archived, it.blocked, it.name) } realm.delete(Contact::class.java) realm.delete(Conversation::class.java) @@ -102,7 +108,7 @@ class SyncRepositoryImpl @Inject constructor( persistedData += oldBlockedSenders.get() .map { threadIdString -> threadIdString.toLong() } .filter { threadId -> persistedData.none { it.id == threadId } } - .map { threadId -> PersistedData(threadId, false, true) } + .map { threadId -> PersistedData(threadId, false, true, "") } // Sync conversations cursorToConversation.getConversationsCursor()?.use { conversationCursor -> @@ -113,6 +119,7 @@ class SyncRepositoryImpl @Inject constructor( val conversation = conversations.firstOrNull { conversation -> conversation.id == data.id } conversation?.archived = data.archived conversation?.blocked = data.blocked + conversation?.name = data.name } realm.insertOrUpdate(conversations) diff --git a/domain/src/main/java/model/Conversation.kt b/domain/src/main/java/model/Conversation.kt index e4ac36c18..f7d66cf9c 100644 --- a/domain/src/main/java/model/Conversation.kt +++ b/domain/src/main/java/model/Conversation.kt @@ -36,5 +36,10 @@ open class Conversation : RealmObject() { var me: Boolean = false var draft: String = "" - fun getTitle() = recipients.joinToString { recipient -> recipient.getDisplayName() } + // For group chats, the user is allowed to set a custom title for the conversation + var name: String = "" + + fun getTitle(): String { + return name.takeIf { it.isNotBlank() } ?: recipients.joinToString { recipient -> recipient.getDisplayName() } + } } diff --git a/domain/src/main/java/repository/MessageRepository.kt b/domain/src/main/java/repository/MessageRepository.kt index 1789a727d..7ee454e1e 100644 --- a/domain/src/main/java/repository/MessageRepository.kt +++ b/domain/src/main/java/repository/MessageRepository.kt @@ -31,6 +31,8 @@ interface MessageRepository { fun getConversationsSnapshot(): List + fun setConversationName(id: Long, name: String) + fun searchConversations(query: String): List fun getBlockedConversations(): RealmResults diff --git a/presentation/src/main/java/feature/compose/ComposeViewModel.kt b/presentation/src/main/java/feature/compose/ComposeViewModel.kt index 6e8cef1e6..332da0ff9 100644 --- a/presentation/src/main/java/feature/compose/ComposeViewModel.kt +++ b/presentation/src/main/java/feature/compose/ComposeViewModel.kt @@ -166,8 +166,7 @@ class ComposeViewModel @Inject constructor( } .filter { conversation -> conversation.isValid } .filter { conversation -> conversation.id != 0L } - .distinctUntilChanged() - .subscribe { conversation.onNext(it) } + .subscribe(conversation::onNext) // When the conversation changes, update the threadId and the messages for the adapter disposables += conversation diff --git a/presentation/src/main/java/feature/conversationinfo/ConversationInfoActivity.kt b/presentation/src/main/java/feature/conversationinfo/ConversationInfoActivity.kt index 47cd03c2b..f8e8284c8 100644 --- a/presentation/src/main/java/feature/conversationinfo/ConversationInfoActivity.kt +++ b/presentation/src/main/java/feature/conversationinfo/ConversationInfoActivity.kt @@ -22,6 +22,7 @@ import android.arch.lifecycle.ViewModelProvider import android.arch.lifecycle.ViewModelProviders import android.os.Bundle import android.support.v7.app.AlertDialog +import android.text.InputFilter import com.jakewharton.rxbinding2.view.clicks import com.moez.QKSMS.R import com.uber.autodispose.android.lifecycle.scope @@ -29,10 +30,14 @@ import com.uber.autodispose.kotlin.autoDisposable import common.Navigator import common.base.QkThemedActivity import common.util.extensions.animateLayoutChanges +import common.util.extensions.dpToPx +import common.util.extensions.resolveThemeColor import common.util.extensions.scrapViews import common.util.extensions.setVisible +import common.widget.QkEditText import dagger.android.AndroidInjection import io.reactivex.subjects.PublishSubject +import io.reactivex.subjects.Subject import kotlinx.android.synthetic.main.conversation_info_activity.* import javax.inject.Inject @@ -44,6 +49,8 @@ class ConversationInfoActivity : QkThemedActivity(), ConversationInfoView { @Inject lateinit var itemDecoration: GridSpacingItemDecoration @Inject lateinit var viewModelFactory: ViewModelProvider.Factory + override val nameIntent by lazy { name.clicks() } + override val nameChangedIntent: Subject = PublishSubject.create() override val notificationsIntent by lazy { notifications.clicks() } override val themeIntent by lazy { themePrefs.clicks() } override val archiveIntent by lazy { archive.clicks() } @@ -87,6 +94,9 @@ class ConversationInfoActivity : QkThemedActivity(), ConversationInfoView { recipientAdapter.threadId = state.threadId recipientAdapter.updateData(state.recipients) + name.setVisible(state.recipients?.size ?: 0 >= 2) + name.summary = state.name + notifications.setVisible(!state.blocked) archive.setVisible(!state.blocked) @@ -103,6 +113,27 @@ class ConversationInfoActivity : QkThemedActivity(), ConversationInfoView { mediaAdapter.updateData(state.media) } + override fun showNameDialog(name: String) { + val editText = QkEditText(this).apply { + val padding = 8.dpToPx(this@ConversationInfoActivity) + setPadding(padding * 3, padding, padding * 3, padding) + setSingleLine(true) + setHint(R.string.info_name_hint) + setText(name) + setHintTextColor(context.resolveThemeColor(android.R.attr.textColorTertiary)) + setTextColor(context.resolveThemeColor(android.R.attr.textColorPrimary)) + filters = arrayOf(InputFilter.LengthFilter(30)) + background = null + } + + AlertDialog.Builder(this) + .setTitle(R.string.info_name) + .setView(editText) + .setPositiveButton(R.string.button_save, { _, _ -> nameChangedIntent.onNext(editText.text.toString()) }) + .setNegativeButton(R.string.button_cancel, null) + .show() + } + override fun showDeleteDialog() { AlertDialog.Builder(this) .setTitle(R.string.dialog_delete_title) diff --git a/presentation/src/main/java/feature/conversationinfo/ConversationInfoState.kt b/presentation/src/main/java/feature/conversationinfo/ConversationInfoState.kt index 6cbacc28c..eeedb5e0b 100644 --- a/presentation/src/main/java/feature/conversationinfo/ConversationInfoState.kt +++ b/presentation/src/main/java/feature/conversationinfo/ConversationInfoState.kt @@ -24,6 +24,7 @@ import model.MmsPart import model.Recipient data class ConversationInfoState( + val name: String = "", val recipients: RealmList? = null, val threadId: Long = 0, val archived: Boolean = false, diff --git a/presentation/src/main/java/feature/conversationinfo/ConversationInfoView.kt b/presentation/src/main/java/feature/conversationinfo/ConversationInfoView.kt index e556b0002..3425fd9ac 100644 --- a/presentation/src/main/java/feature/conversationinfo/ConversationInfoView.kt +++ b/presentation/src/main/java/feature/conversationinfo/ConversationInfoView.kt @@ -18,11 +18,13 @@ */ package feature.conversationinfo -import io.reactivex.Observable import common.base.QkView +import io.reactivex.Observable interface ConversationInfoView : QkView { + val nameIntent: Observable<*> + val nameChangedIntent: Observable val notificationsIntent: Observable val themeIntent: Observable val archiveIntent: Observable @@ -30,6 +32,7 @@ interface ConversationInfoView : QkView { val deleteIntent: Observable val confirmDeleteIntent: Observable + fun showNameDialog(name: String) fun showDeleteDialog() } \ No newline at end of file diff --git a/presentation/src/main/java/feature/conversationinfo/ConversationInfoViewModel.kt b/presentation/src/main/java/feature/conversationinfo/ConversationInfoViewModel.kt index 836ff5287..1ec427940 100644 --- a/presentation/src/main/java/feature/conversationinfo/ConversationInfoViewModel.kt +++ b/presentation/src/main/java/feature/conversationinfo/ConversationInfoViewModel.kt @@ -27,9 +27,10 @@ import interactor.MarkArchived import interactor.MarkBlocked import interactor.MarkUnarchived import interactor.MarkUnblocked -import io.reactivex.Observable import io.reactivex.rxkotlin.plusAssign import io.reactivex.rxkotlin.withLatestFrom +import io.reactivex.subjects.BehaviorSubject +import io.reactivex.subjects.Subject import model.Conversation import repository.MessageRepository import util.extensions.asObservable @@ -38,21 +39,21 @@ import javax.inject.Named class ConversationInfoViewModel @Inject constructor( @Named("threadId") threadId: Long, - messageRepo: MessageRepository, private val markArchived: MarkArchived, private val markUnarchived: MarkUnarchived, private val markBlocked: MarkBlocked, private val markUnblocked: MarkUnblocked, + private val messageRepo: MessageRepository, private val navigator: Navigator, private val deleteConversations: DeleteConversations ) : QkViewModel( ConversationInfoState(threadId = threadId, media = messageRepo.getPartsForConversation(threadId)) ) { - private val conversation: Observable + private val conversation: Subject = BehaviorSubject.create() init { - conversation = messageRepo.getConversationAsync(threadId) + disposables += messageRepo.getConversationAsync(threadId) .asObservable() .filter { conversation -> conversation.isLoaded } .doOnNext { conversation -> @@ -62,6 +63,7 @@ class ConversationInfoViewModel @Inject constructor( } .filter { conversation -> conversation.isValid } .filter { conversation -> conversation.id != 0L } + .subscribe(conversation::onNext) disposables += markArchived disposables += markUnarchived @@ -75,6 +77,12 @@ class ConversationInfoViewModel @Inject constructor( .distinctUntilChanged() .subscribe { recipients -> newState { copy(recipients = recipients) } } + // 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 } @@ -91,6 +99,21 @@ class ConversationInfoViewModel @Inject constructor( override fun bindView(view: ConversationInfoView) { super.bindView(view) + // Show the conversation title dialog + view.nameIntent + .withLatestFrom(conversation) { _, conversation -> conversation } + .map { conversation -> conversation.name } + .autoDisposable(view.scope()) + .subscribe(view::showNameDialog) + + // Set the conversation title + view.nameChangedIntent + .withLatestFrom(conversation) { name, conversation -> + messageRepo.setConversationName(conversation.id, name) + } + .autoDisposable(view.scope()) + .subscribe() + // Show the notifications settings for the conversation view.notificationsIntent .withLatestFrom(conversation, { _, conversation -> conversation }) diff --git a/presentation/src/main/res/drawable/ic_people_black_24dp.xml b/presentation/src/main/res/drawable/ic_people_black_24dp.xml new file mode 100644 index 000000000..0f4e3b15a --- /dev/null +++ b/presentation/src/main/res/drawable/ic_people_black_24dp.xml @@ -0,0 +1,27 @@ + + + + diff --git a/presentation/src/main/res/layout/conversation_info_activity.xml b/presentation/src/main/res/layout/conversation_info_activity.xml index c1c74b30b..174acdd4e 100644 --- a/presentation/src/main/res/layout/conversation_info_activity.xml +++ b/presentation/src/main/res/layout/conversation_info_activity.xml @@ -61,6 +61,14 @@ android:layout_marginBottom="8dp" android:background="?android:attr/divider" /> + + Failed to send. Tap to try again Details + Conversation title + Enter title Notifications Theme Archive @@ -215,6 +217,7 @@ Cancel Delete + Save More Set Unblock