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 92e14040..ddeece80 100644 --- a/data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt +++ b/data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt @@ -36,7 +36,7 @@ class QkRealmMigration @Inject constructor( ) : RealmMigration { companion object { - const val SchemaVersion: Long = 11 + const val SchemaVersion: Long = 12 } @SuppressLint("ApplySharedPref") @@ -233,6 +233,14 @@ class QkRealmMigration @Inject constructor( version++ } + if (version == 11L) { + realm.schema.get("ScheduledMessage") + ?.addField("conversationId", Long::class.java, FieldAttribute.REQUIRED) + // Because there was never any property associated with which conversation/recipients a scheduled message was for, + // we can't update this field on a realm migration. It will be set to a default of 0 + + version++ + } check(version >= newVersion) { "Migration missing from v$oldVersion to v$newVersion" } } diff --git a/data/src/main/java/com/moez/QKSMS/repository/ScheduledMessageRepositoryImpl.kt b/data/src/main/java/com/moez/QKSMS/repository/ScheduledMessageRepositoryImpl.kt index 8892afe2..774efeaf 100644 --- a/data/src/main/java/com/moez/QKSMS/repository/ScheduledMessageRepositoryImpl.kt +++ b/data/src/main/java/com/moez/QKSMS/repository/ScheduledMessageRepositoryImpl.kt @@ -21,7 +21,8 @@ class ScheduledMessageRepositoryImpl @Inject constructor() : ScheduledMessageRep recipients: List, sendAsGroup: Boolean, body: String, - attachments: List + attachments: List, + conversationId: Long ): ScheduledMessage { Realm.getDefaultInstance().use { realm -> val id = (realm @@ -34,7 +35,7 @@ class ScheduledMessageRepositoryImpl @Inject constructor() : ScheduledMessageRep val attachmentsRealmList = RealmList(*attachments.toTypedArray()) val message = ScheduledMessage(id, date, subId, recipientsRealmList, sendAsGroup, body, - attachmentsRealmList) + attachmentsRealmList, conversationId) realm.executeTransaction { realm.insertOrUpdate(message) } @@ -63,6 +64,13 @@ class ScheduledMessageRepositoryImpl @Inject constructor() : ScheduledMessageRep .findFirst() } + override fun getScheduledMessagesForConversation(conversationId: Long): RealmResults { + return Realm.getDefaultInstance() + .where(ScheduledMessage::class.java) + .equalTo("conversationId", conversationId) + .findAllAsync() + } + override fun deleteScheduledMessage(id: Long) { val subscription = Completable.fromAction { Realm.getDefaultInstance().use { realm -> diff --git a/domain/src/main/java/com/moez/QKSMS/interactor/AddScheduledMessage.kt b/domain/src/main/java/com/moez/QKSMS/interactor/AddScheduledMessage.kt index a4fe3c5f..89762b8c 100644 --- a/domain/src/main/java/com/moez/QKSMS/interactor/AddScheduledMessage.kt +++ b/domain/src/main/java/com/moez/QKSMS/interactor/AddScheduledMessage.kt @@ -41,7 +41,8 @@ class AddScheduledMessage @Inject constructor( val recipients: List, val sendAsGroup: Boolean, val body: String, - val attachments: List + val attachments: List, + val conversationId: Long ) @SuppressLint("Range") @@ -50,7 +51,13 @@ class AddScheduledMessage @Inject constructor( return Flowable.just(params) .map { // step 1 - save, as-is, to db to get primary key id scheduledMessageRepo.saveScheduledMessage( - it.date, it.subId, it.recipients, it.sendAsGroup, it.body, it.attachments + it.date, + it.subId, + it.recipients, + it.sendAsGroup, + it.body, + it.attachments, + it.conversationId ) } .map { scheduledMessageDb -> diff --git a/domain/src/main/java/com/moez/QKSMS/model/ScheduledMessage.kt b/domain/src/main/java/com/moez/QKSMS/model/ScheduledMessage.kt index a49c486b..09f586a1 100644 --- a/domain/src/main/java/com/moez/QKSMS/model/ScheduledMessage.kt +++ b/domain/src/main/java/com/moez/QKSMS/model/ScheduledMessage.kt @@ -29,7 +29,8 @@ open class ScheduledMessage( var recipients: RealmList = RealmList(), var sendAsGroup: Boolean = true, var body: String = "", - var attachments: RealmList = RealmList() + var attachments: RealmList = RealmList(), + var conversationId: Long = 0 ) : RealmObject() { fun copy( @@ -39,10 +40,11 @@ open class ScheduledMessage( recipients: RealmList = this.recipients, sendAsGroup: Boolean = this.sendAsGroup, body: String = this.body, - attachments: RealmList = this.attachments + attachments: RealmList = this.attachments, + conversationId: Long = this.conversationId ): ScheduledMessage { - return ScheduledMessage(id, date, subId, recipients, sendAsGroup, body, attachments) + return ScheduledMessage(id, date, subId, recipients, sendAsGroup, body, attachments, conversationId) } } \ No newline at end of file diff --git a/domain/src/main/java/com/moez/QKSMS/repository/ScheduledMessageRepository.kt b/domain/src/main/java/com/moez/QKSMS/repository/ScheduledMessageRepository.kt index 386faebc..53a0c2eb 100644 --- a/domain/src/main/java/com/moez/QKSMS/repository/ScheduledMessageRepository.kt +++ b/domain/src/main/java/com/moez/QKSMS/repository/ScheduledMessageRepository.kt @@ -18,7 +18,9 @@ */ package dev.octoshrimpy.quik.repository +import dev.octoshrimpy.quik.model.Recipient import dev.octoshrimpy.quik.model.ScheduledMessage +import io.realm.RealmList import io.realm.RealmResults interface ScheduledMessageRepository { @@ -32,7 +34,8 @@ interface ScheduledMessageRepository { recipients: List, sendAsGroup: Boolean, body: String, - attachments: List + attachments: List, + conversationId: Long ): ScheduledMessage /** @@ -50,6 +53,11 @@ interface ScheduledMessageRepository { */ fun getScheduledMessage(id: Long): ScheduledMessage? + /** + * Returns all scheduled messages with the given [conversationId] + */ + fun getScheduledMessagesForConversation(conversationId: Long): RealmResults + /** * Deletes the scheduled message with the given [id] */ 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 775c2810..d867e794 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/Navigator.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/Navigator.kt @@ -160,8 +160,9 @@ class Navigator @Inject constructor( startActivity(Intent(context, BackupActivity::class.java)) } - fun showScheduled() { + fun showScheduled(conversationId: Long?) { val intent = Intent(context, ScheduledActivity::class.java) + conversationId?.let { intent.putExtra("conversationId", it) } startActivity(intent) } 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 c3caa97a..36a15b28 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 @@ -469,6 +469,8 @@ class ComposeActivity : QkThemedActivity(), ComposeView { // Don't set the adapters unless needed if (state.editingMode && chips.adapter == null) chips.adapter = chipsAdapter + toolbar.menu.findItem(R.id.viewScheduledMessages)?.isVisible = !state.editingMode && state.selectedMessages == 0 + && state.query.isEmpty() && state.hasScheduledMessages toolbar.menu.findItem(R.id.select_all)?.isVisible = !state.editingMode && (messageAdapter.itemCount > 1) && state.selectedMessages != 0 toolbar.menu.findItem(R.id.add)?.isVisible = state.editingMode toolbar.menu.findItem(R.id.call)?.isVisible = !state.editingMode && state.selectedMessages == 0 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 ebd448fa..81b5bd05 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 @@ -47,6 +47,7 @@ data class ComposeState( val remaining: String = "", val subscription: SubscriptionInfoCompat? = null, val canSend: Boolean = false, + val hasScheduledMessages: Boolean = false, val validRecipientNumbers: Int = 1, val audioMsgRecording: Boolean = false, val saveDraft: 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 64077b07..b8a91763 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 @@ -72,6 +72,7 @@ import dev.octoshrimpy.quik.model.getText import dev.octoshrimpy.quik.repository.ContactRepository import dev.octoshrimpy.quik.repository.ConversationRepository import dev.octoshrimpy.quik.repository.MessageRepository +import dev.octoshrimpy.quik.repository.ScheduledMessageRepository import dev.octoshrimpy.quik.util.ActiveSubscriptionObservable import dev.octoshrimpy.quik.util.FileUtils import dev.octoshrimpy.quik.util.PhoneNumberUtils @@ -114,6 +115,7 @@ class ComposeViewModel @Inject constructor( private val markRead: MarkRead, private val messageDetailsFormatter: MessageDetailsFormatter, private val messageRepo: MessageRepository, + private val scheduledMessageRepo: ScheduledMessageRepository, private val navigator: Navigator, private val permissionManager: PermissionManager, private val phoneNumberUtils: PhoneNumberUtils, @@ -265,6 +267,22 @@ class ComposeViewModel @Inject constructor( newState { copy(subscription = sub) } }.subscribe() + // checks if there are any scheduled messages in convo + disposables += conversation + .distinctUntilChanged { conversation -> conversation.id } + .observeOn(AndroidSchedulers.mainThread()) + .switchMap { conversation -> + scheduledMessageRepo + .getScheduledMessagesForConversation(conversation.id) + .asFlowable() + .toObservable() + } + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { liveResults -> + val hasAny = liveResults.isNotEmpty() + newState { copy(hasScheduledMessages = hasAny) } + } + // actions if (mode == "scheduling") newState { copy(scheduling = true) } @@ -338,6 +356,15 @@ class ComposeViewModel @Inject constructor( .autoDisposable(view.scope()) .subscribe { newState { copy() } } + // Show scheduled messages + view.optionsItemIntent + .filter {it == R.id.viewScheduledMessages} + .withLatestFrom(state, conversation) + .autoDisposable(view.scope()) + .subscribe { (_, _, conversation) -> + navigator.showScheduled(conversation.id) + } + // toggle select all / select none view.optionsItemIntent .filter { it == R.id.select_all } @@ -1099,6 +1126,7 @@ class ComposeViewModel @Inject constructor( } val subId = state.subscription?.subscriptionId ?: -1 + val conversationId = (conversation.id) val addresses = when (conversation.recipients.isNotEmpty()) { true -> conversation.recipients.map { it.address } false -> chips.map { chip -> chip.address } @@ -1116,10 +1144,11 @@ class ComposeViewModel @Inject constructor( addresses, sendAsGroup, body.toString(), - state.attachments.map { it.uri.toString() } + state.attachments.map { it.uri.toString() }, + conversationId ) ).also { - newState { copy(scheduled = 0) } + newState { copy(scheduled = 0, hasScheduledMessages = true ) } showScheduledToast = true } 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 ec51803f..fae962d1 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 @@ -24,6 +24,7 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.core.text.buildSpannedString import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView import dev.octoshrimpy.quik.R import dev.octoshrimpy.quik.common.Navigator import dev.octoshrimpy.quik.common.base.QkRealmAdapter @@ -33,7 +34,9 @@ import dev.octoshrimpy.quik.common.util.DateFormatter import dev.octoshrimpy.quik.common.util.extensions.resolveThemeColor import dev.octoshrimpy.quik.common.util.extensions.setTint import dev.octoshrimpy.quik.model.Conversation +import dev.octoshrimpy.quik.repository.ScheduledMessageRepository import dev.octoshrimpy.quik.util.PhoneNumberUtils +import io.reactivex.disposables.CompositeDisposable import kotlinx.android.synthetic.main.conversation_list_item.* import kotlinx.android.synthetic.main.conversation_list_item.view.* import javax.inject.Inject @@ -42,9 +45,11 @@ class ConversationsAdapter @Inject constructor( private val colors: Colors, private val context: Context, private val dateFormatter: DateFormatter, + private val scheduledMessageRepo: ScheduledMessageRepository, private val navigator: Navigator, private val phoneNumberUtils: PhoneNumberUtils ) : QkRealmAdapter() { + private val disposables = CompositeDisposable() init { // This is how we access the threadId for the swipe actions @@ -117,6 +122,16 @@ class ConversationsAdapter @Inject constructor( // Make the preview in italics if draft if (conversation.draft.isNotEmpty()) holder.snippet.setTypeface(null, Typeface.ITALIC) + // Get Scheduled Messages + val disposable = scheduledMessageRepo + .getScheduledMessagesForConversation(conversation.id) + .asFlowable() + .toObservable() + .subscribe { messages -> + holder.scheduled.isVisible = messages.isNotEmpty() + } + disposables.add(disposable) + holder.pinned.isVisible = conversation.pinned holder.unread.setTint(theme) } @@ -129,4 +144,10 @@ class ConversationsAdapter @Inject constructor( return if (getItem(position)?.unread == false) 0 else 1 } + override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { + super.onDetachedFromRecyclerView(recyclerView) + disposables.clear() + } + + } 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 5da9edcf..5407b85e 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 @@ -317,7 +317,7 @@ class MainViewModel @Inject constructor( else -> newState { copy(hasError = true) } } NavItem.BACKUP -> navigator.showBackup() - NavItem.SCHEDULED -> navigator.showScheduled() + NavItem.SCHEDULED -> navigator.showScheduled(null) NavItem.BLOCKING -> navigator.showBlockedConversations() NavItem.SETTINGS -> navigator.showSettings() // NavItem.PLUS -> navigator.showQksmsPlusActivity("main_menu") diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/plus/PlusViewModel.kt b/presentation/src/main/java/com/moez/QKSMS/feature/plus/PlusViewModel.kt index ced17068..8f84e6cc 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/plus/PlusViewModel.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/plus/PlusViewModel.kt @@ -66,7 +66,7 @@ class PlusViewModel @Inject constructor( view.scheduleClicks .autoDisposable(view.scope()) - .subscribe { navigator.showScheduled() } + .subscribe { navigator.showScheduled(null) } view.backupClicks .autoDisposable(view.scope()) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/scheduled/ScheduledActivity.kt b/presentation/src/main/java/com/moez/QKSMS/feature/scheduled/ScheduledActivity.kt index a09be06e..5be1d40e 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/scheduled/ScheduledActivity.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/scheduled/ScheduledActivity.kt @@ -99,7 +99,7 @@ class ScheduledActivity : QkThemedActivity(), ScheduledView { ((scheduledMessageAdapter.itemCount != 0) && (state.selectedMessages == 1)) // show compose button - compose.isVisible = state.upgraded + compose.isVisible = state.upgraded && (state.conversationId == null) upgrade.isVisible = !state.upgraded } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/scheduled/ScheduledActivityModule.kt b/presentation/src/main/java/com/moez/QKSMS/feature/scheduled/ScheduledActivityModule.kt index 03735b05..333040b1 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/scheduled/ScheduledActivityModule.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/scheduled/ScheduledActivityModule.kt @@ -23,6 +23,7 @@ import dagger.Module import dagger.Provides import dagger.multibindings.IntoMap import dev.octoshrimpy.quik.injection.ViewModelKey +import javax.inject.Named @Module class ScheduledActivityModule { @@ -32,4 +33,9 @@ class ScheduledActivityModule { @ViewModelKey(ScheduledViewModel::class) fun provideScheduledViewModel(viewModel: ScheduledViewModel): ViewModel = viewModel -} \ No newline at end of file + @Provides + @Named("conversationId") + fun getConversationId(activity: ScheduledActivity): + Long? = activity.intent.extras?.getLong("conversationId") + +} diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/scheduled/ScheduledState.kt b/presentation/src/main/java/com/moez/QKSMS/feature/scheduled/ScheduledState.kt index 92d42439..25b790e7 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/scheduled/ScheduledState.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/scheduled/ScheduledState.kt @@ -25,4 +25,5 @@ data class ScheduledState( val scheduledMessages: RealmResults? = null, val upgraded: Boolean = true, val selectedMessages: Int = 0, + val conversationId: Long? = null, ) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/scheduled/ScheduledViewModel.kt b/presentation/src/main/java/com/moez/QKSMS/feature/scheduled/ScheduledViewModel.kt index d2608ec9..e57c504b 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/scheduled/ScheduledViewModel.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/scheduled/ScheduledViewModel.kt @@ -15,8 +15,10 @@ import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.rxkotlin.plusAssign import io.reactivex.schedulers.Schedulers import javax.inject.Inject +import javax.inject.Named class ScheduledViewModel @Inject constructor( + @Named("conversationId") private val conversationId: Long?, billingManager: BillingManager, private val context: Context, private val navigator: Navigator, @@ -24,10 +26,12 @@ class ScheduledViewModel @Inject constructor( private val sendScheduledMessageInteractor: SendScheduledMessage, private val deleteScheduledMessagesInteractor: DeleteScheduledMessages, ) : QkViewModel(ScheduledState( - scheduledMessages = scheduledMessageRepo.getScheduledMessages() + scheduledMessages = scheduledMessageRepo.getScheduledMessages(), + conversationId = conversationId )) { init { + loadMessages(conversationId) disposables += billingManager.upgradeStatus .subscribe { upgraded -> newState { copy(upgraded = upgraded) } } } @@ -157,4 +161,12 @@ class ScheduledViewModel @Inject constructor( .autoDisposable(view.scope()) .subscribe { navigator.showQksmsPlusActivity("schedule_fab") } } + + private fun loadMessages(conversationId: Long?) { + val results = if (conversationId != null) + scheduledMessageRepo.getScheduledMessagesForConversation(conversationId) + else + scheduledMessageRepo.getScheduledMessages() + newState { copy(scheduledMessages = results) } + } } diff --git a/presentation/src/main/res/layout/conversation_list_item.xml b/presentation/src/main/res/layout/conversation_list_item.xml index 524c714d..e528615c 100644 --- a/presentation/src/main/res/layout/conversation_list_item.xml +++ b/presentation/src/main/res/layout/conversation_list_item.xml @@ -69,7 +69,7 @@ android:ellipsize="end" android:maxLines="1" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toStartOf="@id/pinned" + app:layout_constraintEnd_toStartOf="@id/scheduled" app:layout_constraintStart_toStartOf="@id/title" app:layout_constraintTop_toBottomOf="@id/title" tools:text="@tools:sample/lorem/random" /> @@ -80,9 +80,23 @@ android:layout_height="16dp" android:layout_marginTop="2dp" android:paddingStart="4dp" + android:paddingTop="2dp" android:src="@drawable/ic_baseline_push_pin_24" android:tint="?android:attr/textColorTertiary" android:visibility="gone" + app:layout_constraintEnd_toStartOf="@id/scheduled" + app:layout_constraintTop_toTopOf="@id/snippet" + tools:visibility="visible" /> + + diff --git a/presentation/src/main/res/menu/compose.xml b/presentation/src/main/res/menu/compose.xml index 87ff23a5..c88f5315 100644 --- a/presentation/src/main/res/menu/compose.xml +++ b/presentation/src/main/res/menu/compose.xml @@ -34,6 +34,13 @@ android:visible="false" app:showAsAction="always" /> + + Selected time must be in the future! You must unlock QUIK+ to use scheduled messaging Added to scheduled messages + Scheduled messages for this conversation Write a message… Can\'t send to other participants Copy text