mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-17 15:33:30 +01:00
Add the ability to filter search by date and author.
This commit is contained in:
committed by
Cody Henthorne
parent
72cbe61f6c
commit
7253aaaa14
@@ -458,6 +458,13 @@
|
||||
android:label="@string/AndroidManifest__select_contacts"
|
||||
android:windowSoftInputMode="stateHidden" />
|
||||
|
||||
<activity
|
||||
android:name=".search.SingleContactSelectionActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:exported="false"
|
||||
android:label="@string/AndroidManifest__select_contacts"
|
||||
android:windowSoftInputMode="stateHidden" />
|
||||
|
||||
<activity
|
||||
android:name=".giph.ui.GiphyActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
|
||||
@@ -1215,6 +1215,14 @@ class MainActivity :
|
||||
toolbarViewModel.setSearchQuery(query)
|
||||
}
|
||||
|
||||
override fun onSearchFilterClick() {
|
||||
supportFragmentManager.fragments.forEach { fragment ->
|
||||
if (fragment is ConversationListFragment) {
|
||||
fragment.showSearchFilterBottomSheet()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNotificationProfileTooltipDismissed() {
|
||||
SignalStore.notificationProfile.hasSeenTooltip = true
|
||||
toolbarViewModel.setShowNotificationProfilesTooltip(false)
|
||||
|
||||
@@ -10,4 +10,5 @@ sealed interface LabsSettingsEvents {
|
||||
data class ToggleStoryArchive(val enabled: Boolean) : LabsSettingsEvents
|
||||
data class ToggleIncognito(val enabled: Boolean) : LabsSettingsEvents
|
||||
data class ToggleGroupSuggestionsForMembers(val enabled: Boolean) : LabsSettingsEvents
|
||||
data class ToggleBetterSearch(val enabled: Boolean) : LabsSettingsEvents
|
||||
}
|
||||
|
||||
@@ -124,6 +124,15 @@ private fun LabsSettingsContent(
|
||||
onCheckChanged = { onEvent(LabsSettingsEvents.ToggleGroupSuggestionsForMembers(it)) }
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.ToggleRow(
|
||||
checked = state.betterSearch,
|
||||
text = "Better Search",
|
||||
label = "Filter search results by date range and author. Adds a filter button to the search toolbar.",
|
||||
onCheckChanged = { onEvent(LabsSettingsEvents.ToggleBetterSearch(it)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,5 +12,6 @@ data class LabsSettingsState(
|
||||
val individualChatPlaintextExport: Boolean = false,
|
||||
val storyArchive: Boolean = false,
|
||||
val incognito: Boolean = false,
|
||||
val groupSuggestionsForMembers: Boolean = false
|
||||
val groupSuggestionsForMembers: Boolean = false,
|
||||
val betterSearch: Boolean = false
|
||||
)
|
||||
|
||||
@@ -33,6 +33,10 @@ class LabsSettingsViewModel : ViewModel() {
|
||||
SignalStore.labs.groupSuggestionsForMembers = event.enabled
|
||||
_state.value = _state.value.copy(groupSuggestionsForMembers = event.enabled)
|
||||
}
|
||||
is LabsSettingsEvents.ToggleBetterSearch -> {
|
||||
SignalStore.labs.betterSearch = event.enabled
|
||||
_state.value = _state.value.copy(betterSearch = event.enabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +45,8 @@ class LabsSettingsViewModel : ViewModel() {
|
||||
individualChatPlaintextExport = SignalStore.labs.individualChatPlaintextExport,
|
||||
storyArchive = SignalStore.labs.storyArchive,
|
||||
incognito = SignalStore.labs.incognito,
|
||||
groupSuggestionsForMembers = SignalStore.labs.groupSuggestionsForMembers
|
||||
groupSuggestionsForMembers = SignalStore.labs.groupSuggestionsForMembers,
|
||||
betterSearch = SignalStore.labs.betterSearch
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@ package org.thoughtcrime.securesms.contacts.paged
|
||||
|
||||
import org.thoughtcrime.securesms.contacts.HeaderAction
|
||||
import org.thoughtcrime.securesms.database.RecipientTable
|
||||
import org.thoughtcrime.securesms.search.SearchFilter
|
||||
|
||||
/**
|
||||
* A strongly typed descriptor of how a given list of contacts should be formatted
|
||||
*/
|
||||
class ContactSearchConfiguration private constructor(
|
||||
val query: String?,
|
||||
val searchFilter: SearchFilter,
|
||||
val sections: List<Section>,
|
||||
val emptyStateSections: List<Section>
|
||||
) {
|
||||
@@ -335,6 +337,7 @@ class ContactSearchConfiguration private constructor(
|
||||
private val sections: MutableList<Section> = mutableListOf()
|
||||
|
||||
override var query: String? = null
|
||||
override var searchFilter: SearchFilter = SearchFilter.EMPTY
|
||||
|
||||
override fun addSection(section: Section) {
|
||||
sections.add(section)
|
||||
@@ -357,6 +360,7 @@ class ContactSearchConfiguration private constructor(
|
||||
private val emptyState = EmptyStateBuilder()
|
||||
|
||||
override var query: String? = null
|
||||
override var searchFilter: SearchFilter = SearchFilter.EMPTY
|
||||
|
||||
override fun addSection(section: Section) {
|
||||
sections.add(section)
|
||||
@@ -369,6 +373,7 @@ class ContactSearchConfiguration private constructor(
|
||||
fun build(): ContactSearchConfiguration {
|
||||
return ContactSearchConfiguration(
|
||||
query = query,
|
||||
searchFilter = searchFilter,
|
||||
sections = sections,
|
||||
emptyStateSections = emptyState.build()
|
||||
)
|
||||
@@ -380,6 +385,7 @@ class ContactSearchConfiguration private constructor(
|
||||
*/
|
||||
interface Builder {
|
||||
var query: String?
|
||||
var searchFilter: SearchFilter
|
||||
|
||||
fun arbitrary(first: String, vararg rest: String) {
|
||||
addSection(Section.Arbitrary(setOf(first) + rest.toSet()))
|
||||
|
||||
@@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.conversationlist.chatfilter.ConversationFilterRequest
|
||||
import org.thoughtcrime.securesms.groups.SelectionLimits
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.search.SearchFilter
|
||||
import org.thoughtcrime.securesms.search.SearchRepository
|
||||
import org.thoughtcrime.securesms.stories.settings.custom.PrivateStorySettingsFragment
|
||||
import org.thoughtcrime.securesms.stories.settings.my.MyStorySettingsFragment
|
||||
@@ -135,6 +136,10 @@ class ContactSearchMediator(
|
||||
viewModel.setConversationFilterRequest(conversationFilterRequest)
|
||||
}
|
||||
|
||||
fun onSearchFilterChanged(searchFilter: SearchFilter) {
|
||||
viewModel.setSearchFilter(searchFilter)
|
||||
}
|
||||
|
||||
fun setKeysSelected(keys: Set<ContactSearchKey>) {
|
||||
Log.d(TAG, "setKeysSelected() Keys: ${keys.map { it.toString() }}")
|
||||
viewModel.setKeysSelected(callbacks.onBeforeContactsSelected(null, keys))
|
||||
|
||||
@@ -457,7 +457,7 @@ class ContactSearchPagedDataSource(
|
||||
check(searchRepository != null)
|
||||
|
||||
if (searchCache.messageSearchResult == null && query != null) {
|
||||
searchCache = searchCache.copy(messageSearchResult = searchRepository.queryMessagesSync(query))
|
||||
searchCache = searchCache.copy(messageSearchResult = searchRepository.queryMessagesSync(query, contactConfiguration.searchFilter))
|
||||
}
|
||||
|
||||
return if (query != null) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.thoughtcrime.securesms.contacts.paged
|
||||
|
||||
import org.thoughtcrime.securesms.conversationlist.chatfilter.ConversationFilterRequest
|
||||
import org.thoughtcrime.securesms.search.SearchFilter
|
||||
|
||||
/**
|
||||
* Simple search state for contacts.
|
||||
@@ -9,5 +10,6 @@ data class ContactSearchState(
|
||||
val query: String? = null,
|
||||
val conversationFilterRequest: ConversationFilterRequest? = null,
|
||||
val expandedSections: Set<ContactSearchConfiguration.SectionKey> = emptySet(),
|
||||
val groupStories: Set<ContactSearchData.Story> = emptySet()
|
||||
val groupStories: Set<ContactSearchData.Story> = emptySet(),
|
||||
val searchFilter: SearchFilter = SearchFilter.EMPTY
|
||||
)
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.conversationlist.chatfilter.ConversationFilter
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode
|
||||
import org.thoughtcrime.securesms.groups.SelectionLimits
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.search.SearchFilter
|
||||
import org.thoughtcrime.securesms.search.SearchRepository
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
import org.whispersystems.signalservice.api.util.Preconditions
|
||||
@@ -93,6 +94,10 @@ class ContactSearchViewModel(
|
||||
configurationStore.update { it.copy(conversationFilterRequest = conversationFilterRequest) }
|
||||
}
|
||||
|
||||
fun setSearchFilter(searchFilter: SearchFilter) {
|
||||
configurationStore.update { it.copy(searchFilter = searchFilter) }
|
||||
}
|
||||
|
||||
fun expandSection(sectionKey: ContactSearchConfiguration.SectionKey) {
|
||||
configurationStore.update { it.copy(expandedSections = it.expandedSections + sectionKey) }
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
package org.thoughtcrime.securesms.conversationlist;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
@@ -35,11 +37,12 @@ import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import org.signal.core.ui.compose.Snackbars;
|
||||
import androidx.compose.ui.platform.ComposeView;
|
||||
@@ -64,10 +67,8 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
import org.signal.core.ui.compose.SignalIcons;
|
||||
import org.signal.core.util.DimensionUnit;
|
||||
import org.signal.core.util.Stopwatch;
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.concurrent.SimpleTask;
|
||||
@@ -113,12 +114,14 @@ import org.thoughtcrime.securesms.components.snackbars.SnackbarState;
|
||||
import org.thoughtcrime.securesms.components.spoiler.SpoilerAnnotation;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNotePlayerView;
|
||||
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode;
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchAdapter;
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration;
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchData;
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey;
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchMediator;
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchState;
|
||||
import org.thoughtcrime.securesms.contacts.selection.ContactSelectionArguments;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationUpdateTick;
|
||||
import org.thoughtcrime.securesms.conversationlist.chatfilter.ConversationFilterRequest;
|
||||
import org.thoughtcrime.securesms.conversationlist.chatfilter.ConversationFilterSource;
|
||||
@@ -148,6 +151,8 @@ import org.thoughtcrime.securesms.ratelimit.RecaptchaProofBottomSheetFragment;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.search.MessageResult;
|
||||
import org.thoughtcrime.securesms.search.SearchFilter;
|
||||
import org.thoughtcrime.securesms.search.SearchFilterBottomSheet;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.util.AppForegroundObserver;
|
||||
import org.thoughtcrime.securesms.util.AppStartup;
|
||||
@@ -232,6 +237,13 @@ public class ConversationListFragment extends MainFragment implements Conversati
|
||||
private MainToolbarViewModel mainToolbarViewModel;
|
||||
private ChatListBackHandler chatListBackHandler;
|
||||
|
||||
private SearchFilter activeSearchFilter = SearchFilter.EMPTY;
|
||||
private ActivityResultLauncher<Intent> searchFilterContactPickerLauncher;
|
||||
|
||||
private static final String EXTRA_FILTER_START_DATE = "filter_start_date";
|
||||
private static final String EXTRA_FILTER_END_DATE = "filter_end_date";
|
||||
private static final String EXTRA_FILTER_AUTHOR_ID = "filter_author_id";
|
||||
|
||||
private BannerManager bannerManager;
|
||||
private SignalProgressDialog progressDialog;
|
||||
|
||||
@@ -258,6 +270,33 @@ public class ConversationListFragment extends MainFragment implements Conversati
|
||||
startupStopwatch = new Stopwatch("startup");
|
||||
mainToolbarViewModel = new ViewModelProvider(requireActivity()).get(MainToolbarViewModel.class);
|
||||
mainNavigationViewModel = new ViewModelProvider(requireActivity(), new MainNavigationViewModel.Factory()).get(MainNavigationViewModel.class);
|
||||
|
||||
searchFilterContactPickerLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
Intent launchIntent = result.getData();
|
||||
long startDate = launchIntent != null ? launchIntent.getLongExtra(EXTRA_FILTER_START_DATE, -1) : -1;
|
||||
long endDate = launchIntent != null ? launchIntent.getLongExtra(EXTRA_FILTER_END_DATE, -1) : -1;
|
||||
String authorStr = launchIntent != null ? launchIntent.getStringExtra(EXTRA_FILTER_AUTHOR_ID) : null;
|
||||
|
||||
RecipientId selectedAuthor = authorStr != null ? RecipientId.from(Long.parseLong(authorStr)) : null;
|
||||
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
|
||||
List<RecipientId> recipients = result.getData().getParcelableArrayListExtra(
|
||||
org.thoughtcrime.securesms.search.SingleContactSelectionActivity.KEY_SELECTED_RECIPIENT
|
||||
);
|
||||
if (recipients != null && !recipients.isEmpty()) {
|
||||
selectedAuthor = recipients.get(0);
|
||||
}
|
||||
}
|
||||
|
||||
SearchFilterBottomSheet.show(
|
||||
getParentFragmentManager(),
|
||||
startDate != -1 ? startDate : null,
|
||||
endDate != -1 ? endDate : null,
|
||||
selectedAuthor
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -320,6 +359,8 @@ public class ConversationListFragment extends MainFragment implements Conversati
|
||||
|
||||
searchAdapter = contactSearchMediator.getAdapter();
|
||||
|
||||
initializeSearchFilterListener();
|
||||
|
||||
if (WindowSizeClassExtensionsKt.isHeightCompact(getWindowSizeClass(getResources()))) {
|
||||
ViewUtil.setBottomMargin(bottomActionBar, ViewUtil.getNavigationBarHeight(bottomActionBar));
|
||||
}
|
||||
@@ -570,6 +611,7 @@ public class ConversationListFragment extends MainFragment implements Conversati
|
||||
boolean unreadOnly = conversationFilterRequest != null && conversationFilterRequest.getFilter() == ConversationFilter.UNREAD;
|
||||
|
||||
builder.setQuery(state.getQuery());
|
||||
builder.setSearchFilter(state.getSearchFilter());
|
||||
builder.addSection(new ContactSearchConfiguration.Section.Chats(
|
||||
unreadOnly,
|
||||
true,
|
||||
@@ -714,6 +756,46 @@ public class ConversationListFragment extends MainFragment implements Conversati
|
||||
);
|
||||
}
|
||||
|
||||
private void initializeSearchFilterListener() {
|
||||
getParentFragmentManager().setFragmentResultListener(
|
||||
SearchFilterBottomSheet.REQUEST_KEY,
|
||||
getViewLifecycleOwner(),
|
||||
(requestKey, result) -> {
|
||||
String action = result.getString(SearchFilterBottomSheet.RESULT_ACTION, "");
|
||||
|
||||
switch (action) {
|
||||
case SearchFilterBottomSheet.ACTION_APPLY:
|
||||
long startDate = result.getLong(SearchFilterBottomSheet.RESULT_START_DATE, -1);
|
||||
long endDate = result.getLong(SearchFilterBottomSheet.RESULT_END_DATE, -1);
|
||||
String authorIdStr = result.getString(SearchFilterBottomSheet.RESULT_AUTHOR_ID);
|
||||
|
||||
activeSearchFilter = new SearchFilter(
|
||||
startDate != -1 ? startDate : null,
|
||||
endDate != -1 ? endDate : null,
|
||||
authorIdStr != null ? RecipientId.from(Long.parseLong(authorIdStr)) : null
|
||||
);
|
||||
mainToolbarViewModel.setHasActiveSearchFilter(!activeSearchFilter.isEmpty());
|
||||
contactSearchMediator.onSearchFilterChanged(activeSearchFilter);
|
||||
break;
|
||||
|
||||
case SearchFilterBottomSheet.ACTION_CLEAR:
|
||||
activeSearchFilter = SearchFilter.EMPTY;
|
||||
mainToolbarViewModel.setHasActiveSearchFilter(false);
|
||||
contactSearchMediator.onSearchFilterChanged(activeSearchFilter);
|
||||
break;
|
||||
|
||||
case SearchFilterBottomSheet.ACTION_SELECT_AUTHOR:
|
||||
searchFilterContactPickerLauncher.launch(new Intent(requireContext(), org.thoughtcrime.securesms.search.SingleContactSelectionActivity.class)
|
||||
.putExtra(ContactSelectionArguments.DISPLAY_MODE, ContactSelectionDisplayMode.FLAG_PUSH | ContactSelectionDisplayMode.FLAG_ACTIVE_GROUPS)
|
||||
.putExtra(EXTRA_FILTER_START_DATE, result.getLong(SearchFilterBottomSheet.RESULT_START_DATE, -1))
|
||||
.putExtra(EXTRA_FILTER_END_DATE, result.getLong(SearchFilterBottomSheet.RESULT_END_DATE, -1))
|
||||
.putExtra(EXTRA_FILTER_AUTHOR_ID, result.getString(SearchFilterBottomSheet.RESULT_AUTHOR_ID)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private void updateSearchToolbarHint(@NonNull ConversationFilterRequest conversationFilterRequest) {
|
||||
mainToolbarViewModel.setSearchHint(
|
||||
conversationFilterRequest.getFilter() == ConversationFilter.OFF ? R.string.SearchToolbar_search : R.string.SearchToolbar_search_unread_chats
|
||||
@@ -1663,6 +1745,15 @@ public class ConversationListFragment extends MainFragment implements Conversati
|
||||
startActivity(AppSettingsActivity.chatFolders(requireContext()));
|
||||
}
|
||||
|
||||
public void showSearchFilterBottomSheet() {
|
||||
SearchFilterBottomSheet.show(
|
||||
getParentFragmentManager(),
|
||||
activeSearchFilter.getStartDate(),
|
||||
activeSearchFilter.getEndDate(),
|
||||
activeSearchFilter.getAuthor()
|
||||
);
|
||||
}
|
||||
|
||||
private void onSearchOpen() {
|
||||
chatListBackHandler.setEnabled(true);
|
||||
}
|
||||
@@ -1672,6 +1763,10 @@ public class ConversationListFragment extends MainFragment implements Conversati
|
||||
setAdapter(defaultAdapter);
|
||||
}
|
||||
|
||||
activeSearchFilter = SearchFilter.EMPTY;
|
||||
mainToolbarViewModel.setHasActiveSearchFilter(false);
|
||||
contactSearchMediator.onSearchFilterChanged(activeSearchFilter);
|
||||
|
||||
chatListBackHandler.setEnabled(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -130,6 +130,43 @@ class SearchTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
|
||||
}
|
||||
}
|
||||
|
||||
fun queryMessages(query: String, filter: org.thoughtcrime.securesms.search.SearchFilter): Cursor? {
|
||||
if (filter.isEmpty) {
|
||||
return queryMessages(query)
|
||||
}
|
||||
|
||||
val fullTextSearchQuery = createFullTextSearchQuery(query)
|
||||
if (fullTextSearchQuery.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
val extraConditions = StringBuilder()
|
||||
val args = mutableListOf<String>(fullTextSearchQuery)
|
||||
|
||||
if (filter.startDate != null) {
|
||||
extraConditions.append(" AND ${MessageTable.TABLE_NAME}.${MessageTable.DATE_RECEIVED} >= ?")
|
||||
args.add(filter.startDate.toString())
|
||||
}
|
||||
|
||||
if (filter.endDate != null) {
|
||||
extraConditions.append(" AND ${MessageTable.TABLE_NAME}.${MessageTable.DATE_RECEIVED} <= ?")
|
||||
args.add(filter.endDate.toString())
|
||||
}
|
||||
|
||||
if (filter.author != null) {
|
||||
extraConditions.append(" AND ${MessageTable.TABLE_NAME}.${MessageTable.FROM_RECIPIENT_ID} = ?")
|
||||
args.add(filter.author.serialize())
|
||||
}
|
||||
|
||||
@Language("sql")
|
||||
val filteredQuery = MESSAGES_QUERY.replace(
|
||||
"ORDER BY ${MessageTable.DATE_RECEIVED} DESC",
|
||||
"$extraConditions ORDER BY ${MessageTable.DATE_RECEIVED} DESC"
|
||||
)
|
||||
|
||||
return readableDatabase.rawQuery(filteredQuery, args.toTypedArray())
|
||||
}
|
||||
|
||||
fun queryMessages(query: String, threadId: Long): Cursor? {
|
||||
val fullTextSearchQuery = createFullTextSearchQuery(query)
|
||||
return if (TextUtils.isEmpty(fullTextSearchQuery)) {
|
||||
|
||||
@@ -8,6 +8,7 @@ class LabsValues internal constructor(store: KeyValueStore) : SignalStoreValues(
|
||||
const val STORY_ARCHIVE: String = "labs.story_archive"
|
||||
const val INCOGNITO: String = "labs.incognito"
|
||||
const val GROUP_SUGGESTIONS_FOR_MEMBERS: String = "labs.group_suggestions_for_members"
|
||||
const val BETTER_SEARCH: String = "labs.better_search"
|
||||
}
|
||||
|
||||
public override fun onFirstEverAppLaunch() = Unit
|
||||
@@ -22,6 +23,8 @@ class LabsValues internal constructor(store: KeyValueStore) : SignalStoreValues(
|
||||
|
||||
var groupSuggestionsForMembers by booleanValue(GROUP_SUGGESTIONS_FOR_MEMBERS, true).falseForExternalUsers()
|
||||
|
||||
var betterSearch by booleanValue(BETTER_SEARCH, true).falseForExternalUsers()
|
||||
|
||||
private fun SignalStoreValueDelegate<Boolean>.falseForExternalUsers(): SignalStoreValueDelegate<Boolean> {
|
||||
return this.map { actualValue -> RemoteConfig.internalUser && actualValue }
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@@ -105,6 +106,7 @@ interface MainToolbarCallback {
|
||||
fun onCloseArchiveClick()
|
||||
fun onCloseActionModeClick()
|
||||
fun onSearchQueryUpdated(query: String)
|
||||
fun onSearchFilterClick()
|
||||
fun onNotificationProfileTooltipDismissed()
|
||||
|
||||
object Empty : MainToolbarCallback {
|
||||
@@ -127,6 +129,7 @@ interface MainToolbarCallback {
|
||||
override fun onCloseArchiveClick() = Unit
|
||||
override fun onCloseActionModeClick() = Unit
|
||||
override fun onSearchQueryUpdated(query: String) = Unit
|
||||
override fun onSearchFilterClick() = Unit
|
||||
override fun onNotificationProfileTooltipDismissed() = Unit
|
||||
}
|
||||
}
|
||||
@@ -164,6 +167,7 @@ data class MainToolbarState(
|
||||
val proxyState: ProxyState = ProxyState.NONE,
|
||||
@StringRes val searchHint: Int = R.string.SearchToolbar_search,
|
||||
val searchQuery: String = "",
|
||||
val hasActiveSearchFilter: Boolean = false,
|
||||
val actionModeCount: Int = 0
|
||||
) {
|
||||
enum class ProxyState(@DrawableRes val icon: Int) {
|
||||
@@ -264,21 +268,41 @@ private fun SearchToolbar(
|
||||
)
|
||||
}
|
||||
},
|
||||
trailingIcon = if (state.searchQuery.isNotEmpty()) {
|
||||
{
|
||||
IconButtons.IconButton(
|
||||
onClick = {
|
||||
callback.onSearchQueryUpdated("")
|
||||
trailingIcon = {
|
||||
Row {
|
||||
if (SignalStore.labs.betterSearch) {
|
||||
Box(contentAlignment = Alignment.TopEnd) {
|
||||
IconButtons.IconButton(
|
||||
onClick = callback::onSearchFilterClick
|
||||
) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.symbol_filter_24),
|
||||
contentDescription = stringResource(R.string.MainToolbar__search_filter_content_description)
|
||||
)
|
||||
}
|
||||
if (state.hasActiveSearchFilter) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(top = 8.dp, end = 8.dp)
|
||||
.size(8.dp)
|
||||
.background(color = MaterialTheme.colorScheme.primary, shape = CircleShape)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (state.searchQuery.isNotEmpty()) {
|
||||
IconButtons.IconButton(
|
||||
onClick = {
|
||||
callback.onSearchQueryUpdated("")
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_x_20),
|
||||
contentDescription = stringResource(R.string.MainToolbar__clear_search_content_description)
|
||||
)
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_x_20),
|
||||
contentDescription = stringResource(R.string.MainToolbar__clear_search_content_description)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
},
|
||||
contentPadding = PaddingValues(0.dp),
|
||||
colors = TextFieldDefaults.colors(
|
||||
|
||||
@@ -213,6 +213,12 @@ class MainToolbarViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun setHasActiveSearchFilter(hasActiveSearchFilter: Boolean) {
|
||||
internalStateFlow.update {
|
||||
it.copy(hasActiveSearchFilter = hasActiveSearchFilter)
|
||||
}
|
||||
}
|
||||
|
||||
private fun emitPossibleSearchStateChangeEvent(previousMode: MainToolbarMode, mode: MainToolbarMode) {
|
||||
if (previousMode == MainToolbarMode.SEARCH && mode != MainToolbarMode.SEARCH) {
|
||||
emitEvent(Event.Search.Close)
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.thoughtcrime.securesms.search
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
data class SearchFilter(
|
||||
val startDate: Long? = null,
|
||||
val endDate: Long? = null,
|
||||
val author: RecipientId? = null
|
||||
) {
|
||||
val isEmpty: Boolean
|
||||
get() = startDate == null && endDate == null && author == null
|
||||
|
||||
companion object {
|
||||
@JvmField
|
||||
val EMPTY = SearchFilter()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
package org.thoughtcrime.securesms.search
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import com.google.android.material.datepicker.MaterialDatePicker
|
||||
import org.signal.core.ui.BottomSheetUtil
|
||||
import org.signal.core.ui.compose.BottomSheets
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.ComposeBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import java.util.Locale
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
/**
|
||||
* A bottom sheet that allows you to aly additional filters to message search.
|
||||
*/
|
||||
class SearchFilterBottomSheet : ComposeBottomSheetDialogFragment() {
|
||||
|
||||
override val peekHeightPercentage: Float = 0.66f
|
||||
|
||||
companion object {
|
||||
const val REQUEST_KEY = "search_filter_result"
|
||||
const val RESULT_ACTION = "action"
|
||||
const val RESULT_START_DATE = "start_date"
|
||||
const val RESULT_END_DATE = "end_date"
|
||||
const val RESULT_AUTHOR_ID = "author_id"
|
||||
|
||||
const val ACTION_APPLY = "apply"
|
||||
const val ACTION_CLEAR = "clear"
|
||||
const val ACTION_SELECT_AUTHOR = "select_author"
|
||||
|
||||
private const val ARG_START_DATE = "arg_start_date"
|
||||
private const val ARG_END_DATE = "arg_end_date"
|
||||
private const val ARG_AUTHOR_ID = "arg_author_id"
|
||||
|
||||
@JvmStatic
|
||||
fun show(fragmentManager: FragmentManager, startDate: Long?, endDate: Long?, authorId: RecipientId?) {
|
||||
val fragment = SearchFilterBottomSheet().apply {
|
||||
arguments = bundleOf(
|
||||
ARG_START_DATE to (startDate ?: -1L),
|
||||
ARG_END_DATE to (endDate ?: -1L),
|
||||
ARG_AUTHOR_ID to authorId?.serialize()
|
||||
)
|
||||
}
|
||||
fragment.show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun SheetContent() {
|
||||
val context = LocalContext.current
|
||||
val args = requireArguments()
|
||||
|
||||
var startDate by remember {
|
||||
val v = args.getLong(ARG_START_DATE, -1L)
|
||||
mutableStateOf(if (v == -1L) null else v)
|
||||
}
|
||||
var endDate by remember {
|
||||
val v = args.getLong(ARG_END_DATE, -1L)
|
||||
mutableStateOf(if (v == -1L) null else v)
|
||||
}
|
||||
val authorIdRaw = remember { args.getString(ARG_AUTHOR_ID) }
|
||||
var authorId by remember { mutableStateOf(authorIdRaw?.let { RecipientId.from(it.toLong()) }) }
|
||||
|
||||
val authorName = remember(authorId) {
|
||||
authorId?.let {
|
||||
Recipient.resolved(it).getDisplayName(context)
|
||||
}
|
||||
}
|
||||
|
||||
val startDateText = remember(startDate) {
|
||||
startDate?.let { DateUtils.getDayPrecisionTimeString(context, Locale.getDefault(), it) }
|
||||
?: context.getString(R.string.SearchFilterBottomSheet__not_set)
|
||||
}
|
||||
|
||||
val endDateText = remember(endDate) {
|
||||
endDate?.let { DateUtils.getDayPrecisionTimeString(context, Locale.getDefault(), it) }
|
||||
?: context.getString(R.string.SearchFilterBottomSheet__not_set)
|
||||
}
|
||||
|
||||
SearchFilterSheetContent(
|
||||
startDateText = startDateText,
|
||||
endDateText = endDateText,
|
||||
authorName = authorName ?: stringResource(R.string.SearchFilterBottomSheet__anyone),
|
||||
onStartDateClick = {
|
||||
val datePicker = MaterialDatePicker.Builder.datePicker()
|
||||
.setTitleText(context.getString(R.string.SearchFilterBottomSheet__select_date))
|
||||
.apply {
|
||||
startDate?.let { setSelection(it) }
|
||||
}
|
||||
.build()
|
||||
|
||||
datePicker.addOnPositiveButtonClickListener { selection ->
|
||||
startDate = selection
|
||||
}
|
||||
datePicker.show(childFragmentManager, "START_DATE_PICKER")
|
||||
},
|
||||
onEndDateClick = {
|
||||
val datePicker = MaterialDatePicker.Builder.datePicker()
|
||||
.setTitleText(context.getString(R.string.SearchFilterBottomSheet__select_date))
|
||||
.apply {
|
||||
endDate?.let { setSelection(it) }
|
||||
}
|
||||
.build()
|
||||
|
||||
datePicker.addOnPositiveButtonClickListener { selection ->
|
||||
// Set to end of the selected day
|
||||
endDate = selection + 1.days.inWholeMilliseconds - 1
|
||||
}
|
||||
datePicker.show(childFragmentManager, "END_DATE_PICKER")
|
||||
},
|
||||
onAuthorClick = {
|
||||
setFragmentResult(
|
||||
REQUEST_KEY,
|
||||
bundleOf(
|
||||
RESULT_ACTION to ACTION_SELECT_AUTHOR,
|
||||
RESULT_START_DATE to (startDate ?: -1L),
|
||||
RESULT_END_DATE to (endDate ?: -1L),
|
||||
RESULT_AUTHOR_ID to authorId?.serialize()
|
||||
)
|
||||
)
|
||||
dismissAllowingStateLoss()
|
||||
},
|
||||
onApplyClick = {
|
||||
setFragmentResult(
|
||||
REQUEST_KEY,
|
||||
bundleOf(
|
||||
RESULT_ACTION to ACTION_APPLY,
|
||||
RESULT_START_DATE to (startDate ?: -1L),
|
||||
RESULT_END_DATE to (endDate ?: -1L),
|
||||
RESULT_AUTHOR_ID to authorId?.serialize()
|
||||
)
|
||||
)
|
||||
dismissAllowingStateLoss()
|
||||
},
|
||||
onClearClick = {
|
||||
setFragmentResult(
|
||||
REQUEST_KEY,
|
||||
bundleOf(
|
||||
RESULT_ACTION to ACTION_CLEAR
|
||||
)
|
||||
)
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SearchFilterSheetContent(
|
||||
startDateText: String,
|
||||
endDateText: String,
|
||||
authorName: String,
|
||||
onStartDateClick: () -> Unit,
|
||||
onEndDateClick: () -> Unit,
|
||||
onAuthorClick: () -> Unit,
|
||||
onApplyClick: () -> Unit,
|
||||
onClearClick: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
BottomSheets.Handle()
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.SearchFilterBottomSheet__filter_search),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.padding(top = 18.dp, bottom = 24.dp)
|
||||
)
|
||||
|
||||
FilterRow(
|
||||
label = stringResource(R.string.SearchFilterBottomSheet__start_date),
|
||||
value = startDateText,
|
||||
onClick = onStartDateClick
|
||||
)
|
||||
|
||||
FilterRow(
|
||||
label = stringResource(R.string.SearchFilterBottomSheet__end_date),
|
||||
value = endDateText,
|
||||
onClick = onEndDateClick
|
||||
)
|
||||
|
||||
FilterRow(
|
||||
label = stringResource(R.string.SearchFilterBottomSheet__author),
|
||||
value = authorName,
|
||||
onClick = onAuthorClick
|
||||
)
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.End),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 18.dp, end = 18.dp, top = 24.dp, bottom = 24.dp)
|
||||
) {
|
||||
Buttons.MediumTonal(
|
||||
onClick = onClearClick
|
||||
) {
|
||||
Text(stringResource(R.string.SearchFilterBottomSheet__clear))
|
||||
}
|
||||
|
||||
Buttons.MediumTonal(
|
||||
onClick = onApplyClick
|
||||
) {
|
||||
Text(stringResource(R.string.SearchFilterBottomSheet__apply))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FilterRow(
|
||||
label: String,
|
||||
value: String,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = 24.dp, vertical = 16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = value,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_expand_down_24),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(start = 8.dp)
|
||||
.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,12 +91,13 @@ public class SearchRepository {
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public @NonNull MessageSearchResult queryMessagesSync(@NonNull String query) {
|
||||
public @NonNull MessageSearchResult queryMessagesSync(@NonNull String query, @NonNull SearchFilter filter) {
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
List<MessageResult> messages = queryMessages(query);
|
||||
List<MessageResult> mentionMessages = queryMentions(convertMentionsQueryToTokens(query));
|
||||
List<MessageResult> combined = mergeMessagesAndMentions(messages, mentionMessages);
|
||||
List<MessageResult> messages = queryMessages(query, filter);
|
||||
List<MessageResult> mentionMessages = queryMentions(convertMentionsQueryToTokens(query));
|
||||
List<MessageResult> filteredMentions = filterMentionResults(mentionMessages, filter);
|
||||
List<MessageResult> combined = mergeMessagesAndMentions(messages, filteredMentions);
|
||||
|
||||
Log.d(TAG, "[messages] Search took " + (System.currentTimeMillis() - start) + " ms");
|
||||
|
||||
@@ -161,13 +162,13 @@ public class SearchRepository {
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull List<MessageResult> queryMessages(@NonNull String query) {
|
||||
private @NonNull List<MessageResult> queryMessages(@NonNull String query, @NonNull SearchFilter filter) {
|
||||
if (Util.isEmpty(query)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<MessageResult> results;
|
||||
try (Cursor cursor = searchDatabase.queryMessages(query)) {
|
||||
try (Cursor cursor = searchDatabase.queryMessages(query, filter)) {
|
||||
results = readToList(cursor, new MessageModelBuilder());
|
||||
}
|
||||
|
||||
@@ -216,7 +217,6 @@ public class SearchRepository {
|
||||
MessageStyler.style(result.getReceivedTimestampMs(), BodyRangeUtil.adjustBodyRanges(ranges, bodyAdjustments), (Spannable) updatedBody);
|
||||
|
||||
updatedSnippet = SpannableString.valueOf(updatedSnippet);
|
||||
//noinspection ConstantConditions
|
||||
updateSnippetWithStyles(result.getReceivedTimestampMs(), updatedBody, (SpannableString) updatedSnippet, BodyRangeUtil.adjustBodyRanges(ranges, snippetAdjustments));
|
||||
}
|
||||
|
||||
@@ -411,6 +411,28 @@ public class SearchRepository {
|
||||
}
|
||||
}
|
||||
|
||||
private static @NonNull List<MessageResult> filterMentionResults(@NonNull List<MessageResult> mentions, @NonNull SearchFilter filter) {
|
||||
if (filter.isEmpty()) {
|
||||
return mentions;
|
||||
}
|
||||
|
||||
List<MessageResult> filtered = new ArrayList<>();
|
||||
for (MessageResult mention : mentions) {
|
||||
if (filter.getStartDate() != null && mention.getReceivedTimestampMs() < filter.getStartDate()) {
|
||||
continue;
|
||||
}
|
||||
if (filter.getEndDate() != null && mention.getReceivedTimestampMs() > filter.getEndDate()) {
|
||||
continue;
|
||||
}
|
||||
if (filter.getAuthor() != null && !mention.getMessageRecipient().getId().equals(filter.getAuthor())) {
|
||||
continue;
|
||||
}
|
||||
filtered.add(mention);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
private static @NonNull List<MessageResult> mergeMessagesAndMentions(@NonNull List<MessageResult> messages, @NonNull List<MessageResult> mentionMessages) {
|
||||
Set<Long> includedMmsMessages = new HashSet<>();
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package org.thoughtcrime.securesms.search
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import org.thoughtcrime.securesms.ContactSelectionActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.contacts.paged.ChatType
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import java.util.Optional
|
||||
import java.util.function.Consumer
|
||||
|
||||
/**
|
||||
* A single-select contact picker that returns immediately when a contact is tapped.
|
||||
*/
|
||||
class SingleContactSelectionActivity : ContactSelectionActivity() {
|
||||
override fun onCreate(icicle: Bundle?, ready: Boolean) {
|
||||
super.onCreate(icicle, ready)
|
||||
toolbar.setNavigationIcon(R.drawable.ic_arrow_left_24)
|
||||
toolbar.setNavigationOnClickListener { _: View? ->
|
||||
setResult(RESULT_CANCELED, Intent(intent))
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBeforeContactSelected(isFromUnknownSearchKey: Boolean, recipientId: Optional<RecipientId>, number: String?, chatType: Optional<ChatType>, callback: Consumer<Boolean>) {
|
||||
callback.accept(true)
|
||||
if (recipientId.isPresent) {
|
||||
val result = Intent(intent)
|
||||
result.putParcelableArrayListExtra(KEY_SELECTED_RECIPIENT, ArrayList(mutableListOf(recipientId.get())))
|
||||
setResult(RESULT_OK, result)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSelectionChanged() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KEY_SELECTED_RECIPIENT: String = "selected_recipient"
|
||||
}
|
||||
}
|
||||
@@ -9341,6 +9341,27 @@
|
||||
<string name="MainToolbar__notification_profile_content_description">Notification Profile</string>
|
||||
<!-- Accessibility label for more options button in MainToolbar -->
|
||||
<string name="MainToolbar__proxy_content_description">Proxy</string>
|
||||
<!-- Accessibility label for search filter button in MainToolbar -->
|
||||
<string name="MainToolbar__search_filter_content_description" translatable="false">Search filter</string>
|
||||
|
||||
<!-- SearchFilterBottomSheet: Title -->
|
||||
<string name="SearchFilterBottomSheet__filter_search" translatable="false">Filter search</string>
|
||||
<!-- SearchFilterBottomSheet: Start date label -->
|
||||
<string name="SearchFilterBottomSheet__start_date" translatable="false">Start date</string>
|
||||
<!-- SearchFilterBottomSheet: End date label -->
|
||||
<string name="SearchFilterBottomSheet__end_date" translatable="false">End date</string>
|
||||
<!-- SearchFilterBottomSheet: Author label -->
|
||||
<string name="SearchFilterBottomSheet__author" translatable="false">Author</string>
|
||||
<!-- SearchFilterBottomSheet: Placeholder for unset date -->
|
||||
<string name="SearchFilterBottomSheet__not_set" translatable="false">Not set</string>
|
||||
<!-- SearchFilterBottomSheet: Placeholder for unset author -->
|
||||
<string name="SearchFilterBottomSheet__anyone" translatable="false">Anyone</string>
|
||||
<!-- SearchFilterBottomSheet: Apply button -->
|
||||
<string name="SearchFilterBottomSheet__apply" translatable="false">Apply</string>
|
||||
<!-- SearchFilterBottomSheet: Clear button -->
|
||||
<string name="SearchFilterBottomSheet__clear" translatable="false">Clear</string>
|
||||
<!-- SearchFilterBottomSheet: Select date dialog title -->
|
||||
<string name="SearchFilterBottomSheet__select_date" translatable="false">Select date</string>
|
||||
|
||||
<!-- Accessibility label for a button displayed in the toolbar to return to the previous screen. -->
|
||||
<string name="DefaultTopAppBar__navigate_up_content_description">Navigate up</string>
|
||||
|
||||
Reference in New Issue
Block a user