Add an internal feature to search your contacts by ID/ACI/PNI.
This commit is contained in:
parent
7c28d8ad51
commit
7318e676f7
6 changed files with 272 additions and 0 deletions
|
@ -128,6 +128,14 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
|||
|
||||
sectionHeaderPref(DSLSettingsText.from("Miscellaneous"))
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from("Search for a recipient"),
|
||||
summary = DSLSettingsText.from("Search by ID, ACI, or PNI."),
|
||||
onClick = {
|
||||
findNavController().safeNavigate(InternalSettingsFragmentDirections.actionInternalSettingsFragmentToInternalSearchFragment())
|
||||
}
|
||||
)
|
||||
|
||||
switchPref(
|
||||
title = DSLSettingsText.from("'Internal Details' button"),
|
||||
summary = DSLSettingsText.from("Show a button in conversation settings that lets you see more information about a user."),
|
||||
|
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.internal.search
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.fragment.app.viewModels
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment
|
||||
import java.util.UUID
|
||||
|
||||
class InternalSearchFragment : ComposeFragment() {
|
||||
|
||||
val viewModel: InternalSearchViewModel by viewModels()
|
||||
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
val results: ImmutableList<InternalSearchResult> by viewModel.results
|
||||
val query: String by viewModel.query
|
||||
|
||||
InternalSearchFragmentScreen(
|
||||
query = query,
|
||||
results = results,
|
||||
onSearchUpdated = { viewModel.onQueryChanged(it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InternalSearchFragmentScreen(query: String, results: ImmutableList<InternalSearchResult>, onSearchUpdated: (String) -> Unit, modifier: Modifier = Modifier) {
|
||||
LazyColumn(
|
||||
modifier = modifier.fillMaxWidth()
|
||||
) {
|
||||
item(key = -1) {
|
||||
SearchBar(query, onSearchUpdated)
|
||||
}
|
||||
results.forEach { recipient ->
|
||||
item(key = recipient.id) {
|
||||
ResultItem(recipient)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SearchBar(query: String, onSearchUpdated: (String) -> Unit, modifier: Modifier = Modifier) {
|
||||
TextField(
|
||||
value = query,
|
||||
onValueChange = onSearchUpdated,
|
||||
placeholder = { Text(text = "Search by ID, ACI, or PNI") },
|
||||
modifier = modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ResultItem(result: InternalSearchResult, modifier: Modifier = Modifier) {
|
||||
val activity = LocalContext.current as? AppCompatActivity
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
if (activity != null) {
|
||||
RecipientBottomSheetDialogFragment.create(result.id, result.groupId).show(activity.supportFragmentManager, "TAG")
|
||||
}
|
||||
}
|
||||
.padding(8.dp)
|
||||
) {
|
||||
Text(text = result.name, style = MaterialTheme.typography.titleSmall)
|
||||
Text(text = "ID: ${result.id}")
|
||||
Text(text = "ACI: ${result.aci ?: "null"}")
|
||||
Text(text = "PNI: ${result.pni ?: "null"}")
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun InternalSearchScreenPreviewLightTheme() {
|
||||
SignalTheme(isDarkMode = false) {
|
||||
Surface {
|
||||
InternalSearchScreenPreview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun InternalSearchScreenPreviewDarkTheme() {
|
||||
SignalTheme(isDarkMode = true) {
|
||||
Surface {
|
||||
InternalSearchScreenPreview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InternalSearchScreenPreview() {
|
||||
InternalSearchFragmentScreen(
|
||||
query = "",
|
||||
results = persistentListOf(
|
||||
InternalSearchResult(
|
||||
name = "Peter Parker",
|
||||
id = RecipientId.from(1),
|
||||
aci = UUID.randomUUID().toString(),
|
||||
pni = UUID.randomUUID().toString()
|
||||
),
|
||||
InternalSearchResult(
|
||||
name = "Mary Jane",
|
||||
id = RecipientId.from(2),
|
||||
aci = UUID.randomUUID().toString(),
|
||||
pni = null
|
||||
)
|
||||
),
|
||||
onSearchUpdated = {}
|
||||
)
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.internal.search
|
||||
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
data class InternalSearchResult(
|
||||
val name: String,
|
||||
val id: RecipientId,
|
||||
val aci: String? = null,
|
||||
val pni: String? = null,
|
||||
val groupId: GroupId? = null
|
||||
)
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.internal.search
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import io.reactivex.rxjava3.subjects.BehaviorSubject
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import org.thoughtcrime.securesms.database.RecipientTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.RecipientRecord
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class InternalSearchViewModel : ViewModel() {
|
||||
|
||||
private val _results: MutableState<ImmutableList<InternalSearchResult>> = mutableStateOf(persistentListOf())
|
||||
val results: State<ImmutableList<InternalSearchResult>> = _results
|
||||
|
||||
private val _query: MutableState<String> = mutableStateOf("")
|
||||
val query: State<String> = _query
|
||||
|
||||
private val disposable: CompositeDisposable = CompositeDisposable()
|
||||
|
||||
private val querySubject: BehaviorSubject<String> = BehaviorSubject.create()
|
||||
|
||||
init {
|
||||
disposable += querySubject
|
||||
.distinctUntilChanged()
|
||||
.debounce(250, TimeUnit.MILLISECONDS, Schedulers.io())
|
||||
.observeOn(Schedulers.io())
|
||||
.map { query ->
|
||||
SignalDatabase.recipients.queryByInternalFields(query)
|
||||
.map { record ->
|
||||
InternalSearchResult(
|
||||
id = record.id,
|
||||
name = record.displayName(),
|
||||
aci = record.serviceId?.toString(),
|
||||
pni = record.pni.toString(),
|
||||
groupId = record.groupId
|
||||
)
|
||||
}
|
||||
.toImmutableList()
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { results ->
|
||||
_results.value = results
|
||||
}
|
||||
}
|
||||
|
||||
fun onQueryChanged(value: String) {
|
||||
_query.value = value
|
||||
querySubject.onNext(value)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
disposable.clear()
|
||||
}
|
||||
|
||||
private fun RecipientRecord.displayName(): String {
|
||||
return when {
|
||||
this.groupType == RecipientTable.GroupType.SIGNAL_V1 -> "GV1::${this.groupId}"
|
||||
this.groupType == RecipientTable.GroupType.SIGNAL_V2 -> "GV2::${this.groupId}"
|
||||
this.groupType == RecipientTable.GroupType.MMS -> "MMS_GROUP::${this.groupId}"
|
||||
this.groupType == RecipientTable.GroupType.DISTRIBUTION_LIST -> "DLIST::${this.distributionListId}"
|
||||
this.systemDisplayName?.isNotBlank() == true -> this.systemDisplayName
|
||||
this.signalProfileName.toString().isNotBlank() -> this.signalProfileName.serialize()
|
||||
this.e164 != null -> this.e164
|
||||
else -> "Unknown"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,6 +23,7 @@ import org.signal.core.util.optionalInt
|
|||
import org.signal.core.util.optionalLong
|
||||
import org.signal.core.util.optionalString
|
||||
import org.signal.core.util.or
|
||||
import org.signal.core.util.readToList
|
||||
import org.signal.core.util.readToSet
|
||||
import org.signal.core.util.readToSingleBoolean
|
||||
import org.signal.core.util.readToSingleLong
|
||||
|
@ -3140,6 +3141,21 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
|||
}
|
||||
}
|
||||
|
||||
fun queryByInternalFields(query: String): List<RecipientRecord> {
|
||||
if (query.isBlank()) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
return readableDatabase
|
||||
.select()
|
||||
.from(TABLE_NAME)
|
||||
.where("$ID LIKE ? OR $SERVICE_ID LIKE ? OR $PNI_COLUMN LIKE ?", "%$query%", "%$query%", "%$query%")
|
||||
.run()
|
||||
.readToList { cursor ->
|
||||
getRecord(context, cursor)
|
||||
}
|
||||
}
|
||||
|
||||
fun getSignalContacts(includeSelf: Boolean): Cursor? {
|
||||
return getSignalContacts(includeSelf, "$SORT_NAME, $SYSTEM_JOINED_NAME, $SEARCH_PROFILE_NAME, $USERNAME, $PHONE")
|
||||
}
|
||||
|
|
|
@ -581,6 +581,9 @@
|
|||
<action
|
||||
android:id="@+id/action_internalSettingsFragment_to_storyDialogsLauncherFragment"
|
||||
app:destination="@id/storyDialogsLauncherFragment" />
|
||||
<action
|
||||
android:id="@+id/action_internalSettingsFragment_to_internalSearchFragment"
|
||||
app:destination="@id/internalSearchFragment" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
|
@ -593,6 +596,11 @@
|
|||
android:name="org.thoughtcrime.securesms.components.settings.app.internal.InternalStoryDialogLauncherFragment"
|
||||
android:label="story_dialogs_launcher_fragment" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/internalSearchFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.internal.search.InternalSearchFragment"
|
||||
android:label="internal_search_fragment" />
|
||||
|
||||
<!-- endregion -->
|
||||
|
||||
<!-- Subscriptions -->
|
||||
|
|
Loading…
Add table
Reference in a new issue