mirror of
https://github.com/quik-sms/quik.git
synced 2026-11-15 18:48:52 +01:00
#678 - Allow deciding between sending as group or individual
This commit is contained in:
@@ -24,6 +24,7 @@ import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.widget.Toast;
|
||||
import com.klinker.android.logger.Log;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* @hide
|
||||
@@ -52,6 +53,7 @@ public final class SqliteWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Cursor query(Context context, ContentResolver resolver, Uri uri,
|
||||
String[] projection, String selection, String[] selectionArgs, String sortOrder) {
|
||||
try {
|
||||
@@ -63,17 +65,6 @@ public final class SqliteWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public static boolean requery(Context context, Cursor cursor) {
|
||||
try {
|
||||
return cursor.requery();
|
||||
} catch (SQLiteException e) {
|
||||
Log.e(TAG, "Catch a SQLiteException when requery: ", e);
|
||||
checkSQLiteException(context, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static int update(Context context, ContentResolver resolver, Uri uri,
|
||||
ContentValues values, String where, String[] selectionArgs) {
|
||||
try {
|
||||
|
||||
@@ -24,6 +24,7 @@ import android.content.ContentUris
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.database.sqlite.SqliteWrapper
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
@@ -37,9 +38,6 @@ import com.klinker.android.send_message.Settings
|
||||
import com.klinker.android.send_message.StripAccents
|
||||
import com.klinker.android.send_message.Transaction
|
||||
import filter.ConversationFilter
|
||||
import io.reactivex.Maybe
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import io.realm.Case
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmResults
|
||||
@@ -56,8 +54,6 @@ import model.SearchResult
|
||||
import util.MessageUtils
|
||||
import util.Preferences
|
||||
import util.extensions.anyOf
|
||||
import util.extensions.asMaybe
|
||||
import util.extensions.insertOrUpdate
|
||||
import util.extensions.map
|
||||
import java.io.ByteArrayOutputStream
|
||||
import javax.inject.Inject
|
||||
@@ -141,39 +137,32 @@ class MessageRepositoryImpl @Inject constructor(
|
||||
return getConversation(threadId) ?: getConversationFromCp(threadId)
|
||||
}
|
||||
|
||||
override fun getOrCreateConversation(address: String): Maybe<Conversation> {
|
||||
override fun getOrCreateConversation(address: String): Conversation? {
|
||||
return getOrCreateConversation(listOf(address))
|
||||
}
|
||||
|
||||
override fun getOrCreateConversation(addresses: List<String>): Maybe<Conversation> {
|
||||
return Maybe.just(addresses)
|
||||
.map { recipients ->
|
||||
recipients.map { address ->
|
||||
when (MessageUtils.isEmailAddress(address)) {
|
||||
true -> MessageUtils.extractAddrSpec(address)
|
||||
false -> address
|
||||
}
|
||||
override fun getOrCreateConversation(addresses: List<String>): Conversation? {
|
||||
return addresses
|
||||
.map { address ->
|
||||
when (MessageUtils.isEmailAddress(address)) {
|
||||
true -> MessageUtils.extractAddrSpec(address)
|
||||
false -> address
|
||||
}
|
||||
}
|
||||
.map { recipients ->
|
||||
.let { recipients ->
|
||||
Uri.parse("content://mms-sms/threadID").buildUpon().apply {
|
||||
recipients.forEach { recipient -> appendQueryParameter("recipient", recipient) }
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
.flatMap { uriBuilder ->
|
||||
context.contentResolver.query(uriBuilder.build(), arrayOf(BaseColumns._ID), null, null, null).asMaybe()
|
||||
}
|
||||
.map { cursor -> cursor.getLong(0) }
|
||||
.filter { threadId -> threadId != 0L }
|
||||
.map { threadId ->
|
||||
.let { uri -> SqliteWrapper.query(context, context.contentResolver, uri, arrayOf(BaseColumns._ID), null, null, null) }
|
||||
?.use { cursor -> if (cursor.moveToFirst()) cursor.getLong(0) else null }
|
||||
?.takeIf { threadId -> threadId != 0L }
|
||||
?.let { threadId ->
|
||||
var conversation = getConversation(threadId)
|
||||
if (conversation != null) conversation = Realm.getDefaultInstance().copyFromRealm(conversation)
|
||||
|
||||
conversation ?: getConversationFromCp(threadId)?.apply { insertOrUpdate() } ?: Conversation()
|
||||
conversation ?: getConversationFromCp(threadId)
|
||||
}
|
||||
.onErrorReturn { Conversation() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
override fun saveDraft(threadId: Long, draft: String) {
|
||||
@@ -561,7 +550,7 @@ class MessageRepositoryImpl @Inject constructor(
|
||||
this.subId = subId
|
||||
|
||||
id = messageIds.newId()
|
||||
threadId = getOrCreateConversation(address).blockingGet().id
|
||||
threadId = getOrCreateConversation(address)?.id ?: 0L
|
||||
boxId = Telephony.Sms.MESSAGE_TYPE_INBOX
|
||||
type = "sms"
|
||||
}
|
||||
@@ -755,7 +744,7 @@ class MessageRepositoryImpl @Inject constructor(
|
||||
|
||||
conversation.recipients.clear()
|
||||
conversation.recipients.addAll(recipients)
|
||||
conversation.insertOrUpdate()
|
||||
realm.executeTransaction { it.insertOrUpdate(conversation) }
|
||||
realm.close()
|
||||
|
||||
conversation
|
||||
|
||||
@@ -39,7 +39,7 @@ class HeadlessSmsSendService : IntentService("HeadlessSmsSendService") {
|
||||
intent.extras?.getString(Intent.EXTRA_TEXT)?.takeIf { it.isNotBlank() }?.let { body ->
|
||||
val intentUri = intent.data
|
||||
val recipients = getRecipients(intentUri).split(";")
|
||||
val threadId = messageRepo.getOrCreateConversation(recipients).blockingGet()?.id ?: 0L
|
||||
val threadId = messageRepo.getOrCreateConversation(recipients)?.id ?: 0L
|
||||
sendMessage.execute(SendMessage.Params(-1, threadId, recipients, body))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
*/
|
||||
package repository
|
||||
|
||||
import io.reactivex.Maybe
|
||||
import io.realm.RealmResults
|
||||
import model.Attachment
|
||||
import model.Conversation
|
||||
@@ -42,9 +41,9 @@ interface MessageRepository {
|
||||
|
||||
fun getOrCreateConversation(threadId: Long): Conversation?
|
||||
|
||||
fun getOrCreateConversation(address: String): Maybe<Conversation>
|
||||
fun getOrCreateConversation(address: String): Conversation?
|
||||
|
||||
fun getOrCreateConversation(addresses: List<String>): Maybe<Conversation>
|
||||
fun getOrCreateConversation(addresses: List<String>): Conversation?
|
||||
|
||||
fun saveDraft(threadId: Long, draft: String)
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ class ComposeActivity : QkThemedActivity(), ComposeView {
|
||||
override val chipDeletedIntent: Subject<Contact> by lazy { chipsAdapter.chipDeleted }
|
||||
override val menuReadyIntent: Observable<Unit> = menu.map { Unit }
|
||||
override val optionsItemIntent: Subject<Int> = PublishSubject.create()
|
||||
override val sendAsGroupIntent by lazy { sendAsGroupBackground.clicks() }
|
||||
override val messageClickIntent: Subject<Message> by lazy { messageAdapter.clicks }
|
||||
override val messagesSelectedIntent by lazy { messageAdapter.selectionChanges }
|
||||
override val cancelSendingIntent: Subject<Message> by lazy { messageAdapter.cancelSending }
|
||||
@@ -185,6 +186,11 @@ class ComposeActivity : QkThemedActivity(), ComposeView {
|
||||
|
||||
chipsAdapter.data = state.selectedContacts
|
||||
contactsAdapter.data = state.contacts
|
||||
|
||||
sendAsGroup.setVisible(state.editingMode && state.selectedContacts.size >= 2)
|
||||
sendAsGroupSwitch.isChecked = state.sendAsGroup
|
||||
|
||||
messageList.setVisible(state.sendAsGroup)
|
||||
messageAdapter.data = state.messages
|
||||
messageAdapter.highlight = state.searchSelectionId
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ data class ComposeState(
|
||||
val contactsVisible: Boolean = false,
|
||||
val selectedConversation: Long = 0,
|
||||
val selectedContacts: List<Contact> = ArrayList(),
|
||||
val sendAsGroup: Boolean = true,
|
||||
val conversationtitle: String = "",
|
||||
val query: String = "",
|
||||
val searchSelectionId: Long = -1,
|
||||
|
||||
@@ -37,6 +37,7 @@ interface ComposeView : QkView<ComposeState> {
|
||||
val chipDeletedIntent: Subject<Contact>
|
||||
val menuReadyIntent: Observable<Unit>
|
||||
val optionsItemIntent: Observable<Int>
|
||||
val sendAsGroupIntent: Observable<*>
|
||||
val messageClickIntent: Subject<Message>
|
||||
val messagesSelectedIntent: Observable<List<Long>>
|
||||
val cancelSendingIntent: Subject<Message>
|
||||
|
||||
@@ -127,7 +127,10 @@ class ComposeViewModel @Inject constructor(
|
||||
|
||||
address.isNotBlank() -> {
|
||||
newState { it.copy(editingMode = false) }
|
||||
messageRepo.getOrCreateConversation(address).toObservable()
|
||||
Observable.just(address)
|
||||
.mapNotNull { messageRepo.getOrCreateConversation(it) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
else -> {
|
||||
@@ -146,7 +149,7 @@ class ComposeViewModel @Inject constructor(
|
||||
.skipUntil(state.filter { state -> state.editingMode })
|
||||
.takeUntil(state.filter { state -> !state.editingMode })
|
||||
.map { contacts -> contacts.map { it.numbers.firstOrNull()?.address ?: "" } }
|
||||
.flatMapMaybe { addresses -> messageRepo.getOrCreateConversation(addresses) }
|
||||
.mapNotNull { addresses -> messageRepo.getOrCreateConversation(addresses) }
|
||||
.mergeWith(initialConversation)
|
||||
.filter { conversation -> conversation.isLoaded }
|
||||
.doOnNext { conversation ->
|
||||
@@ -379,6 +382,12 @@ class ComposeViewModel @Inject constructor(
|
||||
.subscribe { newState { it.copy(query = "", searchSelectionId = -1) } }
|
||||
|
||||
|
||||
// Toggle the group sending mode
|
||||
view.sendAsGroupIntent
|
||||
.autoDisposable(view.scope())
|
||||
.subscribe { newState { it.copy(sendAsGroup = !it.sendAsGroup) } }
|
||||
|
||||
|
||||
// Scroll to search position
|
||||
searchSelection
|
||||
.filter { id -> id != -1L }
|
||||
@@ -524,17 +533,31 @@ class ComposeViewModel @Inject constructor(
|
||||
view.sendIntent
|
||||
.withLatestFrom(view.textChangedIntent, { _, body -> body })
|
||||
.map { body -> body.toString() }
|
||||
.withLatestFrom(state, attachments, conversation, { body, state, attachments, conversation ->
|
||||
.withLatestFrom(state, attachments, conversation, selectedContacts, { body, state, attachments, conversation, contacts ->
|
||||
val subId = state.subscription?.subscriptionId ?: -1
|
||||
val threadId = conversation.id
|
||||
val addresses = conversation.recipients.map { it.address }
|
||||
sendMessage.execute(SendMessage.Params(subId, threadId, addresses, body, attachments))
|
||||
|
||||
if (state.sendAsGroup) {
|
||||
val threadId = conversation.id
|
||||
val addresses = conversation.recipients.map { it.address }
|
||||
sendMessage.execute(SendMessage.Params(subId, threadId, addresses, body, attachments))
|
||||
} else {
|
||||
contacts
|
||||
.map { contact -> contact.numbers }
|
||||
.mapNotNull { numbers -> numbers.firstOrNull() }
|
||||
.map { number -> number.address }
|
||||
.mapNotNull { address -> messageRepo.getOrCreateConversation(address) }
|
||||
.filter { it.recipients.isNotEmpty() }
|
||||
.forEach {
|
||||
val address = it.recipients.map { it.address }
|
||||
sendMessage.execute(SendMessage.Params(subId, it.id, address, body, attachments))
|
||||
}
|
||||
}
|
||||
|
||||
view.setDraft("")
|
||||
this.attachments.onNext(ArrayList())
|
||||
})
|
||||
.withLatestFrom(state, { _, state ->
|
||||
|
||||
if (state.editingMode) {
|
||||
newState { it.copy(editingMode = false) }
|
||||
newState { it.copy(editingMode = false, sendAsGroup = true, hasError = !state.sendAsGroup) }
|
||||
}
|
||||
})
|
||||
.autoDisposable(view.scope())
|
||||
|
||||
@@ -287,12 +287,69 @@
|
||||
|
||||
</android.support.v7.widget.Toolbar>
|
||||
|
||||
<android.support.constraint.Group
|
||||
android:id="@+id/sendAsGroup"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:constraint_referenced_ids="sendAsGroupBackground, sendAsGroupTitle, sendAsGroupSummary, sendAsGroupSwitch, sendAsGroupShadow" />
|
||||
|
||||
<View
|
||||
android:id="@+id/sendAsGroupBackground"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:background="?attr/colorPrimary"
|
||||
app:layout_constraintBottom_toBottomOf="@id/sendAsGroupSummary"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar" />
|
||||
|
||||
<common.widget.QkTextView
|
||||
android:id="@+id/sendAsGroupTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/compose_send_group_title"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toStartOf="@id/sendAsGroupSwitch"
|
||||
app:layout_constraintStart_toStartOf="@id/sendAsGroupBackground"
|
||||
app:layout_constraintTop_toTopOf="@id/sendAsGroupBackground"
|
||||
app:textSize="primary" />
|
||||
|
||||
<common.widget.QkTextView
|
||||
android:id="@+id/sendAsGroupSummary"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="16dp"
|
||||
android:text="@string/compose_send_group_summary"
|
||||
android:textColor="?android:attr/textColorTertiary"
|
||||
app:layout_constraintEnd_toEndOf="@id/sendAsGroupTitle"
|
||||
app:layout_constraintStart_toStartOf="@id/sendAsGroupTitle"
|
||||
app:layout_constraintTop_toBottomOf="@id/sendAsGroupTitle"
|
||||
app:textSize="secondary" />
|
||||
|
||||
<common.widget.QkSwitch
|
||||
android:id="@+id/sendAsGroupSwitch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:checked="true"
|
||||
android:clickable="false"
|
||||
app:layout_constraintBottom_toBottomOf="@id/sendAsGroupBackground"
|
||||
app:layout_constraintEnd_toEndOf="@id/sendAsGroupBackground"
|
||||
app:layout_constraintTop_toTopOf="@id/sendAsGroupBackground" />
|
||||
|
||||
<View
|
||||
android:id="@+id/sendAsGroupShadow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="8dp"
|
||||
android:background="@drawable/ab_shadow"
|
||||
app:layout_constraintTop_toBottomOf="@id/sendAsGroupBackground" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="8dp"
|
||||
android:background="@drawable/ab_shadow"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar" />
|
||||
|
||||
<View
|
||||
|
||||
@@ -83,6 +83,8 @@
|
||||
|
||||
<string name="compose_title_selected">%d selected</string>
|
||||
<string name="compose_subtitle_results">%1$d of %2$d results</string>
|
||||
<string name="compose_send_group_title">Send as group message</string>
|
||||
<string name="compose_send_group_summary">Recipients and replies will be visible to everyone</string>
|
||||
<string name="compose_messages_empty">This is the start of your conversation. Say something nice!</string>
|
||||
<string name="compose_hint">Write a message…</string>
|
||||
<string name="compose_menu_copy">Copy text</string>
|
||||
|
||||
Reference in New Issue
Block a user